Skip to content

Commit 7b86ead

Browse files
committed
r/aws_sagemaker_image_version: read correct version after create
Previously the read operation would always retrieve the latest image version matching the `image_name` argument, rather than tracking the version created by a given resource instance. This change updates the `id` attribute to a comma-delimited string concatenating `image_name` and `version`, enabling subsequent read operations to correctly match the version created by this resource, and supporting import of existing versions which are not the most recently created. It also fixes issues observed when managing multiple image versions of the same name in a single workspace. ```console % SAGEMAKER_IMAGE_VERSION_BASE_IMAGE="<redacted>" make testacc PKG=sagemaker TESTS=TestAccSageMakerImageVersion_ make: Verifying source code with gofmt... ==> Checking that code complies with gofmt requirements... TF_ACC=1 go1.23.8 test ./internal/service/sagemaker/... -v -count 1 -parallel 20 -run='TestAccSageMakerImageVersion_' -timeout 360m -vet=off 2025/05/08 10:39:43 Initializing Terraform AWS Provider... --- PASS: TestAccSageMakerImageVersion_Disappears_image (80.09s) --- PASS: TestAccSageMakerImageVersion_basic (82.82s) --- PASS: TestAccSageMakerImageVersion_disappears (84.49s) --- PASS: TestAccSageMakerImageVersion_multiple (88.17s) --- PASS: TestAccSageMakerImageVersion_update (91.47s) --- PASS: TestAccSageMakerImageVersion_upgrade_V5_98_0 (102.47s) PASS ok github.com/hashicorp/terraform-provider-aws/internal/service/sagemaker 108.081s ```
1 parent e2f9641 commit 7b86ead

File tree

9 files changed

+405
-62
lines changed

9 files changed

+405
-62
lines changed

.changelog/42536.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```release-note:bug
2+
resource/aws_sagemaker_image_version: Read the correct image version after creation rather than always fetching the latest
3+
```
4+
```release-note:breaking-change
5+
resource/aws_sagemaker_image_version: `id` is now a comma-delimited string concatenating `image_name` and `version`
6+
```

