diff --git a/builtin/providers/aws/import_aws_api_gateway_usage_plan_test.go b/builtin/providers/aws/import_aws_api_gateway_usage_plan_test.go new file mode 100644 index 000000000000..76a58e0c5d5d --- /dev/null +++ b/builtin/providers/aws/import_aws_api_gateway_usage_plan_test.go @@ -0,0 +1,30 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAWSAPIGatewayUsagePlan_importBasic(t *testing.T) { + resourceName := "aws_api_gateway_usage_plan.main" + rName := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(rName), + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 30c56fa30e8d..8b9be43b9d23 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -219,6 +219,7 @@ func Provider() terraform.ResourceProvider { "aws_api_gateway_model": resourceAwsApiGatewayModel(), "aws_api_gateway_resource": resourceAwsApiGatewayResource(), "aws_api_gateway_rest_api": resourceAwsApiGatewayRestApi(), + "aws_api_gateway_usage_plan": resourceAwsApiGatewayUsagePlan(), "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_appautoscaling_target": resourceAwsAppautoscalingTarget(), "aws_appautoscaling_policy": resourceAwsAppautoscalingPolicy(), diff --git a/builtin/providers/aws/resource_aws_api_gateway_usage_plan.go b/builtin/providers/aws/resource_aws_api_gateway_usage_plan.go new file mode 100644 index 000000000000..0d4930d08c07 --- /dev/null +++ b/builtin/providers/aws/resource_aws_api_gateway_usage_plan.go @@ -0,0 +1,499 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "time" + + "errors" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/apigateway" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsApiGatewayUsagePlan() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsApiGatewayUsagePlanCreate, + Read: resourceAwsApiGatewayUsagePlanRead, + Update: resourceAwsApiGatewayUsagePlanUpdate, + Delete: resourceAwsApiGatewayUsagePlanDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, // Required since not addable nor removable afterwards + }, + + "description": { + Type: schema.TypeString, + Optional: true, + }, + + "api_stages": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "api_id": { + Type: schema.TypeString, + Required: true, + }, + + "stage": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + + "quota_settings": { + Type: schema.TypeSet, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "limit": { + Type: schema.TypeInt, + Required: true, // Required as not removable singularly + }, + + "offset": { + Type: schema.TypeInt, + Default: 0, + Optional: true, + }, + + "period": { + Type: schema.TypeString, + Required: true, // Required as not removable + ValidateFunc: validateApiGatewayUsagePlanQuotaSettingsPeriod, + }, + }, + }, + }, + + "throttle_settings": { + Type: schema.TypeSet, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "burst_limit": { + Type: schema.TypeInt, + Default: 0, + Optional: true, + }, + + "rate_limit": { + Type: schema.TypeInt, + Default: 0, + Optional: true, + }, + }, + }, + }, + + "product_code": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceAwsApiGatewayUsagePlanCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + log.Print("[DEBUG] Creating API Gateway Usage Plan") + + params := &apigateway.CreateUsagePlanInput{ + Name: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("description"); ok { + params.Description = aws.String(v.(string)) + } + + if s, ok := d.GetOk("api_stages"); ok { + stages := s.([]interface{}) + as := make([]*apigateway.ApiStage, 0) + + for _, v := range stages { + sv := v.(map[string]interface{}) + stage := &apigateway.ApiStage{} + + if v, ok := sv["api_id"].(string); ok && v != "" { + stage.ApiId = aws.String(v) + } + + if v, ok := sv["stage"].(string); ok && v != "" { + stage.Stage = aws.String(v) + } + + as = append(as, stage) + } + + if len(as) > 0 { + params.ApiStages = as + } + } + + if v, ok := d.GetOk("quota_settings"); ok { + settings := v.(*schema.Set).List() + q, ok := settings[0].(map[string]interface{}) + + if errors := validateApiGatewayUsagePlanQuotaSettings(q); len(errors) > 0 { + return fmt.Errorf("Error validating the quota settings: %v", errors) + } + + if !ok { + return errors.New("At least one field is expected inside quota_settings") + } + + qs := &apigateway.QuotaSettings{} + + if sv, ok := q["limit"].(int); ok { + qs.Limit = aws.Int64(int64(sv)) + } + + if sv, ok := q["offset"].(int); ok { + qs.Offset = aws.Int64(int64(sv)) + } + + if sv, ok := q["period"].(string); ok && sv != "" { + qs.Period = aws.String(sv) + } + + params.Quota = qs + } + + if v, ok := d.GetOk("throttle_settings"); ok { + settings := v.(*schema.Set).List() + q, ok := settings[0].(map[string]interface{}) + + if !ok { + return errors.New("At least one field is expected inside throttle_settings") + } + + ts := &apigateway.ThrottleSettings{} + + if sv, ok := q["burst_limit"].(int); ok { + ts.BurstLimit = aws.Int64(int64(sv)) + } + + if sv, ok := q["rate_limit"].(float64); ok { + ts.RateLimit = aws.Float64(float64(sv)) + } + + params.Throttle = ts + } + + up, err := conn.CreateUsagePlan(params) + if err != nil { + return fmt.Errorf("Error creating API Gateway Usage Plan: %s", err) + } + + d.SetId(*up.Id) + + // Handle case of adding the product code since not addable when + // creating the Usage Plan initially. + if v, ok := d.GetOk("product_code"); ok { + updateParameters := &apigateway.UpdateUsagePlanInput{ + UsagePlanId: aws.String(d.Id()), + PatchOperations: []*apigateway.PatchOperation{ + { + Op: aws.String("add"), + Path: aws.String("/productCode"), + Value: aws.String(v.(string)), + }, + }, + } + + up, err = conn.UpdateUsagePlan(updateParameters) + if err != nil { + return fmt.Errorf("Error creating the API Gateway Usage Plan product code: %s", err) + } + } + + return resourceAwsApiGatewayUsagePlanRead(d, meta) +} + +func resourceAwsApiGatewayUsagePlanRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + log.Printf("[DEBUG] Reading API Gateway Usage Plan: %s", d.Id()) + + up, err := conn.GetUsagePlan(&apigateway.GetUsagePlanInput{ + UsagePlanId: aws.String(d.Id()), + }) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NotFoundException" { + d.SetId("") + return nil + } + return err + } + + d.Set("name", up.Name) + d.Set("description", up.Description) + d.Set("product_code", up.ProductCode) + + if up.ApiStages != nil { + if err := d.Set("api_stages", flattenApiGatewayUsageApiStages(up.ApiStages)); err != nil { + return fmt.Errorf("[DEBUG] Error setting api_stages error: %#v", err) + } + } + + if up.Throttle != nil { + if err := d.Set("throttle_settings", flattenApiGatewayUsagePlanThrottling(up.Throttle)); err != nil { + return fmt.Errorf("[DEBUG] Error setting throttle_settings error: %#v", err) + } + } + + if up.Quota != nil { + if err := d.Set("quota_settings", flattenApiGatewayUsagePlanQuota(up.Quota)); err != nil { + return fmt.Errorf("[DEBUG] Error setting quota_settings error: %#v", err) + } + } + + return nil +} + +func resourceAwsApiGatewayUsagePlanUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + log.Print("[DEBUG] Updating API Gateway Usage Plan") + + operations := make([]*apigateway.PatchOperation, 0) + + if d.HasChange("name") { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/name"), + Value: aws.String(d.Get("name").(string)), + }) + } + + if d.HasChange("description") { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/description"), + Value: aws.String(d.Get("description").(string)), + }) + } + + if d.HasChange("product_code") { + v, ok := d.GetOk("product_code") + + if ok { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/productCode"), + Value: aws.String(v.(string)), + }) + } else { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("remove"), + Path: aws.String("/productCode"), + }) + } + } + + if d.HasChange("api_stages") { + o, n := d.GetChange("api_stages") + old := o.([]interface{}) + new := n.([]interface{}) + + // Remove every stages associated. Simpler to remove and add new ones, + // since there are no replacings. + for _, v := range old { + m := v.(map[string]interface{}) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("remove"), + Path: aws.String("/apiStages"), + Value: aws.String(fmt.Sprintf("%s:%s", m["api_id"].(string), m["stage"].(string))), + }) + } + + // Handle additions + if len(new) > 0 { + for _, v := range new { + m := v.(map[string]interface{}) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("add"), + Path: aws.String("/apiStages"), + Value: aws.String(fmt.Sprintf("%s:%s", m["api_id"].(string), m["stage"].(string))), + }) + } + } + } + + if d.HasChange("throttle_settings") { + o, n := d.GetChange("throttle_settings") + + os := o.(*schema.Set) + ns := n.(*schema.Set) + diff := ns.Difference(os).List() + + // Handle Removal + if len(diff) == 0 { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("remove"), + Path: aws.String("/throttle"), + }) + } + + if len(diff) > 0 { + d := diff[0].(map[string]interface{}) + + // Handle Replaces + if o != nil && n != nil { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/throttle/rateLimit"), + Value: aws.String(strconv.Itoa(d["rate_limit"].(int))), + }) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/throttle/burstLimit"), + Value: aws.String(strconv.Itoa(d["burst_limit"].(int))), + }) + } + + // Handle Additions + if o == nil && n != nil { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("add"), + Path: aws.String("/throttle/rateLimit"), + Value: aws.String(strconv.Itoa(d["rate_limit"].(int))), + }) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("add"), + Path: aws.String("/throttle/burstLimit"), + Value: aws.String(strconv.Itoa(d["burst_limit"].(int))), + }) + } + } + } + + if d.HasChange("quota_settings") { + o, n := d.GetChange("quota_settings") + + os := o.(*schema.Set) + ns := n.(*schema.Set) + diff := ns.Difference(os).List() + + // Handle Removal + if len(diff) == 0 { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("remove"), + Path: aws.String("/quota"), + }) + } + + if len(diff) > 0 { + d := diff[0].(map[string]interface{}) + + if errors := validateApiGatewayUsagePlanQuotaSettings(d); len(errors) > 0 { + return fmt.Errorf("Error validating the quota settings: %v", errors) + } + + // Handle Replaces + if o != nil && n != nil { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/quota/limit"), + Value: aws.String(strconv.Itoa(d["limit"].(int))), + }) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/quota/offset"), + Value: aws.String(strconv.Itoa(d["offset"].(int))), + }) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/quota/period"), + Value: aws.String(d["period"].(string)), + }) + } + + // Handle Additions + if o == nil && n != nil { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("add"), + Path: aws.String("/quota/limit"), + Value: aws.String(strconv.Itoa(d["limit"].(int))), + }) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("add"), + Path: aws.String("/quota/offset"), + Value: aws.String(strconv.Itoa(d["offset"].(int))), + }) + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("add"), + Path: aws.String("/quota/period"), + Value: aws.String(d["period"].(string)), + }) + } + } + } + + params := &apigateway.UpdateUsagePlanInput{ + UsagePlanId: aws.String(d.Id()), + PatchOperations: operations, + } + + _, err := conn.UpdateUsagePlan(params) + if err != nil { + return fmt.Errorf("Error updating API Gateway Usage Plan: %s", err) + } + + return resourceAwsApiGatewayUsagePlanRead(d, meta) +} + +func resourceAwsApiGatewayUsagePlanDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + + // Removing existing api stages associated + if apistages, ok := d.GetOk("api_stages"); ok { + log.Printf("[DEBUG] Deleting API Stages associated with Usage Plan: %s", d.Id()) + stages := apistages.([]interface{}) + operations := []*apigateway.PatchOperation{} + + for _, v := range stages { + sv := v.(map[string]interface{}) + + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("remove"), + Path: aws.String("/apiStages"), + Value: aws.String(fmt.Sprintf("%s:%s", sv["api_id"].(string), sv["stage"].(string))), + }) + } + + _, err := conn.UpdateUsagePlan(&apigateway.UpdateUsagePlanInput{ + UsagePlanId: aws.String(d.Id()), + PatchOperations: operations, + }) + if err != nil { + return fmt.Errorf("Error removing API Stages associated with Usage Plan: %s", err) + } + } + + log.Printf("[DEBUG] Deleting API Gateway Usage Plan: %s", d.Id()) + + return resource.Retry(5*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteUsagePlan(&apigateway.DeleteUsagePlanInput{ + UsagePlanId: aws.String(d.Id()), + }) + + if err == nil { + return nil + } + + return resource.NonRetryableError(err) + }) +} diff --git a/builtin/providers/aws/resource_aws_api_gateway_usage_plan_test.go b/builtin/providers/aws/resource_aws_api_gateway_usage_plan_test.go new file mode 100644 index 000000000000..13d7afc2dbb6 --- /dev/null +++ b/builtin/providers/aws/resource_aws_api_gateway_usage_plan_test.go @@ -0,0 +1,557 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/apigateway" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSAPIGatewayUsagePlan_basic(t *testing.T) { + var conf apigateway.UsagePlan + name := acctest.RandString(10) + updatedName := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", ""), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanBasicUpdatedConfig(updatedName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", updatedName), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", ""), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", ""), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayUsagePlan_description(t *testing.T) { + var conf apigateway.UsagePlan + name := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", ""), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanDescriptionConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", "This is a description"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanDescriptionUpdatedConfig(name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", "This is a new description"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanDescriptionConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", "This is a description"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "description", ""), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayUsagePlan_productCode(t *testing.T) { + var conf apigateway.UsagePlan + name := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "product_code", ""), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanProductCodeConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "product_code", "MYCODE"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanProductCodeUpdatedConfig(name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "product_code", "MYCODE2"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanProductCodeConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "product_code", "MYCODE"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "product_code", ""), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayUsagePlan_throttling(t *testing.T) { + var conf apigateway.UsagePlan + name := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckNoResourceAttr("aws_api_gateway_usage_plan.main", "throttle_settings"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanThrottlingConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "throttle_settings.4173790118.burst_limit", "2"), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "throttle_settings.4173790118.rate_limit", "5"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanThrottlingModifiedConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "throttle_settings.1779463053.burst_limit", "3"), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "throttle_settings.1779463053.rate_limit", "6"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckNoResourceAttr("aws_api_gateway_usage_plan.main", "throttle_settings"), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayUsagePlan_quota(t *testing.T) { + var conf apigateway.UsagePlan + name := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckNoResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanQuotaConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings.1956747625.limit", "100"), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings.1956747625.offset", "6"), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings.1956747625.period", "WEEK"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanQuotaModifiedConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings.3909168194.limit", "200"), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings.3909168194.offset", "20"), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings.3909168194.period", "MONTH"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckNoResourceAttr("aws_api_gateway_usage_plan.main", "quota_settings"), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayUsagePlan_apiStages(t *testing.T) { + var conf apigateway.UsagePlan + name := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, + Steps: []resource.TestStep{ + // Create UsagePlan WITH Stages as the API calls are different + // when creating or updating. + { + Config: testAccAWSApiGatewayUsagePlanApiStagesConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "api_stages.0.stage", "test"), + ), + }, + // Handle api stages removal + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckNoResourceAttr("aws_api_gateway_usage_plan.main", "api_stages"), + ), + }, + // Handle api stages additions + { + Config: testAccAWSApiGatewayUsagePlanApiStagesConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "api_stages.0.stage", "test"), + ), + }, + // Handle api stages updates + { + Config: testAccAWSApiGatewayUsagePlanApiStagesModifiedConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "api_stages.0.stage", "foo"), + ), + }, + { + Config: testAccAWSApiGatewayUsagePlanBasicConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayUsagePlanExists("aws_api_gateway_usage_plan.main", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_usage_plan.main", "name", name), + resource.TestCheckNoResourceAttr("aws_api_gateway_usage_plan.main", "api_stages"), + ), + }, + }, + }) +} + +func testAccCheckAWSAPIGatewayUsagePlanExists(n string, res *apigateway.UsagePlan) 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 API Gateway Usage Plan ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).apigateway + + req := &apigateway.GetUsagePlanInput{ + UsagePlanId: aws.String(rs.Primary.ID), + } + up, err := conn.GetUsagePlan(req) + if err != nil { + return err + } + + if *up.Id != rs.Primary.ID { + return fmt.Errorf("APIGateway Usage Plan not found") + } + + *res = *up + + return nil + } +} + +func testAccCheckAWSAPIGatewayUsagePlanDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).apigateway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_api_gateway_usage_plan" { + continue + } + + req := &apigateway.GetUsagePlanInput{ + UsagePlanId: aws.String(s.RootModule().Resources["aws_api_gateway_rest_api.test"].Primary.ID), + } + describe, err := conn.GetUsagePlan(req) + + if err == nil { + if describe.Id != nil && *describe.Id == rs.Primary.ID { + return fmt.Errorf("API Gateway Usage Plan still exists") + } + } + + aws2err, ok := err.(awserr.Error) + if !ok { + return err + } + if aws2err.Code() != "NotFoundException" { + return err + } + + return nil + } + + return nil +} + +const testAccAWSAPIGatewayUsagePlanConfig = ` +resource "aws_api_gateway_rest_api" "test" { + name = "test" +} + +resource "aws_api_gateway_resource" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + parent_id = "${aws_api_gateway_rest_api.test.root_resource_id}" + path_part = "test" +} + +resource "aws_api_gateway_method" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_resource.test.id}" + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_method_response" "error" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_resource.test.id}" + http_method = "${aws_api_gateway_method.test.http_method}" + status_code = "400" +} + +resource "aws_api_gateway_integration" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_resource.test.id}" + http_method = "${aws_api_gateway_method.test.http_method}" + + type = "HTTP" + uri = "https://www.google.de" + integration_http_method = "GET" +} + +resource "aws_api_gateway_integration_response" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_resource.test.id}" + http_method = "${aws_api_gateway_integration.test.http_method}" + status_code = "${aws_api_gateway_method_response.error.status_code}" +} + +resource "aws_api_gateway_deployment" "test" { + depends_on = ["aws_api_gateway_integration.test"] + + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + stage_name = "test" + description = "This is a test" + + variables = { + "a" = "2" + } +} + +resource "aws_api_gateway_deployment" "foo" { + depends_on = ["aws_api_gateway_integration.test"] + + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + stage_name = "foo" + description = "This is a prod stage" +} +` + +func testAccAWSApiGatewayUsagePlanBasicConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanDescriptionConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + description = "This is a description" +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanDescriptionUpdatedConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + description = "This is a new description" +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanProductCodeConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + product_code = "MYCODE" +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanProductCodeUpdatedConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + product_code = "MYCODE2" +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanBasicUpdatedConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanThrottlingConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + + throttle_settings { + burst_limit = 2 + rate_limit = 5 + } +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanThrottlingModifiedConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + + throttle_settings { + burst_limit = 3 + rate_limit = 6 + } +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanQuotaConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + + quota_settings { + limit = 100 + offset = 6 + period = "WEEK" + } +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanQuotaModifiedConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + + quota_settings { + limit = 200 + offset = 20 + period = "MONTH" + } +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanApiStagesConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + + api_stages { + api_id = "${aws_api_gateway_rest_api.test.id}" + stage = "${aws_api_gateway_deployment.test.stage_name}" + } +} +`, rName) +} + +func testAccAWSApiGatewayUsagePlanApiStagesModifiedConfig(rName string) string { + return fmt.Sprintf(testAccAWSAPIGatewayUsagePlanConfig+` +resource "aws_api_gateway_usage_plan" "main" { + name = "%s" + + api_stages { + api_id = "${aws_api_gateway_rest_api.test.id}" + stage = "${aws_api_gateway_deployment.foo.stage_name}" + } +} +`, rName) +} diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index 24aec4ed2ba9..302571c6c4ab 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -1849,3 +1849,63 @@ func flattenInspectorTags(cfTags []*cloudformation.Tag) map[string]string { } return tags } + +func flattenApiGatewayUsageApiStages(s []*apigateway.ApiStage) []map[string]interface{} { + stages := make([]map[string]interface{}, 0) + + for _, bd := range s { + if bd.ApiId != nil && bd.Stage != nil { + stage := make(map[string]interface{}) + stage["api_id"] = *bd.ApiId + stage["stage"] = *bd.Stage + + stages = append(stages, stage) + } + } + + if len(stages) > 0 { + return stages + } + + return nil +} + +func flattenApiGatewayUsagePlanThrottling(s *apigateway.ThrottleSettings) []map[string]interface{} { + settings := make(map[string]interface{}, 0) + + if s == nil { + return nil + } + + if s.BurstLimit != nil { + settings["burst_limit"] = *s.BurstLimit + } + + if s.RateLimit != nil { + settings["rate_limit"] = *s.RateLimit + } + + return []map[string]interface{}{settings} +} + +func flattenApiGatewayUsagePlanQuota(s *apigateway.QuotaSettings) []map[string]interface{} { + settings := make(map[string]interface{}, 0) + + if s == nil { + return nil + } + + if s.Limit != nil { + settings["limit"] = *s.Limit + } + + if s.Offset != nil { + settings["offset"] = *s.Offset + } + + if s.Period != nil { + settings["period"] = *s.Period + } + + return []map[string]interface{}{settings} +} diff --git a/builtin/providers/aws/validators.go b/builtin/providers/aws/validators.go index 159192b84006..4990e939698f 100644 --- a/builtin/providers/aws/validators.go +++ b/builtin/providers/aws/validators.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/aws/aws-sdk-go/service/apigateway" "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform/helper/schema" ) @@ -926,7 +927,7 @@ func validateConfigExecutionFrequency(v interface{}, k string) (ws []string, err } } errors = append(errors, fmt.Errorf( - "%q contains an invalid freqency %q. Valid frequencies are %q.", + "%q contains an invalid frequency %q. Valid frequencies are %q.", k, frequency, validFrequencies)) return } @@ -974,3 +975,40 @@ func validateIamRolePolicyNamePrefix(v interface{}, k string) (ws []string, erro } return } + +func validateApiGatewayUsagePlanQuotaSettingsPeriod(v interface{}, k string) (ws []string, errors []error) { + validPeriods := []string{ + apigateway.QuotaPeriodTypeDay, + apigateway.QuotaPeriodTypeWeek, + apigateway.QuotaPeriodTypeMonth, + } + period := v.(string) + for _, f := range validPeriods { + if period == f { + return + } + } + errors = append(errors, fmt.Errorf( + "%q contains an invalid period %q. Valid period are %q.", + k, period, validPeriods)) + return +} + +func validateApiGatewayUsagePlanQuotaSettings(v map[string]interface{}) (errors []error) { + period := v["period"].(string) + offset := v["offset"].(int) + + if period == apigateway.QuotaPeriodTypeDay && offset != 0 { + errors = append(errors, fmt.Errorf("Usage Plan quota offset must be zero in the DAY period")) + } + + if period == apigateway.QuotaPeriodTypeWeek && (offset < 0 || offset > 6) { + errors = append(errors, fmt.Errorf("Usage Plan quota offset must be between 0 and 6 inclusive in the WEEK period")) + } + + if period == apigateway.QuotaPeriodTypeMonth && (offset < 0 || offset > 27) { + errors = append(errors, fmt.Errorf("Usage Plan quota offset must be between 0 and 27 inclusive in the MONTH period")) + } + + return +} diff --git a/builtin/providers/aws/validators_test.go b/builtin/providers/aws/validators_test.go index cefbf0899f64..f717b506089a 100644 --- a/builtin/providers/aws/validators_test.go +++ b/builtin/providers/aws/validators_test.go @@ -1629,3 +1629,106 @@ func TestValidateIamRoleProfileNamePrefix(t *testing.T) { } } } + +func TestValidateApiGatewayUsagePlanQuotaSettingsPeriod(t *testing.T) { + validEntries := []string{ + "DAY", + "WEEK", + "MONTH", + } + + invalidEntries := []string{ + "fooBAR", + "foobar45Baz", + "foobar45Baz@!", + } + + for _, v := range validEntries { + _, errors := validateApiGatewayUsagePlanQuotaSettingsPeriod(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid API Gateway Quota Settings Period: %v", v, errors) + } + } + + for _, v := range invalidEntries { + _, errors := validateApiGatewayUsagePlanQuotaSettingsPeriod(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should not be a API Gateway Quota Settings Period", v) + } + } +} + +func TestValidateApiGatewayUsagePlanQuotaSettings(t *testing.T) { + cases := []struct { + Offset int + Period string + ErrCount int + }{ + { + Offset: 0, + Period: "DAY", + ErrCount: 0, + }, + { + Offset: -1, + Period: "DAY", + ErrCount: 1, + }, + { + Offset: 1, + Period: "DAY", + ErrCount: 1, + }, + { + Offset: 0, + Period: "WEEK", + ErrCount: 0, + }, + { + Offset: 6, + Period: "WEEK", + ErrCount: 0, + }, + { + Offset: -1, + Period: "WEEK", + ErrCount: 1, + }, + { + Offset: 7, + Period: "WEEK", + ErrCount: 1, + }, + { + Offset: 0, + Period: "MONTH", + ErrCount: 0, + }, + { + Offset: 27, + Period: "MONTH", + ErrCount: 0, + }, + { + Offset: -1, + Period: "MONTH", + ErrCount: 1, + }, + { + Offset: 28, + Period: "MONTH", + ErrCount: 1, + }, + } + + for _, tc := range cases { + m := make(map[string]interface{}) + m["offset"] = tc.Offset + m["period"] = tc.Period + + errors := validateApiGatewayUsagePlanQuotaSettings(m) + if len(errors) != tc.ErrCount { + t.Fatalf("API Gateway Usage Plan Quota Settings validation failed: %v", errors) + } + } +} diff --git a/website/source/docs/providers/aws/r/api_gateway_usage_plan.html.markdown b/website/source/docs/providers/aws/r/api_gateway_usage_plan.html.markdown new file mode 100644 index 000000000000..70f75ca3a2b2 --- /dev/null +++ b/website/source/docs/providers/aws/r/api_gateway_usage_plan.html.markdown @@ -0,0 +1,99 @@ +--- +layout: "aws" +page_title: "AWS: aws_api_gateway_usage_plan" +sidebar_current: "docs-aws-resource-api-gateway-usage-plan" +description: |- + Provides an API Gateway Usage Plan. +--- + +# aws\_api\_usage\_plan + +Provides an API Gateway Usage Plan. + +## Example Usage + +``` +resource "aws_api_gateway_rest_api" "myapi" { + name = "MyDemoAPI" +} + +... + +resource "aws_api_gateway_deployment" "dev" { + rest_api_id = "${aws_api_gateway_rest_api.myapi.id}" + stage_name = "dev" +} + +resource "aws_api_gateway_deployment" "prod" { + rest_api_id = "${aws_api_gateway_rest_api.myapi.id}" + stage_name = "prod" +} + +resource "aws_api_gateway_usage_plan" "MyUsagePlan" { + name = "my-usage-plan" + description = "my description" + product_code = "MYCODE" + + api_stages { + api_id = "${aws_api_gateway_rest_api.myapi.id}" + stage = "${aws_api_gateway_deployment.dev.stage_name}" + } + + api_stages { + api_id = "${aws_api_gateway_rest_api.myapi.id}" + stage = "${aws_api_gateway_deployment.prod.stage_name}" + } + + quota_settings { + limit = 20 + offset = 2 + period = "WEEK" + } + + throttle_settings { + burst_limit = 5 + rate_limit = 10 + } +} +``` + +## Argument Reference + +The API Gateway Usage Plan argument layout is a structure composed of several sub-resources - these resources are laid out below. + +### Top-Level Arguments + +* `name` - (Required) The name of the usage plan. +* `description` - (Required) The description of a usage plan. +* `api_stages` - (Optional) The associated [API stages](#api-stages-arguments) of the usage plan. +* `quota_settings` - (Optional) The [quota settings](#quota-settings-arguments) of the usage plan. +* `throttle_settings` - (Optional) The [throttling limits](#throttling-settings-arguments) of the usage plan. +* `product_code` - (Optional) The AWS Markeplace product identifier to associate with the usage plan as a SaaS product on AWS Marketplace. + +#### Api Stages arguments + + * `api_id` (Optional) - API Id of the associated API stage in a usage plan. + * `stage` (Optional) - API stage name of the associated API stage in a usage plan. + +#### Quota Settings Arguments + + * `limit` (Optional) - The maximum number of requests that can be made in a given time period. + * `offset` (Optional) - The number of requests subtracted from the given limit in the initial time period. + * `period` (Optional) - The time period in which the limit applies. Valid values are "DAY", "WEEK" or "MONTH". + +#### Throttling Settings Arguments + + * `burst_limit` (Optional) - The API request burst limit, the maximum rate limit over a time ranging from one to a few seconds, depending upon whether the underlying token bucket is at its full capacity. + * `rate_limit` (Optional) - The API request steady-state rate limit. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the API resource +* `name` - The name of the usage plan. +* `description` - The description of a usage plan. +* `api_stages` - The associated API stages of the usage plan. +* `quota_settings` - The quota of the usage plan. +* `throttle_settings` - The throttling limits of the usage plan. +* `product_code` - The AWS Markeplace product identifier to associate with the usage plan as a SaaS product on AWS Marketplace. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 105d4a447cf1..8bcced9aa22e 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -179,6 +179,9 @@ > aws_api_gateway_rest_api + > + aws_api_gateway_usage_plan +