diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 67e813a9e..9a2f7e5ff 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -221,6 +221,7 @@ func Provider() terraform.ResourceProvider { "aws_api_gateway_model": resourceAwsApiGatewayModel(), "aws_api_gateway_resource": resourceAwsApiGatewayResource(), "aws_api_gateway_rest_api": resourceAwsApiGatewayRestApi(), + "aws_api_gateway_stage": resourceAwsApiGatewayStage(), "aws_api_gateway_usage_plan": resourceAwsApiGatewayUsagePlan(), "aws_api_gateway_usage_plan_key": resourceAwsApiGatewayUsagePlanKey(), "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), diff --git a/builtin/providers/aws/resource_aws_api_gateway_stage.go b/builtin/providers/aws/resource_aws_api_gateway_stage.go new file mode 100644 index 000000000..1b8579e3d --- /dev/null +++ b/builtin/providers/aws/resource_aws_api_gateway_stage.go @@ -0,0 +1,342 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "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 resourceAwsApiGatewayStage() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsApiGatewayStageCreate, + Read: resourceAwsApiGatewayStageRead, + Update: resourceAwsApiGatewayStageUpdate, + Delete: resourceAwsApiGatewayStageDelete, + + Schema: map[string]*schema.Schema{ + "cache_cluster_enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "cache_cluster_size": { + Type: schema.TypeString, + Optional: true, + }, + "client_certificate_id": { + Type: schema.TypeString, + Optional: true, + }, + "deployment_id": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "documentation_version": { + Type: schema.TypeString, + Optional: true, + }, + "rest_api_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "stage_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "variables": { + Type: schema.TypeMap, + Optional: true, + }, + }, + } +} + +func resourceAwsApiGatewayStageCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + + d.Partial(true) + + input := apigateway.CreateStageInput{ + RestApiId: aws.String(d.Get("rest_api_id").(string)), + StageName: aws.String(d.Get("stage_name").(string)), + DeploymentId: aws.String(d.Get("deployment_id").(string)), + } + + waitForCache := false + if v, ok := d.GetOk("cache_cluster_enabled"); ok { + input.CacheClusterEnabled = aws.Bool(v.(bool)) + waitForCache = true + } + if v, ok := d.GetOk("cache_cluster_size"); ok { + input.CacheClusterSize = aws.String(v.(string)) + waitForCache = true + } + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + if v, ok := d.GetOk("documentation_version"); ok { + input.DocumentationVersion = aws.String(v.(string)) + } + if vars, ok := d.GetOk("variables"); ok { + variables := make(map[string]string, 0) + for k, v := range vars.(map[string]interface{}) { + variables[k] = v.(string) + } + input.Variables = aws.StringMap(variables) + } + + out, err := conn.CreateStage(&input) + if err != nil { + return fmt.Errorf("Error creating API Gateway Stage: %s", err) + } + + d.SetId(fmt.Sprintf("ags-%s-%s", d.Get("rest_api_id").(string), d.Get("stage_name").(string))) + + d.SetPartial("rest_api_id") + d.SetPartial("stage_name") + d.SetPartial("deployment_id") + d.SetPartial("description") + d.SetPartial("variables") + + if waitForCache && *out.CacheClusterStatus != "NOT_AVAILABLE" { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "CREATE_IN_PROGRESS", + "DELETE_IN_PROGRESS", + "FLUSH_IN_PROGRESS", + }, + Target: []string{"AVAILABLE"}, + Refresh: apiGatewayStageCacheRefreshFunc(conn, + d.Get("rest_api_id").(string), + d.Get("stage_name").(string)), + Timeout: 90 * time.Minute, + } + + _, err := stateConf.WaitForState() + if err != nil { + return err + } + } + + d.SetPartial("cache_cluster_enabled") + d.SetPartial("cache_cluster_size") + d.Partial(false) + + if _, ok := d.GetOk("client_certificate_id"); ok { + return resourceAwsApiGatewayStageUpdate(d, meta) + } + return resourceAwsApiGatewayStageRead(d, meta) +} + +func resourceAwsApiGatewayStageRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + + log.Printf("[DEBUG] Reading API Gateway Stage %s", d.Id()) + input := apigateway.GetStageInput{ + RestApiId: aws.String(d.Get("rest_api_id").(string)), + StageName: aws.String(d.Get("stage_name").(string)), + } + stage, err := conn.GetStage(&input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NotFoundException" { + log.Printf("[WARN] API Gateway Stage %s not found, removing", d.Id()) + d.SetId("") + return nil + } + return err + } + log.Printf("[DEBUG] Received API Gateway Stage: %s", stage) + + d.Set("client_certificate_id", stage.ClientCertificateId) + + if stage.CacheClusterStatus != nil && *stage.CacheClusterStatus == "DELETE_IN_PROGRESS" { + d.Set("cache_cluster_enabled", false) + d.Set("cache_cluster_size", nil) + } else { + d.Set("cache_cluster_enabled", stage.CacheClusterEnabled) + d.Set("cache_cluster_size", stage.CacheClusterSize) + } + + d.Set("deployment_id", stage.DeploymentId) + d.Set("description", stage.Description) + d.Set("documentation_version", stage.DocumentationVersion) + d.Set("variables", aws.StringValueMap(stage.Variables)) + + return nil +} + +func resourceAwsApiGatewayStageUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + + d.Partial(true) + operations := make([]*apigateway.PatchOperation, 0) + waitForCache := false + if d.HasChange("cache_cluster_enabled") { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/cacheClusterEnabled"), + Value: aws.String(fmt.Sprintf("%t", d.Get("cache_cluster_enabled").(bool))), + }) + waitForCache = true + } + if d.HasChange("cache_cluster_size") { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/cacheClusterSize"), + Value: aws.String(d.Get("cache_cluster_size").(string)), + }) + waitForCache = true + } + if d.HasChange("client_certificate_id") { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/clientCertificateId"), + Value: aws.String(d.Get("client_certificate_id").(string)), + }) + } + if d.HasChange("deployment_id") { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/deploymentId"), + Value: aws.String(d.Get("deployment_id").(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("documentation_version") { + operations = append(operations, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String("/documentationVersion"), + Value: aws.String(d.Get("documentation_version").(string)), + }) + } + if d.HasChange("variables") { + o, n := d.GetChange("variables") + oldV := o.(map[string]interface{}) + newV := n.(map[string]interface{}) + operations = append(operations, diffVariablesOps("/variables/", oldV, newV)...) + } + + input := apigateway.UpdateStageInput{ + RestApiId: aws.String(d.Get("rest_api_id").(string)), + StageName: aws.String(d.Get("stage_name").(string)), + PatchOperations: operations, + } + log.Printf("[DEBUG] Updating API Gateway Stage: %s", input) + out, err := conn.UpdateStage(&input) + if err != nil { + return fmt.Errorf("Updating API Gateway Stage failed: %s", err) + } + + d.SetPartial("client_certificate_id") + d.SetPartial("deployment_id") + d.SetPartial("description") + d.SetPartial("variables") + + if waitForCache && *out.CacheClusterStatus != "NOT_AVAILABLE" { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "CREATE_IN_PROGRESS", + "FLUSH_IN_PROGRESS", + }, + Target: []string{ + "AVAILABLE", + // There's an AWS API bug (raised & confirmed in Sep 2016 by support) + // which causes the stage to remain in deletion state forever + "DELETE_IN_PROGRESS", + }, + Refresh: apiGatewayStageCacheRefreshFunc(conn, + d.Get("rest_api_id").(string), + d.Get("stage_name").(string)), + Timeout: 30 * time.Minute, + } + + _, err := stateConf.WaitForState() + if err != nil { + return err + } + } + + d.SetPartial("cache_cluster_enabled") + d.SetPartial("cache_cluster_size") + d.Partial(false) + + return resourceAwsApiGatewayStageRead(d, meta) +} + +func diffVariablesOps(prefix string, oldVars, newVars map[string]interface{}) []*apigateway.PatchOperation { + ops := make([]*apigateway.PatchOperation, 0) + + for k, _ := range oldVars { + if _, ok := newVars[k]; !ok { + ops = append(ops, &apigateway.PatchOperation{ + Op: aws.String("remove"), + Path: aws.String(prefix + k), + }) + } + } + + for k, v := range newVars { + newValue := v.(string) + + if oldV, ok := oldVars[k]; ok { + oldValue := oldV.(string) + if oldValue == newValue { + continue + } + } + ops = append(ops, &apigateway.PatchOperation{ + Op: aws.String("replace"), + Path: aws.String(prefix + k), + Value: aws.String(newValue), + }) + } + + return ops +} + +func apiGatewayStageCacheRefreshFunc(conn *apigateway.APIGateway, apiId, stageName string) func() (interface{}, string, error) { + return func() (interface{}, string, error) { + input := apigateway.GetStageInput{ + RestApiId: aws.String(apiId), + StageName: aws.String(stageName), + } + out, err := conn.GetStage(&input) + if err != nil { + return 42, "", err + } + + return out, *out.CacheClusterStatus, nil + } +} + +func resourceAwsApiGatewayStageDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigateway + log.Printf("[DEBUG] Deleting API Gateway Stage: %s", d.Id()) + input := apigateway.DeleteStageInput{ + RestApiId: aws.String(d.Get("rest_api_id").(string)), + StageName: aws.String(d.Get("stage_name").(string)), + } + _, err := conn.DeleteStage(&input) + if err != nil { + return fmt.Errorf("Deleting API Gateway Stage failed: %s", err) + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_api_gateway_stage_test.go b/builtin/providers/aws/resource_aws_api_gateway_stage_test.go new file mode 100644 index 000000000..c64ac1c6d --- /dev/null +++ b/builtin/providers/aws/resource_aws_api_gateway_stage_test.go @@ -0,0 +1,196 @@ +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/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSAPIGatewayStage_basic(t *testing.T) { + var conf apigateway.Stage + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayStageDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAPIGatewayStageConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayStageExists("aws_api_gateway_stage.test", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "stage_name", "prod"), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "cache_cluster_enabled", "true"), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "cache_cluster_size", "0.5"), + ), + }, + resource.TestStep{ + Config: testAccAWSAPIGatewayStageConfig_updated(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayStageExists("aws_api_gateway_stage.test", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "stage_name", "prod"), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "cache_cluster_enabled", "false"), + ), + }, + resource.TestStep{ + Config: testAccAWSAPIGatewayStageConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayStageExists("aws_api_gateway_stage.test", &conf), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "stage_name", "prod"), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "cache_cluster_enabled", "true"), + resource.TestCheckResourceAttr("aws_api_gateway_stage.test", "cache_cluster_size", "0.5"), + ), + }, + }, + }) +} + +func testAccCheckAWSAPIGatewayStageExists(n string, res *apigateway.Stage) 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 Stage ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).apigateway + + req := &apigateway.GetStageInput{ + RestApiId: aws.String(s.RootModule().Resources["aws_api_gateway_rest_api.test"].Primary.ID), + StageName: aws.String(rs.Primary.Attributes["stage_name"]), + } + out, err := conn.GetStage(req) + if err != nil { + return err + } + + *res = *out + + return nil + } +} + +func testAccCheckAWSAPIGatewayStageDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).apigateway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_api_gateway_stage" { + continue + } + + req := &apigateway.GetStageInput{ + RestApiId: aws.String(s.RootModule().Resources["aws_api_gateway_rest_api.test"].Primary.ID), + StageName: aws.String(rs.Primary.Attributes["stage_name"]), + } + out, err := conn.GetStage(req) + if err == nil { + return fmt.Errorf("API Gateway Stage still exists: %s", out) + } + + awsErr, ok := err.(awserr.Error) + if !ok { + return err + } + if awsErr.Code() != "NotFoundException" { + return err + } + + return nil + } + + return nil +} + +const testAccAWSAPIGatewayStageConfig_base = ` +resource "aws_api_gateway_rest_api" "test" { + name = "tf-acc-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.co.uk" + 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" "dev" { + depends_on = ["aws_api_gateway_integration.test"] + + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + stage_name = "dev" + description = "This is a dev env" + + variables = { + "a" = "2" + } +} +` + +func testAccAWSAPIGatewayStageConfig_basic() string { + return testAccAWSAPIGatewayStageConfig_base + ` +resource "aws_api_gateway_stage" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + stage_name = "prod" + deployment_id = "${aws_api_gateway_deployment.dev.id}" + cache_cluster_enabled = true + cache_cluster_size = "0.5" + variables { + one = "1" + two = "2" + } +} +` +} + +func testAccAWSAPIGatewayStageConfig_updated() string { + return testAccAWSAPIGatewayStageConfig_base + ` +resource "aws_api_gateway_stage" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + stage_name = "prod" + deployment_id = "${aws_api_gateway_deployment.dev.id}" + cache_cluster_enabled = false + description = "Hello world" + variables { + one = "1" + three = "3" + } +} +` +} diff --git a/website/source/docs/providers/aws/r/api_gateway_stage.html.markdown b/website/source/docs/providers/aws/r/api_gateway_stage.html.markdown new file mode 100644 index 000000000..155837a2b --- /dev/null +++ b/website/source/docs/providers/aws/r/api_gateway_stage.html.markdown @@ -0,0 +1,78 @@ +--- +layout: "aws" +page_title: "AWS: aws_api_gateway_stage" +sidebar_current: "docs-aws-resource-api-gateway-stage" +description: |- + Provides an API Gateway Stage. +--- + +# aws\_api\_gateway\_stage + +Provides an API Gateway Stage. + +## Example Usage + +```hcl +resource "aws_api_gateway_stage" "test" { + stage_name = "prod" + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + deployment_id = "${aws_api_gateway_deployment.test.id}" +} + +resource "aws_api_gateway_rest_api" "test" { + name = "MyDemoAPI" + description = "This is my API for demonstration purposes" +} + +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 = "dev" +} + +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 = "mytestresource" +} + +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_settings" "s" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + stage_name = "${aws_api_gateway_stage.test.stage_name}" + method_path = "${aws_api_gateway_resource.test.path_part}/${aws_api_gateway_method.test.http_method}" + + settings { + metrics_enabled = true + logging_level = "INFO" + } +} + +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 = "MOCK" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `rest_api_id` - (Required) The ID of the associated REST API +* `stage_name` - (Required) The name of the stage +* `deployment_id` - (Required) The ID of the deployment that the stage points to +* `cache_cluster_enabled` - (Optional) Specifies whether a cache cluster is enabled for the stage +* `cache_cluster_size` - (Optional) The size of the cache cluster for the stage, if enabled. + Allowed values include `0.5`, `1.6`, `6.1`, `13.5`, `28.4`, `58.2`, `118` and `237`. +* `client_certificate_id` - (Optional) The identifier of a client certificate for the stage. +* `description` - (Optional) The description of the stage +* `documentation_version` - (Optional) The version of the associated API documentation +* `variables` - (Optional) A map that defines the stage variables diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 716a53411..dba72163b 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -185,6 +185,9 @@ > aws_api_gateway_rest_api + > + aws_api_gateway_stage + > aws_api_gateway_usage_plan