internal/service/sagemaker/exports_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var (
4949
FindHubByName = findHubByName
5050
FindHumanTaskUIByName = findHumanTaskUIByName
5151
FindImageByName = findImageByName
52-
FindImageVersionByName = findImageVersionByName
52+
FindImageVersionByTwoPartKey = findImageVersionByTwoPartKey
5353
FindMlflowTrackingServerByName = findMlflowTrackingServerByName
5454
FindModelByName = findModelByName
5555
FindModelPackageGroupByName = findModelPackageGroupByName

internal/service/sagemaker/image_version.go

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package sagemaker
66
import (
77
"context"
88
"log"
9+
"strconv"
910

1011
"github.com/YakDriver/regexache"
1112
"github.com/aws/aws-sdk-go-v2/aws"
@@ -20,21 +21,34 @@ import (
2021
"github.com/hashicorp/terraform-provider-aws/internal/enum"
2122
"github.com/hashicorp/terraform-provider-aws/internal/errs"
2223
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
24+
"github.com/hashicorp/terraform-provider-aws/internal/flex"
2325
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
2426
"github.com/hashicorp/terraform-provider-aws/names"
2527
)
2628

29+
const imageVersionResourcePartCount = 2
30+
2731
// @SDKResource("aws_sagemaker_image_version", name="Image Version")
2832
func resourceImageVersion() *schema.Resource {
2933
return &schema.Resource{
3034
CreateWithoutTimeout: resourceImageVersionCreate,
3135
ReadWithoutTimeout: resourceImageVersionRead,
3236
UpdateWithoutTimeout: resourceImageVersionUpdate,
3337
DeleteWithoutTimeout: resourceImageVersionDelete,
38+
3439
Importer: &schema.ResourceImporter{
3540
StateContext: schema.ImportStatePassthroughContext,
3641
},
3742

43+
StateUpgraders: []schema.StateUpgrader{
44+
{
45+
Type: resourceImageVersionV0().CoreConfigSchema().ImpliedType(),
46+
Upgrade: imageVersionStateUpgradeV0,
47+
Version: 0,
48+
},
49+
},
50+
51+
SchemaVersion: 1,
3852
Schema: map[string]*schema.Schema{
3953
names.AttrARN: {
4054
Type: schema.TypeString,
@@ -105,7 +119,7 @@ func resourceImageVersionCreate(ctx context.Context, d *schema.ResourceData, met
105119
conn := meta.(*conns.AWSClient).SageMakerClient(ctx)
106120

107121
name := d.Get("image_name").(string)
108-
input := &sagemaker.CreateImageVersionInput{
122+
input := sagemaker.CreateImageVersionInput{
109123
ImageName: aws.String(name),
110124
BaseImage: aws.String(d.Get("base_image").(string)),
111125
ClientToken: aws.String(id.UniqueId()),
@@ -139,25 +153,37 @@ func resourceImageVersionCreate(ctx context.Context, d *schema.ResourceData, met
139153
input.ProgrammingLang = aws.String(v.(string))
140154
}
141155

142-
_, err := conn.CreateImageVersion(ctx, input)
143-
if err != nil {
156+
if _, err := conn.CreateImageVersion(ctx, &input); err != nil {
144157
return sdkdiag.AppendErrorf(diags, "creating SageMaker AI Image Version %s: %s", name, err)
145158
}
146159

147-
d.SetId(name)
148-
149-
if _, err := waitImageVersionCreated(ctx, conn, d.Id()); err != nil {
160+
out, err := waitImageVersionCreated(ctx, conn, name)
161+
if err != nil {
150162
return sdkdiag.AppendErrorf(diags, "waiting for SageMaker AI Image Version (%s) to be created: %s", d.Id(), err)
151163
}
152164

165+
parts := []string{name, strconv.Itoa(int(aws.ToInt32(out.Version)))}
166+
id, err := flex.FlattenResourceId(parts, imageVersionResourcePartCount, false)
167+
if err != nil {
168+
return sdkdiag.AppendErrorf(diags, "creating SageMaker AI Image Version %s: %s", name, err)
169+
}
170+
171+
d.SetId(id)
172+
153173
return append(diags, resourceImageVersionRead(ctx, d, meta)...)
154174
}
155175

156176
func resourceImageVersionRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
157177
var diags diag.Diagnostics
158178
conn := meta.(*conns.AWSClient).SageMakerClient(ctx)
159179

160-
image, err := findImageVersionByName(ctx, conn, d.Id())
180+
name, version, err := expandImageVersionResourceID(d.Id())
181+
if err != nil {
182+
return sdkdiag.AppendErrorf(diags, "reading SageMaker AI Image Version (%s): %s", d.Id(), err)
183+
}
184+
d.Set("image_name", name) // to support import
185+
186+
image, err := findImageVersionByTwoPartKey(ctx, conn, name, version)
161187

162188
if !d.IsNewResource() && tfresource.NotFound(err) {
163189
d.SetId("")
@@ -174,7 +200,6 @@ func resourceImageVersionRead(ctx context.Context, d *schema.ResourceData, meta
174200
d.Set("image_arn", image.ImageArn)
175201
d.Set("container_image", image.ContainerImage)
176202
d.Set(names.AttrVersion, image.Version)
177-
d.Set("image_name", d.Id())
178203
d.Set("horovod", image.Horovod)
179204
d.Set("job_type", image.JobType)
180205
d.Set("processor", image.Processor)
@@ -190,9 +215,12 @@ func resourceImageVersionUpdate(ctx context.Context, d *schema.ResourceData, met
190215
var diags diag.Diagnostics
191216
conn := meta.(*conns.AWSClient).SageMakerClient(ctx)
192217

193-
input := &sagemaker.UpdateImageVersionInput{
194-
ImageName: aws.String(d.Id()),
195-
Version: aws.Int32(int32(d.Get(names.AttrVersion).(int))),
218+
name := d.Get("image_name").(string)
219+
version := d.Get(names.AttrVersion).(int)
220+
221+
input := sagemaker.UpdateImageVersionInput{
222+
ImageName: aws.String(name),
223+
Version: aws.Int32(int32(version)),
196224
}
197225

198226
if d.HasChange("horovod") {
@@ -223,7 +251,7 @@ func resourceImageVersionUpdate(ctx context.Context, d *schema.ResourceData, met
223251
input.ProgrammingLang = aws.String(d.Get("programming_lang").(string))
224252
}
225253

226-
if _, err := conn.UpdateImageVersion(ctx, input); err != nil {
254+
if _, err := conn.UpdateImageVersion(ctx, &input); err != nil {
227255
return sdkdiag.AppendErrorf(diags, "updating SageMaker AI Image Version (%s): %s", d.Id(), err)
228256
}
229257

@@ -234,9 +262,12 @@ func resourceImageVersionDelete(ctx context.Context, d *schema.ResourceData, met
234262
var diags diag.Diagnostics
235263
conn := meta.(*conns.AWSClient).SageMakerClient(ctx)
236264

265+
name := d.Get("image_name").(string)
266+
version := d.Get(names.AttrVersion).(int)
267+
237268
input := &sagemaker.DeleteImageVersionInput{
238-
ImageName: aws.String(d.Id()),
239-
Version: aws.Int32(int32(d.Get(names.AttrVersion).(int))),
269+
ImageName: aws.String(name),
270+
Version: aws.Int32(int32(version)),
240271
}
241272

242273
if _, err := conn.DeleteImageVersion(ctx, input); err != nil {
@@ -246,19 +277,24 @@ func resourceImageVersionDelete(ctx context.Context, d *schema.ResourceData, met
246277
return sdkdiag.AppendErrorf(diags, "deleting SageMaker AI Image Version (%s): %s", d.Id(), err)
247278
}
248279

249-
if _, err := waitImageVersionDeleted(ctx, conn, d.Id()); err != nil {
250-
return sdkdiag.AppendErrorf(diags, "waiting for SageMaker AI Image Version (%s) to delete: %s", d.Id(), err)
280+
if _, err := waitImageVersionDeleted(ctx, conn, name, version); err != nil {
281+
return sdkdiag.AppendErrorf(diags, "waiting for SageMaker AI Image Version (%s) deletion: %s", d.Id(), err)
251282
}
252283

253284
return diags
254285
}
255286

287+
// findImageVersionByName is used immediately after creation to poll for status of
288+
// the most recently created image version
289+
//
290+
// findImageVersionByTwoPartKey should be used for all subsequent operations once the
291+
// version number is assigned.
256292
func findImageVersionByName(ctx context.Context, conn *sagemaker.Client, name string) (*sagemaker.DescribeImageVersionOutput, error) {
257-
input := &sagemaker.DescribeImageVersionInput{
293+
input := sagemaker.DescribeImageVersionInput{
258294
ImageName: aws.String(name),
259295
}
260296

261-
output, err := conn.DescribeImageVersion(ctx, input)
297+
output, err := conn.DescribeImageVersion(ctx, &input)
262298

263299
if errs.IsAErrorMessageContains[*awstypes.ResourceNotFound](err, "does not exist") {
264300
return nil, &retry.NotFoundError{
@@ -277,3 +313,48 @@ func findImageVersionByName(ctx context.Context, conn *sagemaker.Client, name st
277313

278314
return output, nil
279315
}
316+
317+
// findImageVersionByTwoPartKey is used to fetch a specific version once a version number
318+
// is assigned
319+
func findImageVersionByTwoPartKey(ctx context.Context, conn *sagemaker.Client, name string, version int) (*sagemaker.DescribeImageVersionOutput, error) {
320+
input := sagemaker.DescribeImageVersionInput{
321+
ImageName: aws.String(name),
322+
Version: aws.Int32(int32(version)),
323+
}
324+
325+
output, err := conn.DescribeImageVersion(ctx, &input)
326+
327+
if errs.IsAErrorMessageContains[*awstypes.ResourceNotFound](err, "does not exist") {
328+
return nil, &retry.NotFoundError{
329+
LastError: err,
330+
LastRequest: input,
331+
}
332+
}
333+
334+
if err != nil {
335+
return nil, err
336+
}
337+
338+
if output == nil {
339+
return nil, tfresource.NewEmptyResultError(input)
340+
}
341+
342+
return output, nil
343+
}
344+
345+
// expandImageVersionResourceID wraps flex.ExpandResourceId and handles conversion
346+
// of the version portion to an integer
347+
func expandImageVersionResourceID(id string) (string, int, error) {
348+
parts, err := flex.ExpandResourceId(id, imageVersionResourcePartCount, false)
349+
if err != nil {
350+
return "", 0, err
351+
}
352+
353+
name := parts[0]
354+
version, err := strconv.Atoi(parts[1])
355+
if err != nil {
356+
return name, version, err
357+
}
358+
359+
return name, version, nil
360+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package sagemaker
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/YakDriver/regexache"
11+
awstypes "github.com/aws/aws-sdk-go-v2/service/sagemaker/types"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
14+
"github.com/hashicorp/terraform-provider-aws/internal/enum"
15+
"github.com/hashicorp/terraform-provider-aws/names"
16+
)
17+
18+
func resourceImageVersionV0() *schema.Resource {
19+
return &schema.Resource{
20+
Schema: map[string]*schema.Schema{
21+
names.AttrARN: {
22+
Type: schema.TypeString,
23+
Computed: true,
24+
},
25+
"base_image": {
26+
Type: schema.TypeString,
27+
Required: true,
28+
ForceNew: true,
29+
},
30+
"container_image": {
31+
Type: schema.TypeString,
32+
Computed: true,
33+
},
34+
"horovod": {
35+
Type: schema.TypeBool,
36+
Optional: true,
37+
},
38+
"image_arn": {
39+
Type: schema.TypeString,
40+
Computed: true,
41+
},
42+
"image_name": {
43+
Type: schema.TypeString,
44+
Required: true,
45+
ForceNew: true,
46+
},
47+
"job_type": {
48+
Type: schema.TypeString,
49+
Optional: true,
50+
ValidateDiagFunc: enum.Validate[awstypes.JobType](),
51+
},
52+
"ml_framework": {
53+
Type: schema.TypeString,
54+
Optional: true,
55+
ValidateFunc: validation.StringMatch(regexache.MustCompile(`^[a-zA-Z]+ ?\d+\.\d+(\.\d+)?$`), ""),
56+
},
57+
"processor": {
58+
Type: schema.TypeString,
59+
Optional: true,
60+
ValidateDiagFunc: enum.Validate[awstypes.Processor](),
61+
},
62+
"programming_lang": {
63+
Type: schema.TypeString,
64+
Optional: true,
65+
ValidateFunc: validation.StringMatch(regexache.MustCompile(`^[a-zA-Z]+ ?\d+\.\d+(\.\d+)?$`), ""),
66+
},
67+
"release_notes": {
68+
Type: schema.TypeString,
69+
Optional: true,
70+
ValidateFunc: validation.StringLenBetween(0, 255),
71+
},
72+
"vendor_guidance": {
73+
Type: schema.TypeString,
74+
Optional: true,
75+
ValidateDiagFunc: enum.Validate[awstypes.VendorGuidance](),
76+
},
77+
names.AttrVersion: {
78+
Type: schema.TypeInt,
79+
Computed: true,
80+
},
81+
},
82+
}
83+
}
84+
85+
func imageVersionStateUpgradeV0(_ context.Context, rawState map[string]any, meta any) (map[string]any, error) {
86+
if rawState == nil {
87+
rawState = map[string]any{}
88+
}
89+
90+
// The version attribute must be asserted into an int, otherwise fmt will assume
91+
// a float64 when a numerical any value is detected.
92+
version, ok := rawState[names.AttrVersion].(int)
93+
if !ok {
94+
version = 1
95+
}
96+
97+
rawState[names.AttrID] = fmt.Sprintf("%s,%d", rawState["image_name"], version)
98+
99+
return rawState, nil
100+
}

0 commit comments

Comments
 (0)