From 8831ae408975590262684b9ab2f37112cd428e5b Mon Sep 17 00:00:00 2001 From: Jack Bruno Date: Sun, 9 Jun 2019 20:45:33 -0600 Subject: [PATCH 01/18] Add Lex intent data source and resoruce --- aws/data_source_aws_lex_intent.go | 88 +++++ aws/data_source_aws_lex_intent_test.go | 49 +++ aws/provider.go | 2 + aws/resource_aws_lex.go | 434 ++++++++++++++++++++++++ aws/resource_aws_lex_intent.go | 326 ++++++++++++++++++ aws/resource_aws_lex_intent_test.go | 379 +++++++++++++++++++++ aws/validators_lex_constants.go | 70 ++++ website/aws.erb | 12 + website/docs/d/lex_intent.html.markdown | 45 +++ website/docs/r/lex_intent.html.markdown | 369 ++++++++++++++++++++ 10 files changed, 1774 insertions(+) create mode 100644 aws/data_source_aws_lex_intent.go create mode 100644 aws/data_source_aws_lex_intent_test.go create mode 100644 aws/resource_aws_lex.go create mode 100644 aws/resource_aws_lex_intent.go create mode 100644 aws/resource_aws_lex_intent_test.go create mode 100644 aws/validators_lex_constants.go create mode 100644 website/docs/d/lex_intent.html.markdown create mode 100644 website/docs/r/lex_intent.html.markdown diff --git a/aws/data_source_aws_lex_intent.go b/aws/data_source_aws_lex_intent.go new file mode 100644 index 00000000000..e0803e60c93 --- /dev/null +++ b/aws/data_source_aws_lex_intent.go @@ -0,0 +1,88 @@ +package aws + +import ( + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lexmodelbuildingservice" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func dataSourceAwsLexIntent() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsLexIntentRead, + + Schema: map[string]*schema.Schema{ + "checksum": { + Type: schema.TypeString, + Computed: true, + }, + "created_date": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "last_updated_date": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(lexNameMinLength, lexNameMaxLength), + validation.StringMatch(regexp.MustCompile(lexNameRegex), ""), + ), + }, + "parent_intent_signature": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Default: lexVersionDefault, + ValidateFunc: validation.All( + validation.StringLenBetween(lexVersionMinLength, lexVersionMaxLength), + validation.StringMatch(regexp.MustCompile(lexVersionRegex), ""), + ), + }, + }, + } +} + +func dataSourceAwsLexIntentRead(d *schema.ResourceData, meta interface{}) error { + intentName := d.Get("name").(string) + intentVersion := lexVersionLatest + if v, ok := d.GetOk("version"); ok { + intentVersion = v.(string) + } + + conn := meta.(*AWSClient).lexmodelconn + + resp, err := conn.GetIntent(&lexmodelbuildingservice.GetIntentInput{ + Name: aws.String(intentName), + Version: aws.String(intentVersion), + }) + if err != nil { + return fmt.Errorf("error getting intent %s: %s", intentName, err) + } + + d.Set("checksum", resp.Checksum) + d.Set("created_date", resp.CreatedDate.UTC().String()) + d.Set("description", resp.Description) + d.Set("last_updated_date", resp.LastUpdatedDate.UTC().String()) + d.Set("name", resp.Name) + d.Set("parent_intent_signature", resp.ParentIntentSignature) + d.Set("version", resp.Version) + + d.SetId(intentName) + + return nil +} diff --git a/aws/data_source_aws_lex_intent_test.go b/aws/data_source_aws_lex_intent_test.go new file mode 100644 index 00000000000..6204574f3da --- /dev/null +++ b/aws/data_source_aws_lex_intent_test.go @@ -0,0 +1,49 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDataSourceAwsLexIntent(t *testing.T) { + resourceName := "aws_lex_intent.test" + dataSourceName := "data." + resourceName + testID := acctest.RandStringFromCharSet(8, acctest.CharSetAlpha) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testDataSourceAwsLexIntentConfig, testID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "checksum", resourceName, "checksum"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "version", resourceName, "version"), + resource.TestCheckResourceAttrSet(dataSourceName, "created_date"), + resource.TestCheckResourceAttrSet(dataSourceName, "last_updated_date"), + ), + }, + }, + }) +} + +const testDataSourceAwsLexIntentConfig = ` +resource "aws_lex_intent" "test" { + description = "Intent to order a bouquet of flowers for pick up" + + fulfillment_activity { + type = "ReturnIntent" + } + + name = "test_intent_%s" +} + +data "aws_lex_intent" "test" { + name = "${aws_lex_intent.test.name}" +} +` diff --git a/aws/provider.go b/aws/provider.go index 9ac25ef9136..df8b412e0c9 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -226,6 +226,7 @@ func Provider() terraform.ResourceProvider { "aws_lambda_layer_version": dataSourceAwsLambdaLayerVersion(), "aws_launch_configuration": dataSourceAwsLaunchConfiguration(), "aws_launch_template": dataSourceAwsLaunchTemplate(), + "aws_lex_intent": dataSourceAwsLexIntent(), "aws_mq_broker": dataSourceAwsMqBroker(), "aws_msk_cluster": dataSourceAwsMskCluster(), "aws_nat_gateway": dataSourceAwsNatGateway(), @@ -545,6 +546,7 @@ func Provider() terraform.ResourceProvider { "aws_lambda_layer_version": resourceAwsLambdaLayerVersion(), "aws_launch_configuration": resourceAwsLaunchConfiguration(), "aws_launch_template": resourceAwsLaunchTemplate(), + "aws_lex_intent": resourceAwsLexIntent(), "aws_licensemanager_association": resourceAwsLicenseManagerAssociation(), "aws_licensemanager_license_configuration": resourceAwsLicenseManagerLicenseConfiguration(), "aws_lightsail_domain": resourceAwsLightsailDomain(), diff --git a/aws/resource_aws_lex.go b/aws/resource_aws_lex.go new file mode 100644 index 00000000000..79e0024c247 --- /dev/null +++ b/aws/resource_aws_lex.go @@ -0,0 +1,434 @@ +package aws + +import ( + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lexmodelbuildingservice" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +// Many of the Lex resources require complex nested objects. Terraform maps only support simple key +// value pairs and not complex or mixed types. That is why these resources are defined using the +// schema.TypeList and a max of 1 item instead of the schema.TypeMap. + +// Convert a slice of items to a map[string]interface{} +// Expects input as a single item slice. +// Required because we use TypeList instead of TypeMap due to TypeMap not supporting nested and mixed complex values. +func expandLexObject(v interface{}) map[string]interface{} { + return v.([]interface{})[0].(map[string]interface{}) +} + +// Covert a map[string]interface{} to a slice of items +// Expects a single map[string]interface{} +// Required because we use TypeList instead of TypeMap due to TypeMap not supporting nested and mixed complex values. +func flattenLexObject(m map[string]interface{}) []map[string]interface{} { + return []map[string]interface{}{m} +} + +func expandLexSet(s *schema.Set) (items []map[string]interface{}) { + for _, rawItem := range s.List() { + item, ok := rawItem.(map[string]interface{}) + if !ok { + continue + } + + items = append(items, item) + } + + return +} + +var lexMessageResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(lexMessageContentMinLength, lexMessageContentMaxLength), + }, + "content_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + lexmodelbuildingservice.ContentTypeCustomPayload, + lexmodelbuildingservice.ContentTypePlainText, + lexmodelbuildingservice.ContentTypeSsml, + }, false), + }, + "group_number": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(lexMessageGroupNumberMin, lexMessageGroupNumberMax), + }, + }, +} + +func flattenLexMessages(messages []*lexmodelbuildingservice.Message) (flattenedMessages []map[string]interface{}) { + for _, message := range messages { + flattenedMessages = append(flattenedMessages, map[string]interface{}{ + "content": aws.StringValue(message.Content), + "content_type": aws.StringValue(message.ContentType), + "group_number": aws.Int64Value(message.GroupNumber), + }) + } + + return +} + +// Expects a slice of maps representing the Lex objects. +// The value passed into this function should have been run through the expandLexSet function. +// Example: []map[content: test content_type: PlainText group_number: 1] +func expandLexMessages(rawValues []map[string]interface{}) (messages []*lexmodelbuildingservice.Message) { + for _, rawValue := range rawValues { + message := &lexmodelbuildingservice.Message{ + Content: aws.String(rawValue["content"].(string)), + ContentType: aws.String(rawValue["content_type"].(string)), + } + + if v, ok := rawValue["group_number"]; ok && v != 0 { + message.GroupNumber = aws.Int64(int64(v.(int))) + } + + messages = append(messages, message) + } + + return +} + +var lexStatementResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "message": { + Type: schema.TypeSet, + Required: true, + MinItems: lexStatementMessagesMin, + MaxItems: lexStatementMessagesMax, + Elem: lexMessageResource, + }, + "response_card": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(lexResponseCardMinLength, lexResponseCardMaxLength), + }, + }, +} + +func flattenLexStatement(statement *lexmodelbuildingservice.Statement) (flattened map[string]interface{}) { + flattened = map[string]interface{}{} + flattened["message"] = flattenLexMessages(statement.Messages) + + if statement.ResponseCard != nil { + flattened["response_card"] = aws.StringValue(statement.ResponseCard) + } + + return +} + +func expandLexStatement(m map[string]interface{}) (statement *lexmodelbuildingservice.Statement) { + statement = &lexmodelbuildingservice.Statement{} + statement.Messages = expandLexMessages(expandLexSet(m["message"].(*schema.Set))) + + if v, ok := m["response_card"]; ok && v != "" { + statement.ResponseCard = aws.String(v.(string)) + } + + return +} + +var lexPromptResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_attempts": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(lexPromptMaxAttemptsMin, lexPromptMaxAttemptsMax), + }, + "message": { + Type: schema.TypeSet, + Required: true, + MinItems: lexStatementMessagesMin, + MaxItems: lexStatementMessagesMax, + Elem: lexMessageResource, + }, + "response_card": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(lexResponseCardMinLength, lexResponseCardMaxLength), + }, + }, +} + +func flattenLexPrompt(prompt *lexmodelbuildingservice.Prompt) (flattened map[string]interface{}) { + flattened = map[string]interface{}{} + flattened["max_attempts"] = aws.Int64Value(prompt.MaxAttempts) + flattened["message"] = flattenLexMessages(prompt.Messages) + + if prompt.ResponseCard != nil { + flattened["response_card"] = aws.StringValue(prompt.ResponseCard) + } + + return +} + +func expandLexPrompt(m map[string]interface{}) (prompt *lexmodelbuildingservice.Prompt) { + prompt = &lexmodelbuildingservice.Prompt{} + prompt.MaxAttempts = aws.Int64(int64(m["max_attempts"].(int))) + prompt.Messages = expandLexMessages(expandLexSet(m["message"].(*schema.Set))) + + if v, ok := m["response_card"]; ok && v != "" { + prompt.ResponseCard = aws.String(v.(string)) + } + + return +} + +var lexCodeHookResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "message_version": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(lexCodeHookMessageVersionMinLength, lexCodeHookMessageVersionMaxLength), + }, + "uri": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + }, +} + +func flattenLexCodeHook(hook *lexmodelbuildingservice.CodeHook) (flattened map[string]interface{}) { + return map[string]interface{}{ + "message_version": aws.StringValue(hook.MessageVersion), + "uri": aws.StringValue(hook.Uri), + } +} + +func expandLexCodeHook(m map[string]interface{}) (hook *lexmodelbuildingservice.CodeHook) { + return &lexmodelbuildingservice.CodeHook{ + MessageVersion: aws.String(m["message_version"].(string)), + Uri: aws.String(m["uri"].(string)), + } +} + +var lexFollowUpPromptResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "prompt": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: lexPromptResource, + }, + "rejection_statement": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: lexStatementResource, + }, + }, +} + +func flattenLexFollowUpPrompt(followUp *lexmodelbuildingservice.FollowUpPrompt) (flattened map[string]interface{}) { + return map[string]interface{}{ + "prompt": flattenLexObject(flattenLexPrompt(followUp.Prompt)), + "rejection_statement": flattenLexObject(flattenLexStatement(followUp.RejectionStatement)), + } +} + +func expandLexFollowUpPrompt(m map[string]interface{}) (followUp *lexmodelbuildingservice.FollowUpPrompt) { + return &lexmodelbuildingservice.FollowUpPrompt{ + Prompt: expandLexPrompt(expandLexObject(m["prompt"])), + RejectionStatement: expandLexStatement(expandLexObject(m["rejection_statement"])), + } +} + +var lexFulfilmentActivityResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "code_hook": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: lexCodeHookResource, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + lexmodelbuildingservice.FulfillmentActivityTypeCodeHook, + lexmodelbuildingservice.FulfillmentActivityTypeReturnIntent, + }, false), + }, + }, +} + +func flattenLexFulfilmentActivity(activity *lexmodelbuildingservice.FulfillmentActivity) (flattened map[string]interface{}) { + flattened = map[string]interface{}{} + flattened["type"] = aws.StringValue(activity.Type) + + if activity.CodeHook != nil { + flattened["code_hook"] = flattenLexObject(flattenLexCodeHook(activity.CodeHook)) + } + + return +} + +func expandLexFulfilmentActivity(m map[string]interface{}) (activity *lexmodelbuildingservice.FulfillmentActivity) { + activity = &lexmodelbuildingservice.FulfillmentActivity{} + activity.Type = aws.String(m["type"].(string)) + + if v, ok := m["code_hook"]; ok && len(v.([]interface{})) != 0 { + activity.CodeHook = expandLexCodeHook(expandLexObject(v)) + } + + return +} + +var lexSlotResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "description": { + Type: schema.TypeString, + Optional: true, + Default: lexDescriptionDefault, + ValidateFunc: validation.StringLenBetween(lexDescriptionMinLength, lexDescriptionMaxLength), + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(lexNameMinLength, lexNameMaxLength), + validation.StringMatch(regexp.MustCompile(lexNameRegex), ""), + ), + }, + "priority": { + Type: schema.TypeInt, + Optional: true, + Default: lexSlotPriorityDefault, + ValidateFunc: validation.IntBetween(lexSlotPriorityMin, lexSlotPriorityMax), + }, + "response_card": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(lexResponseCardMinLength, lexResponseCardMaxLength), + }, + "sample_utterances": { + Type: schema.TypeList, + Optional: true, + MinItems: lexSlotSampleUtterancesMin, + MaxItems: lexSlotSampleUtterancesMax, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(lexUtteranceMinLength, lexUtteranceMaxLength), + }, + }, + "slot_constraint": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + lexmodelbuildingservice.SlotConstraintOptional, + lexmodelbuildingservice.SlotConstraintRequired, + }, false), + }, + "slot_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(lexSlotTypeMinLength, lexSlotTypeMaxLength), + validation.StringMatch(regexp.MustCompile(lexSlotTypeRegex), ""), + ), + }, + "slot_type_version": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.All( + validation.StringLenBetween(lexVersionMinLength, lexVersionMaxLength), + validation.StringMatch(regexp.MustCompile(lexVersionRegex), ""), + ), + }, + "value_elicitation_prompt": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: lexPromptResource, + }, + }, +} + +func flattenLexSlots(slots []*lexmodelbuildingservice.Slot) (flattenedSlots []map[string]interface{}) { + for _, slot := range slots { + flattenedSlot := map[string]interface{}{ + "name": aws.StringValue(slot.Name), + "priority": aws.Int64Value(slot.Priority), + "slot_constraint": aws.StringValue(slot.SlotConstraint), + "slot_type": aws.StringValue(slot.SlotType), + } + + if slot.Description != nil { + flattenedSlot["description"] = aws.StringValue(slot.Description) + } + + if slot.ResponseCard != nil { + flattenedSlot["response_card"] = aws.StringValue(slot.ResponseCard) + } + + if slot.SampleUtterances != nil { + flattenedSlot["sample_utterances"] = flattenStringList(slot.SampleUtterances) + } + + if slot.SlotTypeVersion != nil { + flattenedSlot["slot_type_version"] = aws.StringValue(slot.SlotTypeVersion) + } + + if slot.ValueElicitationPrompt != nil { + flattenedSlot["value_elicitation_prompt"] = flattenLexPrompt(slot.ValueElicitationPrompt) + } + + flattenedSlots = append(flattenedSlots, flattenedSlot) + } + + return +} + +// Expects a slice of maps representing the Lex objects. +// The value passed into this function should have been run through the expandLexSet function. +// Example: []map[name: test priority: 0 ...] +func expandLexSlots(rawValues []map[string]interface{}) (slots []*lexmodelbuildingservice.Slot) { + for _, rawValue := range rawValues { + slot := &lexmodelbuildingservice.Slot{ + Name: aws.String(rawValue["name"].(string)), + Priority: aws.Int64(int64(rawValue["priority"].(int))), + SlotConstraint: aws.String(rawValue["slot_constraint"].(string)), + SlotType: aws.String(rawValue["slot_type"].(string)), + } + + if v, ok := rawValue["description"]; ok && v != "" { + slot.Description = aws.String(v.(string)) + } + + if v, ok := rawValue["response_card"]; ok && v != "" { + slot.ResponseCard = aws.String(v.(string)) + } + + if v, ok := rawValue["response_card"]; ok && v != "" { + slot.ResponseCard = aws.String(v.(string)) + } + + if v, ok := rawValue["sample_utterances"]; ok && len(v.([]interface{})) != 0 { + slot.SampleUtterances = expandStringList(v.([]interface{})) + } + + if v, ok := rawValue["slot_type_version"]; ok && v != "" { + slot.SlotTypeVersion = aws.String(v.(string)) + } + + if v, ok := rawValue["value_elicitation_prompt"]; ok && len(v.([]interface{})) != 0 { + slot.ValueElicitationPrompt = expandLexPrompt(expandLexObject(v)) + } + + slots = append(slots, slot) + } + + return +} diff --git a/aws/resource_aws_lex_intent.go b/aws/resource_aws_lex_intent.go new file mode 100644 index 00000000000..d5ccf8e2be0 --- /dev/null +++ b/aws/resource_aws_lex_intent.go @@ -0,0 +1,326 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lexmodelbuildingservice" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsLexIntent() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsLexIntentCreate, + Read: resourceAwsLexIntentRead, + Update: resourceAwsLexIntentUpdate, + Delete: resourceAwsLexIntentDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "checksum": { + Type: schema.TypeString, + Computed: true, + }, + "conclusion_statement": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: lexStatementResource, + }, + "confirmation_prompt": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: lexPromptResource, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: lexDescriptionDefault, + ValidateFunc: validation.StringLenBetween(lexDescriptionMinLength, lexDescriptionMaxLength), + }, + "dialog_code_hook": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: lexCodeHookResource, + }, + "follow_up_prompt": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: lexFollowUpPromptResource, + }, + // Must be required because required by updates even though optional for creates + "fulfillment_activity": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: lexFulfilmentActivityResource, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(lexNameMinLength, lexNameMaxLength), + validation.StringMatch(regexp.MustCompile(lexNameRegex), ""), + ), + }, + "parent_intent_signature": { + Type: schema.TypeString, + Optional: true, + }, + "rejection_statement": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: lexStatementResource, + }, + "sample_utterances": { + Type: schema.TypeList, + Optional: true, + MinItems: lexUtterancesMin, + MaxItems: lexUtterancesMax, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(lexUtteranceMinLength, lexUtteranceMaxLength), + }, + }, + "slot": { + Type: schema.TypeSet, + Optional: true, + MinItems: lexSlotsMin, + MaxItems: lexSlotsMax, + Elem: lexSlotResource, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Default: lexVersionDefault, + ValidateFunc: validation.All( + validation.StringLenBetween(lexVersionMinLength, lexVersionMaxLength), + validation.StringMatch(regexp.MustCompile(lexVersionRegex), ""), + ), + }, + }, + } +} + +func resourceAwsLexIntentCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lexmodelconn + name := d.Get("name").(string) + + input := &lexmodelbuildingservice.PutIntentInput{ + Name: aws.String(name), + } + + // optional attributes + + if v, ok := d.GetOk("conclusion_statement"); ok { + input.ConclusionStatement = expandLexStatement(expandLexObject(v)) + } + + if v, ok := d.GetOk("confirmation_prompt"); ok { + input.ConfirmationPrompt = expandLexPrompt(expandLexObject(v)) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("dialog_code_hook"); ok { + input.DialogCodeHook = expandLexCodeHook(expandLexObject(v)) + } + + if v, ok := d.GetOk("follow_up_prompt"); ok { + input.FollowUpPrompt = expandLexFollowUpPrompt(expandLexObject(v)) + } + + if v, ok := d.GetOk("fulfillment_activity"); ok { + input.FulfillmentActivity = expandLexFulfilmentActivity(expandLexObject(v)) + } + + if v, ok := d.GetOk("parent_intent_signature"); ok { + input.ParentIntentSignature = aws.String(v.(string)) + } + + if v, ok := d.GetOk("rejection_statement"); ok { + input.RejectionStatement = expandLexStatement(expandLexObject(v)) + } + + if v, ok := d.GetOk("sample_utterances"); ok { + input.SampleUtterances = expandStringList(v.([]interface{})) + } + + if v, ok := d.GetOk("slot"); ok { + input.Slots = expandLexSlots(expandLexSet(v.(*schema.Set))) + } + + if _, err := conn.PutIntent(input); err != nil { + return fmt.Errorf("error creating Lex Intent %s: %s", name, err) + } + + d.SetId(name) + + return resourceAwsLexIntentRead(d, meta) +} + +func resourceAwsLexIntentRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lexmodelconn + + version := lexVersionLatest + if v, ok := d.GetOk("version"); ok { + version = v.(string) + } + + resp, err := conn.GetIntent(&lexmodelbuildingservice.GetIntentInput{ + Name: aws.String(d.Id()), + Version: aws.String(version), + }) + if err != nil { + if isAWSErr(err, lexmodelbuildingservice.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Intent (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + return fmt.Errorf("error getting intent %s: %s", d.Id(), err) + } + + d.Set("checksum", resp.Checksum) + d.Set("name", resp.Name) + d.Set("version", resp.Version) + + // optional attributes + + if resp.ConclusionStatement != nil { + d.Set("conclusion_statement", flattenLexObject(flattenLexStatement(resp.ConclusionStatement))) + } + + if resp.ConfirmationPrompt != nil { + d.Set("confirmation_prompt", flattenLexObject(flattenLexPrompt(resp.ConfirmationPrompt))) + } + + if resp.Description != nil { + d.Set("description", resp.Description) + } + + if resp.DialogCodeHook != nil { + d.Set("dialog_code_hook", flattenLexObject(flattenLexCodeHook(resp.DialogCodeHook))) + } + + if resp.FollowUpPrompt != nil { + d.Set("follow_up_prompt", flattenLexObject(flattenLexFollowUpPrompt(resp.FollowUpPrompt))) + } + + if resp.FulfillmentActivity != nil { + d.Set("fulfillment_activity", flattenLexObject(flattenLexFulfilmentActivity(resp.FulfillmentActivity))) + } + + if resp.ParentIntentSignature != nil { + d.Set("parent_intent_signature", resp.ParentIntentSignature) + } + + if resp.RejectionStatement != nil { + d.Set("rejection_statement", flattenLexObject(flattenLexStatement(resp.RejectionStatement))) + } + + if resp.SampleUtterances != nil { + d.Set("sample_utterances", resp.SampleUtterances) + } + + if resp.Slots != nil { + d.Set("slot", flattenLexSlots(resp.Slots)) + } + + return nil +} + +func resourceAwsLexIntentUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lexmodelconn + + input := &lexmodelbuildingservice.PutIntentInput{ + Checksum: aws.String(d.Get("checksum").(string)), + Name: aws.String(d.Id()), + } + + // optional attributes + + if v, ok := d.GetOk("conclusion_statement"); ok { + input.ConclusionStatement = expandLexStatement(expandLexObject(v)) + } + + if v, ok := d.GetOk("confirmation_prompt"); ok { + input.ConfirmationPrompt = expandLexPrompt(expandLexObject(v)) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("dialog_code_hook"); ok { + input.DialogCodeHook = expandLexCodeHook(expandLexObject(v)) + } + + if v, ok := d.GetOk("follow_up_prompt"); ok { + input.FollowUpPrompt = expandLexFollowUpPrompt(expandLexObject(v)) + } + + if v, ok := d.GetOk("fulfillment_activity"); ok { + input.FulfillmentActivity = expandLexFulfilmentActivity(expandLexObject(v)) + } + + if v, ok := d.GetOk("parent_intent_signature"); ok { + input.ParentIntentSignature = aws.String(v.(string)) + } + + if v, ok := d.GetOk("rejection_statement"); ok { + input.RejectionStatement = expandLexStatement(expandLexObject(v)) + } + + if v, ok := d.GetOk("sample_utterances"); ok { + input.SampleUtterances = expandStringList(v.([]interface{})) + } + + if v, ok := d.GetOk("slot"); ok { + input.Slots = expandLexSlots(expandLexSet(v.(*schema.Set))) + } + + _, err := RetryOnAwsCodes([]string{lexmodelbuildingservice.ErrCodeConflictException}, func() (interface{}, error) { + return conn.PutIntent(input) + }) + if err != nil { + return fmt.Errorf("error updating intent %s: %s", d.Id(), err) + } + + return resourceAwsLexIntentRead(d, meta) +} + +func resourceAwsLexIntentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lexmodelconn + + _, err := RetryOnAwsCodes([]string{lexmodelbuildingservice.ErrCodeConflictException}, func() (interface{}, error) { + return conn.DeleteIntent(&lexmodelbuildingservice.DeleteIntentInput{ + Name: aws.String(d.Id()), + }) + }) + + if err != nil { + return fmt.Errorf("error deleteing intent %s: %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_lex_intent_test.go b/aws/resource_aws_lex_intent_test.go new file mode 100644 index 00000000000..fecd12f79e4 --- /dev/null +++ b/aws/resource_aws_lex_intent_test.go @@ -0,0 +1,379 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lexmodelbuildingservice" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// Intents can accept a custom SlotType but it must be removed from the Intent before the SlotType can be deleted. +// This means we cannot reference the SlotType in the Intent with interpolation because Terraform will try to delete +// the SlotType first which will fail. So we do not have a test for custom slot types. + +func TestAccAwsLexIntent(t *testing.T) { + resourceName := "aws_lex_intent.test" + testID := acctest.RandStringFromCharSet(8, acctest.CharSetAlpha) + testIntentID := "test_intent_" + testID + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsLexIntentDestroy(testIntentID, lexVersionLatest), + + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccAwsLexIntentMinConfig, testID), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsLexIntentExists(testIntentID, lexVersionLatest), + + // user provided attributes + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.0.type", lexmodelbuildingservice.FulfillmentActivityTypeReturnIntent), + resource.TestCheckResourceAttr(resourceName, "name", testIntentID), + + // computed attributes + resource.TestCheckResourceAttrSet(resourceName, "checksum"), + resource.TestCheckResourceAttrSet(resourceName, "version"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: fmt.Sprintf(testAccAwsLexIntentUpdateWithConclusionConfig, testID), + Check: resource.ComposeAggregateTestCheckFunc( + // user updated attributes + resource.TestCheckResourceAttr(resourceName, "name", testIntentID), + resource.TestCheckResourceAttr(resourceName, "conclusion_statement.#", "1"), + resource.TestCheckResourceAttr(resourceName, "conclusion_statement.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.#", "1"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.0.max_attempts", "2"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "description", "Intent to order a bouquet of flowers for pick up"), + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.0.type", lexmodelbuildingservice.FulfillmentActivityTypeReturnIntent), + resource.TestCheckResourceAttr(resourceName, "rejection_statement.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rejection_statement.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "sample_utterances.#", "2"), + resource.TestCheckResourceAttr(resourceName, "slot.#", "1"), + + // computed attributes + resource.TestCheckResourceAttrSet(resourceName, "checksum"), + resource.TestCheckResourceAttrSet(resourceName, "version"), + ), + }, + { + Config: fmt.Sprintf(testAccAwsLexIntentUpdateWithConclusionSlotsConfig, testID), + Check: resource.ComposeAggregateTestCheckFunc( + // user updated attributes + resource.TestCheckResourceAttr(resourceName, "name", testIntentID), + resource.TestCheckResourceAttr(resourceName, "conclusion_statement.#", "1"), + resource.TestCheckResourceAttr(resourceName, "conclusion_statement.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.#", "1"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.0.max_attempts", "2"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "description", "Intent to order a bouquet of flowers for pick up"), + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.0.type", lexmodelbuildingservice.FulfillmentActivityTypeReturnIntent), + resource.TestCheckResourceAttr(resourceName, "rejection_statement.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rejection_statement.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "sample_utterances.#", "2"), + resource.TestCheckResourceAttr(resourceName, "slot.#", "2"), + + // computed attributes + resource.TestCheckResourceAttrSet(resourceName, "checksum"), + resource.TestCheckResourceAttrSet(resourceName, "version"), + ), + }, + { + Config: fmt.Sprintf(testAccAwsLexIntentUpdateWithFollowUpConfig, testID), + Check: resource.ComposeAggregateTestCheckFunc( + // user provided attributes + resource.TestCheckResourceAttr(resourceName, "name", testIntentID), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.#", "1"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.0.max_attempts", "2"), + resource.TestCheckResourceAttr(resourceName, "confirmation_prompt.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "description", "Intent to order a bouquet of flowers for pick up"), + resource.TestCheckResourceAttr(resourceName, "follow_up_prompt.#", "1"), + resource.TestCheckResourceAttr(resourceName, "follow_up_prompt.0.prompt.#", "1"), + resource.TestCheckResourceAttr(resourceName, "follow_up_prompt.0.prompt.0.max_attempts", "2"), + resource.TestCheckResourceAttr(resourceName, "follow_up_prompt.0.prompt.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "follow_up_prompt.0.rejection_statement.#", "1"), + resource.TestCheckResourceAttr(resourceName, "follow_up_prompt.0.rejection_statement.0.message.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fulfillment_activity.0.type", lexmodelbuildingservice.FulfillmentActivityTypeReturnIntent), + resource.TestCheckResourceAttr(resourceName, "rejection_statement.#", "1"), + resource.TestCheckResourceAttr(resourceName, "rejection_statement.0.message.#", "1"), + + // computed attributes + resource.TestCheckResourceAttrSet(resourceName, "checksum"), + resource.TestCheckResourceAttrSet(resourceName, "version"), + ), + }, + }, + }) +} + +func testAccCheckAwsLexIntentExists(intentName, intentVersion string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).lexmodelconn + + _, err := conn.GetIntent(&lexmodelbuildingservice.GetIntentInput{ + Name: aws.String(intentName), + Version: aws.String(intentVersion), + }) + if err != nil { + if isAWSErr(err, lexmodelbuildingservice.ErrCodeNotFoundException, "") { + return fmt.Errorf("error intent %s not found, %s", intentName, err) + } + + return fmt.Errorf("error getting intent %s: %s", intentName, err) + } + + return nil + } +} + +func testAccCheckAwsLexIntentDestroy(intentName, intentVersion string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).lexmodelconn + + _, err := conn.GetIntent(&lexmodelbuildingservice.GetIntentInput{ + Name: aws.String(intentName), + Version: aws.String(lexVersionLatest), + }) + + if err != nil { + if isAWSErr(err, lexmodelbuildingservice.ErrCodeNotFoundException, "") { + return nil + } + + return fmt.Errorf("error getting intent %s: %s", intentName, err) + } + + return fmt.Errorf("error intent still exists after delete, %s", intentName) + } +} + +const testAccAwsLexIntentMinConfig = ` +resource "aws_lex_intent" "test" { + fulfillment_activity { + type = "ReturnIntent" + } + + name = "test_intent_%[1]s" +} +` + +// with conclusion statement + +const testAccAwsLexIntentUpdateWithConclusionConfig = ` +resource "aws_lex_intent" "test" { + conclusion_statement { + message { + content = "Your order for {FlowerType} has been placed and will be ready by {PickupTime} on {PickupDate}" + content_type = "PlainText" + } + } + + confirmation_prompt { + max_attempts = 2 + + message { + content = "Okay, your {FlowerType} will be ready for pickup by {PickupTime} on {PickupDate}. Does this sound okay?" + content_type = "PlainText" + } + } + + description = "Intent to order a bouquet of flowers for pick up" + + // requires a lambda function to test + // dialog_code_hook { + // message_version = "1" + // uri = "arn:aws:lambda:us-east-1:123456789012:function:RetrieveAvailableFlowers" + // } + + fulfillment_activity { + // requires a lambda function to test + // code_hook { + // message_version = "1" + // uri = "arn:aws:lambda:us-east-1:123456789012:function:ProcessFlowerOrder" + // } + + type = "ReturnIntent" + } + name = "test_intent_%[1]s" + rejection_statement { + message { + content = "Okay, I will not place your order." + content_type = "PlainText" + } + } + sample_utterances = [ + "I would like to order some flowers", + "I would like to pick up flowers", + ] + slot { + description = "The date to pick up the flowers" + name = "PickupDate" + priority = 2 + + sample_utterances = [ + "I would like to order {FlowerType}", + ] + + slot_constraint = "Required" + slot_type = "AMAZON.DATE" + + value_elicitation_prompt { + max_attempts = 2 + + message { + content = "What day do you want the {FlowerType} to be picked up?" + content_type = "PlainText" + } + } + } +} +` + +// with conclusion update slots + +const testAccAwsLexIntentUpdateWithConclusionSlotsConfig = ` +resource "aws_lex_intent" "test" { + conclusion_statement { + message { + content = "Your order for {FlowerType} has been placed and will be ready by {PickupTime} on {PickupDate}" + content_type = "PlainText" + } + } + + confirmation_prompt { + max_attempts = 2 + + message { + content = "Okay, your {FlowerType} will be ready for pickup by {PickupTime} on {PickupDate}. Does this sound okay?" + content_type = "PlainText" + } + } + + description = "Intent to order a bouquet of flowers for pick up" + + fulfillment_activity { + type = "ReturnIntent" + } + + name = "test_intent_%[1]s" + + rejection_statement { + message { + content = "Okay, I will not place your order." + content_type = "PlainText" + } + } + + sample_utterances = [ + "I would like to order some flowers", + "I would like to pick up flowers", + ] + + slot { + description = "The date to pick up the flowers" + name = "PickupDate" + priority = 2 + + sample_utterances = [ + "I would like to order {FlowerType}", + ] + + slot_constraint = "Required" + slot_type = "AMAZON.DATE" + + value_elicitation_prompt { + max_attempts = 2 + + message { + content = "What day do you want the {FlowerType} to be picked up?" + content_type = "PlainText" + } + } + } + + slot { + description = "The time to pick up the flowers" + name = "PickupTime" + priority = 3 + + sample_utterances = [ + "I would like to order {FlowerType}", + ] + + slot_constraint = "Required" + slot_type = "AMAZON.TIME" + + value_elicitation_prompt { + max_attempts = 2 + + message { + content = "Pick up the {FlowerType} at what time on {PickupDate}?" + content_type = "PlainText" + } + } + } +} +` + +// with follow up prompt + +const testAccAwsLexIntentUpdateWithFollowUpConfig = ` +resource "aws_lex_intent" "test" { + confirmation_prompt { + max_attempts = 2 + + message { + content = "Okay, your {FlowerType} will be ready for pickup by {PickupTime} on {PickupDate}. Does this sound okay?" + content_type = "PlainText" + } + } + + description = "Intent to order a bouquet of flowers for pick up" + + follow_up_prompt { + prompt { + max_attempts = 2 + + message { + content = "Would you like to place another order?" + content_type = "PlainText" + } + } + + rejection_statement { + message { + content = "Your order for {FlowerType} has been placed and will be ready by {PickupTime} on {PickupDate}" + content_type = "PlainText" + } + } + } + + fulfillment_activity { + type = "ReturnIntent" + } + + name = "test_intent_%[1]s" + + rejection_statement { + message { + content = "Okay, I will not place your order." + content_type = "PlainText" + } + } +} +` diff --git a/aws/validators_lex_constants.go b/aws/validators_lex_constants.go new file mode 100644 index 00000000000..e2d77cf4ec6 --- /dev/null +++ b/aws/validators_lex_constants.go @@ -0,0 +1,70 @@ +package aws + +// Amazon Lex Resource Constants. Data models are documented here +// https://docs.aws.amazon.com/lex/latest/dg/API_Types_Amazon_Lex_Model_Building_Service.html + +const ( + + // General + + lexNameMinLength = 1 + lexNameMaxLength = 100 + lexNameRegex = "^([A-Za-z]_?)+$" + + lexVersionMinLength = 1 + lexVersionMaxLength = 64 + lexVersionRegex = "\\$LATEST|[0-9]+" + lexVersionLatest = "$LATEST" + lexVersionDefault = "$LATEST" + + lexDescriptionMinLength = 0 + lexDescriptionMaxLength = 200 + lexDescriptionDefault = "" + + // Message + + lexMessageContentMinLength = 1 + lexMessageContentMaxLength = 1000 + lexMessageGroupNumberMin = 1 + lexMessageGroupNumberMax = 5 + + // Statement + + lexResponseCardMinLength = 1 + lexResponseCardMaxLength = 50000 + lexStatementMessagesMin = 1 + lexStatementMessagesMax = 15 + + // Prompt + + lexPromptMaxAttemptsMin = 1 + lexPromptMaxAttemptsMax = 5 + + // Code Hook + + lexCodeHookMessageVersionMinLength = 1 + lexCodeHookMessageVersionMaxLength = 5 + + // Slot + + lexSlotsMin = 0 + lexSlotsMax = 100 + lexSlotPriorityMin = 0 + lexSlotPriorityMax = 100 + lexSlotPriorityDefault = 0 + lexSlotSampleUtterancesMin = 1 + lexSlotSampleUtterancesMax = 10 + + // Slot Type + + lexSlotTypeMinLength = 1 + lexSlotTypeMaxLength = 100 + lexSlotTypeRegex = "^((AMAZON\\.)_?|[A-Za-z]_?)+" + + // Utterance + + lexUtterancesMin = 0 + lexUtterancesMax = 1500 + lexUtteranceMinLength = 1 + lexUtteranceMaxLength = 200 +) diff --git a/website/aws.erb b/website/aws.erb index 72efcc2f0c2..133505465e2 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -312,6 +312,9 @@
  • aws_lb_target_group
  • +
  • + aws_lex_intent +
  • aws_mq_broker
  • @@ -1804,6 +1807,15 @@ +
  • + Lex Resources + +
  • +
  • License Manager Resources