diff --git a/.changelog/20570.txt b/.changelog/20570.txt new file mode 100644 index 00000000000..a264a20d864 --- /dev/null +++ b/.changelog/20570.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_sagemaker_human_task_ui +``` \ No newline at end of file diff --git a/aws/internal/service/sagemaker/finder/finder.go b/aws/internal/service/sagemaker/finder/finder.go index f4e2d893064..a77dd187734 100644 --- a/aws/internal/service/sagemaker/finder/finder.go +++ b/aws/internal/service/sagemaker/finder/finder.go @@ -246,3 +246,31 @@ func WorkteamByName(conn *sagemaker.SageMaker, name string) (*sagemaker.Workteam return output.Workteam, nil } + +func HumanTaskUiByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeHumanTaskUiOutput, error) { + input := &sagemaker.DescribeHumanTaskUiInput{ + HumanTaskUiName: aws.String(name), + } + + output, err := conn.DescribeHumanTaskUi(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, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/provider.go b/aws/provider.go index 1beb5e9c100..39138bbdb13 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -1009,6 +1009,7 @@ func Provider() *schema.Provider { "aws_sagemaker_feature_group": resourceAwsSagemakerFeatureGroup(), "aws_sagemaker_image": resourceAwsSagemakerImage(), "aws_sagemaker_image_version": resourceAwsSagemakerImageVersion(), + "aws_sagemaker_human_task_ui": resourceAwsSagemakerHumanTaskUi(), "aws_sagemaker_model": resourceAwsSagemakerModel(), "aws_sagemaker_model_package_group": resourceAwsSagemakerModelPackageGroup(), "aws_sagemaker_notebook_instance_lifecycle_configuration": resourceAwsSagemakerNotebookInstanceLifeCycleConfiguration(), diff --git a/aws/resource_aws_sagemaker_human_task_ui.go b/aws/resource_aws_sagemaker_human_task_ui.go new file mode 100644 index 00000000000..5a388f57275 --- /dev/null +++ b/aws/resource_aws_sagemaker_human_task_ui.go @@ -0,0 +1,204 @@ +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 resourceAwsSagemakerHumanTaskUi() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSagemakerHumanTaskUiCreate, + Read: resourceAwsSagemakerHumanTaskUiRead, + Update: resourceAwsSagemakerHumanTaskUiUpdate, + Delete: resourceAwsSagemakerHumanTaskUiDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "ui_template": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 128000), + }, + "content_sha256": { + Type: schema.TypeString, + Computed: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "human_task_ui_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 63), + validation.StringMatch(regexp.MustCompile(`^[a-z0-9](-*[a-z0-9])*$`), "Valid characters are a-z, A-Z, 0-9, and - (hyphen)."), + ), + }, + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + CustomizeDiff: SetTagsDiff, + } +} + +func resourceAwsSagemakerHumanTaskUiCreate(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("human_task_ui_name").(string) + input := &sagemaker.CreateHumanTaskUiInput{ + HumanTaskUiName: aws.String(name), + UiTemplate: expandSagemakerHumanTaskUiUiTemplate(d.Get("ui_template").([]interface{})), + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().SagemakerTags() + } + + log.Printf("[DEBUG] Creating SageMaker HumanTaskUi: %s", input) + _, err := conn.CreateHumanTaskUi(input) + + if err != nil { + return fmt.Errorf("error creating SageMaker HumanTaskUi (%s): %w", name, err) + } + + d.SetId(name) + + return resourceAwsSagemakerHumanTaskUiRead(d, meta) +} + +func resourceAwsSagemakerHumanTaskUiRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sagemakerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + humanTaskUi, err := finder.HumanTaskUiByName(conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] SageMaker HumanTaskUi (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading SageMaker HumanTaskUi (%s): %w", d.Id(), err) + } + + arn := aws.StringValue(humanTaskUi.HumanTaskUiArn) + d.Set("arn", arn) + d.Set("human_task_ui_name", humanTaskUi.HumanTaskUiName) + + if err := d.Set("ui_template", flattenSagemakerHumanTaskUiUiTemplate(humanTaskUi.UiTemplate, d.Get("ui_template.0.content").(string))); err != nil { + return fmt.Errorf("error setting ui_template: %w", err) + } + + tags, err := keyvaluetags.SagemakerListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for SageMaker HumanTaskUi (%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 resourceAwsSagemakerHumanTaskUiUpdate(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 SageMaker HumanTaskUi (%s) tags: %w", d.Id(), err) + } + } + + return resourceAwsSagemakerHumanTaskUiRead(d, meta) +} + +func resourceAwsSagemakerHumanTaskUiDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).sagemakerconn + + log.Printf("[DEBUG] Deleting SageMaker HumanTaskUi: %s", d.Id()) + _, err := conn.DeleteHumanTaskUi(&sagemaker.DeleteHumanTaskUiInput{ + HumanTaskUiName: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, sagemaker.ErrCodeResourceNotFound) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting SageMaker HumanTaskUi (%s): %w", d.Id(), err) + } + + return nil +} + +func expandSagemakerHumanTaskUiUiTemplate(l []interface{}) *sagemaker.UiTemplate { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &sagemaker.UiTemplate{ + Content: aws.String(m["content"].(string)), + } + + return config +} + +func flattenSagemakerHumanTaskUiUiTemplate(config *sagemaker.UiTemplateInfo, content string) []map[string]interface{} { + if config == nil { + return []map[string]interface{}{} + } + + m := map[string]interface{}{ + "content_sha256": aws.StringValue(config.ContentSha256), + "url": aws.StringValue(config.Url), + "content": content, + } + + return []map[string]interface{}{m} +} diff --git a/aws/resource_aws_sagemaker_human_task_ui_test.go b/aws/resource_aws_sagemaker_human_task_ui_test.go new file mode 100644 index 00000000000..defa06bc569 --- /dev/null +++ b/aws/resource_aws_sagemaker_human_task_ui_test.go @@ -0,0 +1,254 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sagemaker" + "github.com/hashicorp/go-multierror" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func init() { + resource.AddTestSweepers("aws_sagemaker_human_task_ui", &resource.Sweeper{ + Name: "aws_sagemaker_human_task_ui", + F: testSweepSagemakerHumanTaskUis, + }) +} + +func testSweepSagemakerHumanTaskUis(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*AWSClient).sagemakerconn + var sweeperErrs *multierror.Error + + err = conn.ListHumanTaskUisPages(&sagemaker.ListHumanTaskUisInput{}, func(page *sagemaker.ListHumanTaskUisOutput, lastPage bool) bool { + for _, humanTaskUi := range page.HumanTaskUiSummaries { + + r := resourceAwsSagemakerHumanTaskUi() + d := r.Data(nil) + d.SetId(aws.StringValue(humanTaskUi.HumanTaskUiName)) + err := r.Delete(d, client) + if err != nil { + log.Printf("[ERROR] %s", err) + sweeperErrs = multierror.Append(sweeperErrs, err) + continue + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping SageMaker humanTaskUi sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving Sagemaker HumanTaskUis: %w", err)) + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAWSSagemakerHumanTaskUi_basic(t *testing.T) { + var humanTaskUi sagemaker.DescribeHumanTaskUiOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_human_task_ui.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sagemaker.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerHumanTaskUiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerHumanTaskUiCognitoBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerHumanTaskUiExists(resourceName, &humanTaskUi), + resource.TestCheckResourceAttr(resourceName, "human_task_ui_name", rName), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "sagemaker", fmt.Sprintf("human-task-ui/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "ui_template.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ui_template.0.content", "ui_template.0.url"}, + }, + }, + }) +} + +func TestAccAWSSagemakerHumanTaskUi_tags(t *testing.T) { + var humanTaskUi sagemaker.DescribeHumanTaskUiOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_human_task_ui.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sagemaker.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerHumanTaskUiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerHumanTaskUiTagsConfig1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerHumanTaskUiExists(resourceName, &humanTaskUi), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ui_template.0.content", "ui_template.0.url"}, + }, + { + Config: testAccAWSSagemakerHumanTaskUiTagsConfig2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerHumanTaskUiExists(resourceName, &humanTaskUi), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSSagemakerHumanTaskUiTagsConfig1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerHumanTaskUiExists(resourceName, &humanTaskUi), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccAWSSagemakerHumanTaskUi_disappears(t *testing.T) { + var humanTaskUi sagemaker.DescribeHumanTaskUiOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_sagemaker_human_task_ui.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sagemaker.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSagemakerHumanTaskUiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSagemakerHumanTaskUiCognitoBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSagemakerHumanTaskUiExists(resourceName, &humanTaskUi), + testAccCheckResourceDisappears(testAccProvider, resourceAwsSagemakerHumanTaskUi(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAWSSagemakerHumanTaskUiDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).sagemakerconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_sagemaker_human_task_ui" { + continue + } + + _, err := finder.HumanTaskUiByName(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("SageMaker HumanTaskUi %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAWSSagemakerHumanTaskUiExists(n string, humanTaskUi *sagemaker.DescribeHumanTaskUiOutput) 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 SageMaker HumanTaskUi ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).sagemakerconn + + output, err := finder.HumanTaskUiByName(conn, rs.Primary.ID) + + if err != nil { + return err + } + + *humanTaskUi = *output + + return nil + } +} + +func testAccAWSSagemakerHumanTaskUiCognitoBasicConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_sagemaker_human_task_ui" "test" { + human_task_ui_name = %[1]q + + ui_template { + content = file("test-fixtures/sagemaker-human-task-ui-tmpl.html") + } +} +`, rName) +} + +func testAccAWSSagemakerHumanTaskUiTagsConfig1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_sagemaker_human_task_ui" "test" { + human_task_ui_name = %[1]q + + ui_template { + content = file("test-fixtures/sagemaker-human-task-ui-tmpl.html") + } + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSSagemakerHumanTaskUiTagsConfig2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_sagemaker_human_task_ui" "test" { + human_task_ui_name = %[1]q + + ui_template { + content = file("test-fixtures/sagemaker-human-task-ui-tmpl.html") + } + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/aws/test-fixtures/sagemaker-human-task-ui-tmpl.html b/aws/test-fixtures/sagemaker-human-task-ui-tmpl.html new file mode 100644 index 00000000000..f8b547b8a8f --- /dev/null +++ b/aws/test-fixtures/sagemaker-human-task-ui-tmpl.html @@ -0,0 +1,54 @@ + + + + + +

Collect utterances for intent

+
+ + +

Collect utterances for intent

+

Given a context and an intent, write how you would express the intent using natural language. Don't try to be overly formal, simply write what you would say if you were in the given situation.

+
+ + +

Context

+

You bought a pair of shoes online but they don't fit

+ +

Intent

+

You want to try to return the shoes via an online customer service chat bot

+ +

Response

+

I would like to return a pair of shoes

+
+ + +

Context

+

You bought a pair of shoes online but they don't fit

+ +

Intent

+

You want to try to return the shoes via an online customer service chat bot

+ +

Response

+

Hi, I'm trying to buy a plane ticket for tomorrow morning and your website isn't working

+
+ + +

Context

+

You bought a pair of shoes online but they don't fit

+ +

Intent

+

You want to try to return the shoes via an online customer service chat bot

+ +

Response

+

Don't fit

+
+
+ + +

Write what you would say in the given situation:

+

Context: {{ task.input.context }}

+

Intent: {{ task.input.intent }}

+ + +
\ No newline at end of file diff --git a/website/docs/r/sagemaker_human_task_ui.html.markdown b/website/docs/r/sagemaker_human_task_ui.html.markdown new file mode 100644 index 00000000000..0eab8d77ad3 --- /dev/null +++ b/website/docs/r/sagemaker_human_task_ui.html.markdown @@ -0,0 +1,58 @@ +--- +subcategory: "Sagemaker" +layout: "aws" +page_title: "AWS: aws_sagemaker_human_task_ui" +description: |- + Provides a Sagemaker Human Task UI resource. +--- + +# Resource: aws_sagemaker_human_task_ui + +Provides a Sagemaker Human Task UI resource. + +## Example Usage + +```terraform +resource "aws_sagemaker_human_task_ui" "example" { + human_task_ui_name = "example" + + ui_template { + content = file("sagemaker-human-task-ui-template.html") + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `human_task_ui_name` - (Required) The name of the Human Task UI. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `ui_template` - (Required) The Liquid template for the worker user interface. See [UI Template](#ui-template) below. + +### UI Template + +* `content` - (Required) The content of the Liquid template for the worker user interface. + + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The Amazon Resource Name (ARN) assigned by AWS to this Human Task UI. +* `id` - The name of the Human Task UI. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). +* `ui_template` - (Required) The Liquid template for the worker user interface. See [UI Template](#ui-template) below. + +### UI Template + +* `content_sha256` - The SHA-256 digest of the contents of the template. +* `url` - The URL for the user interface template. + +## Import + +Sagemaker Human Task UIs can be imported using the `human_task_ui_name`, e.g. + +``` +$ terraform import aws_sagemaker_human_task_ui.example example +```