diff --git a/.changelog/30784.txt b/.changelog/30784.txt new file mode 100644 index 00000000000..ce99935fbb6 --- /dev/null +++ b/.changelog/30784.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_vpclattice_listener_rule +``` \ No newline at end of file diff --git a/internal/service/vpclattice/listener.go b/internal/service/vpclattice/listener.go index 514734bfbdc..a0cd7a58054 100644 --- a/internal/service/vpclattice/listener.go +++ b/internal/service/vpclattice/listener.go @@ -335,7 +335,7 @@ func flattenListenerRuleActions(config types.RuleAction) []interface{} { switch v := config.(type) { case *types.RuleActionMemberFixedResponse: - m["fixed_response"] = flattenRuleActionMemberFixedResponse(&v.Value) + m["fixed_response"] = flattenFixedResponseAction(&v.Value) case *types.RuleActionMemberForward: m["forward"] = flattenComplexDefaultActionForward(&v.Value) } @@ -344,7 +344,7 @@ func flattenListenerRuleActions(config types.RuleAction) []interface{} { } // Flatten function for fixed_response action -func flattenRuleActionMemberFixedResponse(response *types.FixedResponseAction) []interface{} { +func flattenFixedResponseAction(response *types.FixedResponseAction) []interface{} { tfMap := map[string]interface{}{} if v := response.StatusCode; v != nil { diff --git a/internal/service/vpclattice/listener_rule.go b/internal/service/vpclattice/listener_rule.go new file mode 100644 index 00000000000..dccc5f8f03a --- /dev/null +++ b/internal/service/vpclattice/listener_rule.go @@ -0,0 +1,883 @@ +package vpclattice + +import ( + "context" + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/vpclattice" + "github.com/aws/aws-sdk-go-v2/service/vpclattice/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKResource("aws_vpclattice_listener_rule", name="Listener Rule") +// @Tags(identifierAttribute="arn") +func ResourceListenerRule() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceListenerRuleCreate, + ReadWithoutTimeout: resourceListenerRuleRead, + UpdateWithoutTimeout: resourceListenerRuleUpdate, + DeleteWithoutTimeout: resourceListenerRuleDelete, + + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + idParts := strings.Split(d.Id(), "/") + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + return nil, fmt.Errorf("unexpected format of ID (%q), expected SERVICE-ID/LISTENER-ID/RULE-ID", d.Id()) + } + serviceIdentifier := idParts[0] + listenerIdentifier := idParts[1] + ruleId := idParts[2] + d.Set("service_identifier", serviceIdentifier) + d.Set("listener_identifier", listenerIdentifier) + d.Set("rule_id", ruleId) + + return []*schema.ResourceData{d}, nil + }, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "fixed_response": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "status_code": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "forward": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target_groups": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 2, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target_group_identifier": { + Type: schema.TypeString, + Required: true, + }, + "weight": { + Type: schema.TypeInt, + ValidateFunc: validation.IntBetween(0, 999), + Default: 100, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "listener_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "match": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "http_match": { + Type: schema.TypeList, + Optional: true, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "method": { + Type: schema.TypeString, + Optional: true, + }, + "header_matches": { + Type: schema.TypeList, + Optional: true, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + MinItems: 1, + MaxItems: 5, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "case_sensitive": { + Type: schema.TypeBool, + Optional: true, + }, + "match": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "contains": { + Type: schema.TypeString, + Optional: true, + }, + "exact": { + Type: schema.TypeString, + Optional: true, + }, + "prefix": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "path_match": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "case_sensitive": { + Type: schema.TypeBool, + Optional: true, + }, + "match": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "exact": { + Type: schema.TypeString, + Optional: true, + }, + "prefix": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(3, 63), + }, + "priority": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 100), + }, + "rule_id": { + Type: schema.TypeString, + Computed: true, + }, + "service_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + }, + + CustomizeDiff: customdiff.All( + verify.SetTagsDiff, + ), + } +} + +const ( + ResNameListenerRule = "Listener Rule" +) + +func resourceListenerRuleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + name := d.Get("name").(string) + in := &vpclattice.CreateRuleInput{ + Action: expandRuleAction(d.Get("action").([]interface{})[0].(map[string]interface{})), + ClientToken: aws.String(id.UniqueId()), + ListenerIdentifier: aws.String(d.Get("listener_identifier").(string)), + Match: expandRuleMatch(d.Get("match").([]interface{})[0].(map[string]interface{})), + Name: aws.String(name), + ServiceIdentifier: aws.String(d.Get("service_identifier").(string)), + Tags: GetTagsIn(ctx), + } + + if v, ok := d.GetOk("priority"); ok { + in.Priority = aws.Int32(int32(v.(int))) + } + + out, err := conn.CreateRule(ctx, in) + if err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionCreating, ResNameListenerRule, name, err) + } + + if out == nil || out.Arn == nil { + return create.DiagError(names.VPCLattice, create.ErrActionCreating, ResNameListenerRule, d.Get("name").(string), errors.New("empty output")) + } + + d.Set("rule_id", out.Id) + d.Set("service_identifier", in.ServiceIdentifier) + d.Set("listener_identifier", in.ListenerIdentifier) + + parts := []string{ + d.Get("service_identifier").(string), + d.Get("listener_identifier").(string), + d.Get("rule_id").(string), + } + + d.SetId(strings.Join(parts, "/")) + + return resourceListenerRuleRead(ctx, d, meta) +} + +func resourceListenerRuleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + serviceId := d.Get("service_identifier").(string) + listenerId := d.Get("listener_identifier").(string) + ruleId := d.Get("rule_id").(string) + + out, err := FindListenerRuleByID(ctx, conn, serviceId, listenerId, ruleId) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] VpcLattice Listener Rule (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionReading, ResNameListenerRule, d.Id(), err) + } + + d.Set("arn", out.Arn) + d.Set("priority", out.Priority) + d.Set("name", out.Name) + d.Set("listener_identifier", listenerId) + d.Set("service_identifier", serviceId) + d.Set("rule_id", out.Id) + + if err := d.Set("action", []interface{}{flattenRuleAction(out.Action)}); err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionSetting, ResNameListenerRule, d.Id(), err) + } + + if err := d.Set("match", []interface{}{flattenRuleMatch(out.Match)}); err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionSetting, ResNameListenerRule, d.Id(), err) + } + + return nil +} + +func resourceListenerRuleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + serviceId := d.Get("service_identifier").(string) + listenerId := d.Get("listener_identifier").(string) + ruleId := d.Get("rule_id").(string) + + if d.HasChangesExcept("tags", "tags_all") { + in := &vpclattice.UpdateRuleInput{ + RuleIdentifier: aws.String(ruleId), + ListenerIdentifier: aws.String(listenerId), + ServiceIdentifier: aws.String(serviceId), + } + + if d.HasChange("action") { + if v, ok := d.GetOk("action"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + in.Action = expandRuleAction(v.([]interface{})[0].(map[string]interface{})) + } + } + + if d.HasChange("match") { + if v, ok := d.GetOk("match"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + in.Match = expandRuleMatch(v.([]interface{})[0].(map[string]interface{})) + } + } + _, err := conn.UpdateRule(ctx, in) + if err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionUpdating, ResNameListenerRule, d.Id(), err) + } + } + + return resourceListenerRuleRead(ctx, d, meta) +} + +func resourceListenerRuleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + serviceId := d.Get("service_identifier").(string) + listenerId := d.Get("listener_identifier").(string) + ruleId := d.Get("rule_id").(string) + + log.Printf("[INFO] Deleting VpcLattice Listening Rule: %s", d.Id()) + _, err := conn.DeleteRule(ctx, &vpclattice.DeleteRuleInput{ + ListenerIdentifier: aws.String(listenerId), + RuleIdentifier: aws.String(ruleId), + ServiceIdentifier: aws.String(serviceId), + }) + + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + + return create.DiagError(names.VPCLattice, create.ErrActionDeleting, ResNameListenerRule, d.Id(), err) + } + + return nil +} + +func FindListenerRuleByID(ctx context.Context, conn *vpclattice.Client, serviceIdentifier string, listenerIdentifier string, ruleId string) (*vpclattice.GetRuleOutput, error) { + in := &vpclattice.GetRuleInput{ + ListenerIdentifier: aws.String(listenerIdentifier), + RuleIdentifier: aws.String(ruleId), + ServiceIdentifier: aws.String(serviceIdentifier), + } + out, err := conn.GetRule(ctx, in) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + if out == nil || out.Id == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +func flattenRuleAction(apiObject types.RuleAction) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := make(map[string]interface{}) + + if v, ok := apiObject.(*types.RuleActionMemberFixedResponse); ok { + tfMap["fixed_response"] = []interface{}{flattenRuleActionMemberFixedResponse(v)} + } + if v, ok := apiObject.(*types.RuleActionMemberForward); ok { + tfMap["forward"] = []interface{}{flattenForwardAction(v)} + } + + return tfMap +} + +func flattenRuleActionMemberFixedResponse(apiObject *types.RuleActionMemberFixedResponse) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Value.StatusCode; v != nil { + tfMap["status_code"] = aws.ToInt32(v) + } + + return tfMap +} + +func flattenForwardAction(apiObject *types.RuleActionMemberForward) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Value.TargetGroups; v != nil { + tfMap["target_groups"] = flattenWeightedTargetGroups(v) + } + + return tfMap +} + +func flattenWeightedTargetGroups(apiObjects []types.WeightedTargetGroup) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + tfList = append(tfList, flattenWeightedTargetGroup(&apiObject)) + } + + return tfList +} + +func flattenWeightedTargetGroup(apiObject *types.WeightedTargetGroup) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.TargetGroupIdentifier; v != nil { + tfMap["target_group_identifier"] = aws.ToString(v) + } + + if v := apiObject.Weight; v != nil { + tfMap["weight"] = aws.ToInt32(v) + } + + return tfMap +} + +func flattenRuleMatch(apiObject types.RuleMatch) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := make(map[string]interface{}) + + if v, ok := apiObject.(*types.RuleMatchMemberHttpMatch); ok { + tfMap["http_match"] = []interface{}{flattenHTTPMatch(&v.Value)} + } + + return tfMap +} + +func flattenHTTPMatch(apiObject *types.HttpMatch) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Method; v != nil { + tfMap["method"] = aws.ToString(v) + } + + if v := apiObject.HeaderMatches; v != nil { + tfMap["header_matches"] = flattenHeaderMatches(v) + } + + if v := apiObject.PathMatch; v != nil { + tfMap["path_match"] = flattenPathMatch(v) + } + + return tfMap +} + +func flattenHeaderMatches(apiObjects []types.HeaderMatch) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + tfList = append(tfList, flattenHeaderMatch(&apiObject)) + } + + return tfList +} + +func flattenHeaderMatch(apiObject *types.HeaderMatch) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.CaseSensitive; v != nil { + tfMap["case_sensitive"] = aws.ToBool(v) + } + + if v := apiObject.Name; v != nil { + tfMap["name"] = aws.ToString(v) + } + + if v := apiObject.Match; v != nil { + tfMap["match"] = []interface{}{flattenHeaderMatchType(v)} + } + + return tfMap +} +func flattenHeaderMatchType(apiObject types.HeaderMatchType) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := make(map[string]interface{}) + + if v, ok := apiObject.(*types.HeaderMatchTypeMemberContains); ok { + return flattenHeaderMatchTypeMemberContains(v) + } else if v, ok := apiObject.(*types.HeaderMatchTypeMemberExact); ok { + return flattenHeaderMatchTypeMemberExact(v) + } else if v, ok := apiObject.(*types.HeaderMatchTypeMemberPrefix); ok { + return flattenHeaderMatchTypeMemberPrefix(v) + } + + return tfMap +} + +func flattenHeaderMatchTypeMemberContains(apiObject *types.HeaderMatchTypeMemberContains) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "contains": apiObject.Value, + } + + return tfMap +} + +func flattenHeaderMatchTypeMemberExact(apiObject *types.HeaderMatchTypeMemberExact) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "exact": apiObject.Value, + } + + return tfMap +} + +func flattenHeaderMatchTypeMemberPrefix(apiObject *types.HeaderMatchTypeMemberPrefix) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "prefix": apiObject.Value, + } + + return tfMap +} + +func flattenPathMatch(apiObject *types.PathMatch) []interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.CaseSensitive; v != nil { + tfMap["case_sensitive"] = aws.ToBool(v) + } + + if v := apiObject.Match; v != nil { + tfMap["match"] = []interface{}{flattenPathMatchType(v)} + } + + return []interface{}{tfMap} +} + +func flattenPathMatchType(apiObject types.PathMatchType) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := make(map[string]interface{}) + + if v, ok := apiObject.(*types.PathMatchTypeMemberExact); ok { + return flattenPathMatchTypeMemberExact(v) + } else if v, ok := apiObject.(*types.PathMatchTypeMemberPrefix); ok { + return flattenPathMatchTypeMemberPrefix(v) + } + + return tfMap +} + +func flattenPathMatchTypeMemberExact(apiObject *types.PathMatchTypeMemberExact) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "exact": apiObject.Value, + } + + return tfMap +} + +func flattenPathMatchTypeMemberPrefix(apiObject *types.PathMatchTypeMemberPrefix) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "prefix": apiObject.Value, + } + + return tfMap +} + +func expandRuleAction(tfMap map[string]interface{}) types.RuleAction { + var apiObject types.RuleAction + + if v, ok := tfMap["fixed_response"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + apiObject = expandFixedResponseAction(v[0].(map[string]interface{})) + } else if v, ok := tfMap["forward"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + apiObject = expandForwardAction(v[0].(map[string]interface{})) + } + + return apiObject +} + +func expandFixedResponseAction(tfMap map[string]interface{}) *types.RuleActionMemberFixedResponse { + apiObject := &types.RuleActionMemberFixedResponse{} + + if v, ok := tfMap["status_code"].(int); ok && v != 0 { + apiObject.Value.StatusCode = aws.Int32(int32(v)) + } + + return apiObject +} + +func expandForwardAction(tfMap map[string]interface{}) *types.RuleActionMemberForward { + apiObject := &types.RuleActionMemberForward{} + + if v, ok := tfMap["target_groups"].([]interface{}); ok && len(v) > 0 && v != nil { + apiObject.Value.TargetGroups = expandWeightedTargetGroups(v) + } + + return apiObject +} + +func expandWeightedTargetGroups(tfList []interface{}) []types.WeightedTargetGroup { + if len(tfList) == 0 { + return nil + } + + var apiObjects []types.WeightedTargetGroup + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandWeightedTargetGroup(tfMap) + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandWeightedTargetGroup(tfMap map[string]interface{}) types.WeightedTargetGroup { + apiObject := types.WeightedTargetGroup{} + + if v, ok := tfMap["target_group_identifier"].(string); ok && v != "" { + apiObject.TargetGroupIdentifier = aws.String(v) + } + + if v, ok := tfMap["weight"].(int); ok && v != 0 { + apiObject.Weight = aws.Int32(int32(v)) + } + + return apiObject +} + +func expandRuleMatch(tfMap map[string]interface{}) types.RuleMatch { + apiObject := &types.RuleMatchMemberHttpMatch{} + + if v, ok := tfMap["http_match"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + apiObject.Value = expandHTTPMatch(v[0].(map[string]interface{})) + } + + return apiObject +} + +func expandHTTPMatch(tfMap map[string]interface{}) types.HttpMatch { + apiObject := types.HttpMatch{} + + if v, ok := tfMap["header_matches"].([]interface{}); ok && len(v) > 0 && v != nil { + apiObject.HeaderMatches = expandHeaderMatches(v) + } + + if v, ok := tfMap["method"].(string); ok { + apiObject.Method = aws.String(v) + } + + if v, ok := tfMap["path_match"].([]interface{}); ok && len(v) > 0 && v != nil { + apiObject.PathMatch = expandPathMatch(v[0].(map[string]interface{})) + } + + return apiObject +} + +func expandHeaderMatches(tfList []interface{}) []types.HeaderMatch { + if len(tfList) == 0 { + return nil + } + + var apiObjects []types.HeaderMatch + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandHeaderMatch(tfMap) + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandHeaderMatch(tfMap map[string]interface{}) types.HeaderMatch { + apiObject := types.HeaderMatch{} + + if v, ok := tfMap["case_sensitive"].(bool); ok { + apiObject.CaseSensitive = aws.Bool(v) + } + + if v, ok := tfMap["name"].(string); ok { + apiObject.Name = aws.String(v) + } + + if v, ok := tfMap["match"].([]interface{}); ok && len(v) > 0 { + matchObj := v[0].(map[string]interface{}) + if matchV, ok := matchObj["exact"].(string); ok && matchV != "" { + apiObject.Match = expandHeaderMatchTypeMemberExact(matchObj) + } + if matchV, ok := matchObj["prefix"].(string); ok && matchV != "" { + apiObject.Match = expandHeaderMatchTypeMemberPrefix(matchObj) + } + if matchV, ok := matchObj["contains"].(string); ok && matchV != "" { + apiObject.Match = expandHeaderMatchTypeMemberContains(matchObj) + } + } + + return apiObject +} + +func expandHeaderMatchTypeMemberContains(tfMap map[string]interface{}) types.HeaderMatchType { + apiObject := &types.HeaderMatchTypeMemberContains{} + + if v, ok := tfMap["contains"].(string); ok && v != "" { + apiObject.Value = v + } + return apiObject +} + +func expandHeaderMatchTypeMemberPrefix(tfMap map[string]interface{}) types.HeaderMatchType { + apiObject := &types.HeaderMatchTypeMemberPrefix{} + + if v, ok := tfMap["prefix"].(string); ok && v != "" { + apiObject.Value = v + } + return apiObject +} + +func expandHeaderMatchTypeMemberExact(tfMap map[string]interface{}) types.HeaderMatchType { + apiObject := &types.HeaderMatchTypeMemberExact{} + + if v, ok := tfMap["exact"].(string); ok && v != "" { + apiObject.Value = v + } + return apiObject +} + +func expandPathMatch(tfMap map[string]interface{}) *types.PathMatch { + apiObject := &types.PathMatch{} + + if v, ok := tfMap["case_sensitive"].(bool); ok { + apiObject.CaseSensitive = aws.Bool(v) + } + + if v, ok := tfMap["match"].([]interface{}); ok && len(v) > 0 { + matchObj := v[0].(map[string]interface{}) + if matchV, ok := matchObj["exact"].(string); ok && matchV != "" { + apiObject.Match = expandPathMatchTypeMemberExact(matchObj) + } + if matchV, ok := matchObj["prefix"].(string); ok && matchV != "" { + apiObject.Match = expandPathMatchTypeMemberPrefix(matchObj) + } + } + + return apiObject +} + +func expandPathMatchTypeMemberExact(tfMap map[string]interface{}) types.PathMatchType { + apiObject := &types.PathMatchTypeMemberExact{} + + if v, ok := tfMap["exact"].(string); ok && v != "" { + apiObject.Value = v + } + + return apiObject +} + +func expandPathMatchTypeMemberPrefix(tfMap map[string]interface{}) types.PathMatchType { + apiObject := &types.PathMatchTypeMemberPrefix{} + + if v, ok := tfMap["prefix"].(string); ok && v != "" { + apiObject.Value = v + } + return apiObject +} diff --git a/internal/service/vpclattice/listener_rule_test.go b/internal/service/vpclattice/listener_rule_test.go new file mode 100644 index 00000000000..6196f45851e --- /dev/null +++ b/internal/service/vpclattice/listener_rule_test.go @@ -0,0 +1,424 @@ +package vpclattice_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/vpclattice" + "github.com/aws/aws-sdk-go-v2/service/vpclattice/types" + sdkacctest "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/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfvpclattice "github.com/hashicorp/terraform-provider-aws/internal/service/vpclattice" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccVPCLatticeListenerRule_basic(t *testing.T) { + ctx := acctest.Context(t) + var listenerRule vpclattice.GetRuleOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_listener_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VPCLatticeEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccChecklistenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &listenerRule), + resource.TestCheckResourceAttr(resourceName, "priority", "20"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "vpc-lattice", regexp.MustCompile(`service/svc-.*/listener/listener-.*/rule/rule.+`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVPCLatticeListenerRule_fixedResponse(t *testing.T) { + ctx := acctest.Context(t) + var listenerRule vpclattice.GetRuleOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_listener_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VPCLatticeEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccChecklistenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleConfig_fixedResponse(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &listenerRule), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "priority", "10"), + resource.TestCheckResourceAttr(resourceName, "action.0.fixed_response.0.status_code", "404"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVPCLatticeListenerRule_methodMatch(t *testing.T) { + ctx := acctest.Context(t) + var listenerRule vpclattice.GetRuleOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_listener_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccChecklistenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleConfig_methodMatch(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &listenerRule), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "priority", "40"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVPCLatticeListenerRule_tags(t *testing.T) { + ctx := acctest.Context(t) + var listenerRule vpclattice.GetRuleOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_listener_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccChecklistenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &listenerRule), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccListenerRuleConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &listenerRule), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckListenerRuleExists(ctx context.Context, name string, rule *vpclattice.GetRuleOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.VPCLattice, create.ErrActionCheckingExistence, tfvpclattice.ResNameListenerRule, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.VPCLattice, create.ErrActionCheckingExistence, tfvpclattice.ResNameListenerRule, name, errors.New("not set")) + } + + serviceIdentifier := rs.Primary.Attributes["service_identifier"] + listenerIdentifier := rs.Primary.Attributes["listener_identifier"] + + conn := acctest.Provider.Meta().(*conns.AWSClient).VPCLatticeClient() + resp, err := conn.GetRule(ctx, &vpclattice.GetRuleInput{ + RuleIdentifier: aws.String(rs.Primary.Attributes["arn"]), + ListenerIdentifier: aws.String(listenerIdentifier), + ServiceIdentifier: aws.String(serviceIdentifier), + }) + + if err != nil { + return create.Error(names.VPCLattice, create.ErrActionCheckingExistence, tfvpclattice.ResNameListenerRule, rs.Primary.ID, err) + } + + *rule = *resp + + return nil + } +} + +func testAccChecklistenerRuleDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).VPCLatticeClient() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpclattice_listener_rule" { + continue + } + + listenerIdentifier := rs.Primary.Attributes["listener_identifier"] + serviceIdentifier := rs.Primary.Attributes["service_identifier"] + + _, err := conn.GetRule(ctx, &vpclattice.GetRuleInput{ + RuleIdentifier: aws.String(rs.Primary.Attributes["arn"]), + ListenerIdentifier: aws.String(listenerIdentifier), + ServiceIdentifier: aws.String(serviceIdentifier), + }) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + return err + } + + return create.Error(names.VPCLattice, create.ErrActionCheckingDestroyed, tfvpclattice.ResNameListenerRule, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccListenerRuleConfig_base(rName string) string { + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 0), fmt.Sprintf(` +resource "aws_vpclattice_service" "test" { + name = %[1]q +} + +resource "aws_vpclattice_target_group" "test" { + count = 2 + + name = "%[1]s-${count.index}" + type = "INSTANCE" + + config { + port = 80 + protocol = "HTTP" + vpc_identifier = aws_vpc.test.id + } +} + +resource "aws_vpclattice_listener" "test" { + name = %[1]q + protocol = "HTTP" + service_identifier = aws_vpclattice_service.test.id + default_action { + fixed_response { + status_code = 404 + } + } +} +`, rName)) +} + +func testAccListenerRuleConfig_basic(rName string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_base(rName), fmt.Sprintf(` +resource "aws_vpclattice_listener_rule" "test" { + name = %[1]q + listener_identifier = aws_vpclattice_listener.test.listener_id + service_identifier = aws_vpclattice_service.test.id + priority = 20 + match { + http_match { + + header_matches { + name = "example-header" + case_sensitive = false + + match { + exact = "example-contains" + } + } + + path_match { + case_sensitive = true + match { + prefix = "/example-path" + } + } + } + } + action { + forward { + target_groups { + target_group_identifier = aws_vpclattice_target_group.test[0].id + weight = 1 + } + target_groups { + target_group_identifier = aws_vpclattice_target_group.test[1].id + weight = 2 + } + } + } +} +`, rName)) +} + +func testAccListenerRuleConfig_fixedResponse(rName string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_base(rName), fmt.Sprintf(` +resource "aws_vpclattice_listener_rule" "test" { + name = %[1]q + listener_identifier = aws_vpclattice_listener.test.listener_id + service_identifier = aws_vpclattice_service.test.id + priority = 10 + match { + http_match { + path_match { + case_sensitive = false + match { + exact = "/example-path" + } + } + } + } + action { + fixed_response { + status_code = 404 + } + } +} +`, rName)) +} + +func testAccListenerRuleConfig_tags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_base(rName), fmt.Sprintf(` +resource "aws_vpclattice_listener_rule" "test" { + name = %[1]q + listener_identifier = aws_vpclattice_listener.test.listener_id + service_identifier = aws_vpclattice_service.test.id + priority = 30 + match { + http_match { + path_match { + case_sensitive = false + match { + prefix = "/example-path" + } + } + } + } + action { + fixed_response { + status_code = 404 + } + } + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1)) +} + +func testAccListenerRuleConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_base(rName), fmt.Sprintf(` +resource "aws_vpclattice_listener_rule" "test" { + name = %[1]q + listener_identifier = aws_vpclattice_listener.test.listener_id + service_identifier = aws_vpclattice_service.test.id + priority = 30 + match { + http_match { + path_match { + case_sensitive = false + match { + prefix = "/example-path" + } + } + } + } + action { + fixed_response { + status_code = 404 + } + } + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} + +func testAccListenerRuleConfig_methodMatch(rName string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_base(rName), fmt.Sprintf(` +resource "aws_vpclattice_listener_rule" "test" { + name = %[1]q + listener_identifier = aws_vpclattice_listener.test.listener_id + service_identifier = aws_vpclattice_service.test.id + priority = 40 + match { + http_match { + + method = "POST" + + header_matches { + name = "example-header" + case_sensitive = false + + match { + contains = "example-contains" + } + } + + path_match { + case_sensitive = true + match { + prefix = "/example-path" + } + } + + } + } + action { + forward { + target_groups { + target_group_identifier = aws_vpclattice_target_group.test[0].id + weight = 1 + } + target_groups { + target_group_identifier = aws_vpclattice_target_group.test[1].id + weight = 2 + } + } + } +} +`, rName)) +} diff --git a/internal/service/vpclattice/listener_test.go b/internal/service/vpclattice/listener_test.go index e7127cf4d6b..b98bd80663c 100644 --- a/internal/service/vpclattice/listener_test.go +++ b/internal/service/vpclattice/listener_test.go @@ -517,8 +517,6 @@ resource "aws_vpclattice_target_group" "test" { vpc_identifier = aws_vpc.test.id } } - - `, rName)) } diff --git a/internal/service/vpclattice/service_package_gen.go b/internal/service/vpclattice/service_package_gen.go index 4966221eff0..d3e7f7d8a32 100644 --- a/internal/service/vpclattice/service_package_gen.go +++ b/internal/service/vpclattice/service_package_gen.go @@ -38,6 +38,14 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka IdentifierAttribute: "arn", }, }, + { + Factory: ResourceListenerRule, + TypeName: "aws_vpclattice_listener_rule", + Name: "Listener Rule", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, { Factory: ResourceService, TypeName: "aws_vpclattice_service", diff --git a/website/docs/r/vpclattice_listener_rule.html.markdown b/website/docs/r/vpclattice_listener_rule.html.markdown new file mode 100644 index 00000000000..77b968dd699 --- /dev/null +++ b/website/docs/r/vpclattice_listener_rule.html.markdown @@ -0,0 +1,167 @@ +--- +subcategory: "VPC Lattice" +layout: "aws" +page_title: "AWS: aws_vpclattice_listener_rule" +description: |- + Terraform resource for managing an AWS VPC Lattice Listener Rule. +--- + +# Resource: aws_vpclattice_listener_rule + +Terraform resource for managing an AWS VPC Lattice Listener Rule. + +## Example Usage + +```terraform +resource "aws_vpclattice_listener_rule" "test" { + name = "example" + listener_identifier = aws_vpclattice_listener.example.listener_id + service_identifier = aws_vpclattice_service.example.id + priority = 20 + match { + http_match { + + header_matches { + name = "example-header" + case_sensitive = false + + match { + exact = "example-contains" + } + } + + path_match { + case_sensitive = true + match { + prefix = "/example-path" + } + } + } + } + action { + forward { + target_groups { + target_group_identifier = aws_vpclattice_target_group.example.id + weight = 1 + } + target_groups { + target_group_identifier = aws_vpclattice_target_group.example2.id + weight = 2 + } + } + + } +} +``` + +### Basic Usage + +```terraform +resource "aws_vpclattice_listener_rule" "test" { + name = "example" + listener_identifier = aws_vpclattice_listener.example.listener_id + service_identifier = aws_vpclattice_service.example.id + priority = 10 + match { + http_match { + path_match { + case_sensitive = false + match { + exact = "/example-path" + } + } + } + } + action { + fixed_response { + status_code = 404 + } + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `service_identifier` - (Required) The ID or Amazon Resource Identifier (ARN) of the service. +* `listener_identifier` - (Required) The ID or Amazon Resource Name (ARN) of the listener. +* `action` - (Required) The action for the default rule. +* `match` - (Required) The rule match. +* `name` - (Required) The name of the rule. The name must be unique within the listener. The valid characters are a-z, 0-9, and hyphens (-). You can't use a hyphen as the first or last character, or immediately after another hyphen. +* `priority` - (Required) The priority assigned to the rule. Each rule for a specific listener must have a unique priority. The lower the priority number the higher the priority. + +The following arguments are optional: + +* `tags` - (Optional) Key-value mapping of resource tags. 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. + +action (`action`) supports the following: + +* `fixed_response` - (Optional) Describes the rule action that returns a custom HTTP response. +* `forward` - (Optional) The forward action. Traffic that matches the rule is forwarded to the specified target groups. + +fixed response (`fixed_response`) supports the following: + +* `status_code` - (Optional) The HTTP response code. + +forward (`forward`) supports the following: + +* `target_groups` - (Optional) The target groups. Traffic matching the rule is forwarded to the specified target groups. With forward actions, you can assign a weight that controls the prioritization and selection of each target group. This means that requests are distributed to individual target groups based on their weights. For example, if two target groups have the same weight, each target group receives half of the traffic. + +The default value is 1 with maximum number of 2. If only one target group is provided, there is no need to set the weight; 100% of traffic will go to that target group. + +action (`match`) supports the following: + +* `http_match` - (Optional) The HTTP criteria that a rule must match. + +http match (`http_match`) supports the following: + +* `header_matches` - (Optional) The header matches. Matches incoming requests with rule based on request header value before applying rule action. +* `method` - (Optional) The HTTP method type. +* `path_match` - (Optional) The path match. + +header matches (`header_matches`) supports the following: + +* `case_sensitive` - (Optional) Indicates whether the match is case sensitive. Defaults to false. +* `match` - (Optional) The header match type. +* `name` - (Optional) The name of the header. + +header matches match (`match`) supports the following: + +* `contains` - (Optional) Specifies a contains type match. +* `exact` - (Optional) Specifies an exact type match. +* `prefix` - (Optional) Specifies a prefix type match. Matches the value with the prefix. + +path match (`path_match`) supports the following: + +* `case_sensitive` - (Optional) Indicates whether the match is case sensitive. Defaults to false. +* `match` - (Optional) The header match type. + +path match match (`match`) supports the following: + +* `exact` - (Optional) Specifies an exact type match. +* `prefix` - (Optional) Specifies a prefix type match. Matches the value with the prefix. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the target group. +* `rule_id` - Unique identifier for the target group. +* `tags_all` - 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). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `60m`) +* `update` - (Default `180m`) +* `delete` - (Default `90m`) + +## Import + +VPC Lattice Listener Rule can be imported using the `example_id_arg`, e.g., + +``` +$ terraform import aws_vpclattice_listener_rule.example rft-8012925589 +```