diff --git a/.changelog/28785.txt b/.changelog/28785.txt new file mode 100644 index 000000000000..cdb785e76a2b --- /dev/null +++ b/.changelog/28785.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sagemaker_endpoint_configuration: Add `name_prefix` argument +``` \ No newline at end of file diff --git a/internal/service/sagemaker/endpoint_configuration.go b/internal/service/sagemaker/endpoint_configuration.go index d408a52c1026..bd2a4571b8ad 100644 --- a/internal/service/sagemaker/endpoint_configuration.go +++ b/internal/service/sagemaker/endpoint_configuration.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/flex" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" @@ -207,11 +208,20 @@ func ResourceEndpointConfiguration() *schema.Resource { ValidateFunc: verify.ValidARN, }, "name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: validName, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, + ValidateFunc: validName, + }, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name"}, + ValidateFunc: validPrefix, }, "production_variants": { Type: schema.TypeList, @@ -460,12 +470,7 @@ func resourceEndpointConfigurationCreate(ctx context.Context, d *schema.Resource defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) - var name string - if v, ok := d.GetOk("name"); ok { - name = v.(string) - } else { - name = resource.UniqueId() - } + name := create.Name(d.Get("name").(string), d.Get("name_prefix").(string)) createOpts := &sagemaker.CreateEndpointConfigInput{ EndpointConfigName: aws.String(name), @@ -522,6 +527,7 @@ func resourceEndpointConfigurationRead(ctx context.Context, d *schema.ResourceDa d.Set("arn", endpointConfig.EndpointConfigArn) d.Set("name", endpointConfig.EndpointConfigName) + d.Set("name_prefix", create.NamePrefixFromName(aws.StringValue(endpointConfig.EndpointConfigName))) d.Set("kms_key_arn", endpointConfig.KmsKeyId) if err := d.Set("production_variants", flattenProductionVariants(endpointConfig.ProductionVariants)); err != nil { diff --git a/internal/service/sagemaker/endpoint_configuration_test.go b/internal/service/sagemaker/endpoint_configuration_test.go index a72793c84b4a..139f245a0fd4 100644 --- a/internal/service/sagemaker/endpoint_configuration_test.go +++ b/internal/service/sagemaker/endpoint_configuration_test.go @@ -54,6 +54,62 @@ func TestAccSageMakerEndpointConfiguration_basic(t *testing.T) { }) } +func TestAccSageMakerEndpointConfiguration_nameGenerated(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_sagemaker_endpoint_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, sagemaker.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEndpointConfigurationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEndpointConfigurationConfig_nameGenerated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEndpointConfigurationExists(ctx, resourceName), + acctest.CheckResourceAttrNameGenerated(resourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "name_prefix", "terraform-"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSageMakerEndpointConfiguration_namePrefix(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_sagemaker_endpoint_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, sagemaker.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEndpointConfigurationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEndpointConfigurationConfig_namePrefix(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEndpointConfigurationExists(ctx, resourceName), + acctest.CheckResourceAttrNameFromPrefix(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "name_prefix", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccSageMakerEndpointConfiguration_shadowProductionVariants(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -607,6 +663,36 @@ resource "aws_sagemaker_endpoint_configuration" "test" { `, rName)) } +func testAccEndpointConfigurationConfig_nameGenerated(rName string) string { + return acctest.ConfigCompose(testAccEndpointConfigurationConfig_base(rName), ` +resource "aws_sagemaker_endpoint_configuration" "test" { + production_variants { + variant_name = "variant-1" + model_name = aws_sagemaker_model.test.name + initial_instance_count = 2 + instance_type = "ml.t2.medium" + initial_variant_weight = 1 + } +} +`) +} + +func testAccEndpointConfigurationConfig_namePrefix(rName string) string { + return acctest.ConfigCompose(testAccEndpointConfigurationConfig_base(rName), fmt.Sprintf(` +resource "aws_sagemaker_endpoint_configuration" "test" { + name_prefix = %[1]q + + production_variants { + variant_name = "variant-1" + model_name = aws_sagemaker_model.test.name + initial_instance_count = 2 + instance_type = "ml.t2.medium" + initial_variant_weight = 1 + } +} +`, rName)) +} + func testAccEndpointConfigurationConfig_shadowProductionVariants(rName string) string { return acctest.ConfigCompose(testAccEndpointConfigurationConfig_base(rName), fmt.Sprintf(` resource "aws_sagemaker_endpoint_configuration" "test" { diff --git a/internal/service/sagemaker/validate.go b/internal/service/sagemaker/validate.go index bfe6c3079298..86ea5b040375 100644 --- a/internal/service/sagemaker/validate.go +++ b/internal/service/sagemaker/validate.go @@ -3,6 +3,8 @@ package sagemaker import ( "fmt" "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func validEnvironment(v interface{}, k string) (ws []string, errors []error) { @@ -78,3 +80,22 @@ func validName(v interface{}, k string) (ws []string, errors []error) { } return } + +func validPrefix(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only alphanumeric characters and hyphens allowed in %q: %q", + k, value)) + } + maxLength := 63 - resource.UniqueIDSuffixLength + if len(value) > maxLength { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than %d characters: %q", k, maxLength, value)) + } + if regexp.MustCompile(`^-`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q cannot begin with a hyphen: %q", k, value)) + } + return +} diff --git a/internal/service/sagemaker/validate_test.go b/internal/service/sagemaker/validate_test.go index 09fa401398af..84270e8289e6 100644 --- a/internal/service/sagemaker/validate_test.go +++ b/internal/service/sagemaker/validate_test.go @@ -35,3 +35,35 @@ func TestValidName(t *testing.T) { } } } + +func TestValidPrefix(t *testing.T) { + t.Parallel() + + maxLength := 37 + validPrefixes := []string{ + "ValidSageMakerName", + "Valid-5a63Mak3r-Name", + "123-456-789", + "1234", + strings.Repeat("W", maxLength), + } + for _, v := range validPrefixes { + _, errors := validPrefix(v, "name_prefix") + if len(errors) != 0 { + t.Fatalf("%q should be a valid SageMaker prefix with maximum length %d chars: %q", v, maxLength, errors) + } + } + + invalidPrefixes := []string{ + "Invalid prefix", // blanks are not allowed + "1#{}nook", // other non-alphanumeric chars + "-nook", // cannot start with hyphen + strings.Repeat("W", maxLength+1), // length > maxLength + } + for _, v := range invalidPrefixes { + _, errors := validPrefix(v, "name_prefix") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid SageMaker prefix", v) + } + } +} diff --git a/website/docs/r/sagemaker_endpoint_configuration.html.markdown b/website/docs/r/sagemaker_endpoint_configuration.html.markdown index 941e009fc675..864303453ac8 100644 --- a/website/docs/r/sagemaker_endpoint_configuration.html.markdown +++ b/website/docs/r/sagemaker_endpoint_configuration.html.markdown @@ -37,7 +37,8 @@ The following arguments are supported: * `production_variants` - (Required) An list of ProductionVariant objects, one for each model that you want to host at this endpoint. Fields are documented below. * `kms_key_arn` - (Optional) Amazon Resource Name (ARN) of a AWS Key Management Service key that Amazon SageMaker uses to encrypt data on the storage volume attached to the ML compute instance that hosts the endpoint. -* `name` - (Optional) The name of the endpoint configuration. If omitted, Terraform will assign a random, unique name. +* `name` - (Optional) The name of the endpoint configuration. If omitted, Terraform will assign a random, unique name. Conflicts with `name_prefix`. +* `name_prefix` - (Optional) Creates a unique endpoint configuration name beginning with the specified prefix. Conflicts with `name`. * `tags` - (Optional) A mapping of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. * `data_capture_config` - (Optional) Specifies the parameters to capture input/output of SageMaker models endpoints. Fields are documented below. * `async_inference_config` - (Optional) Specifies configuration for how an endpoint performs asynchronous inference.