Skip to content

Commit

Permalink
Merge pull request #23754 from DrFaust92/apigw-stage-canary
Browse files Browse the repository at this point in the history
r/apigateway_stage - add `canary_settings` argument
  • Loading branch information
ewbankkit authored Mar 18, 2022
2 parents 006f7c2 + 4319f8c commit ecb0f36
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 10 deletions.
3 changes: 3 additions & 0 deletions .changelog/23754.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/apigateway_stage: Add `canary_settings` argument.
```
146 changes: 142 additions & 4 deletions internal/service/apigateway/stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ func ResourceStage() *schema.Resource {
Optional: true,
ValidateFunc: validation.StringInSlice(apigateway.CacheClusterSize_Values(), true),
},
"canary_settings": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"percent_traffic": {
Type: schema.TypeFloat,
Optional: true,
Default: 0.0,
},
"stage_variable_overrides": {
Type: schema.TypeMap,
Elem: schema.TypeString,
Optional: true,
},
"use_stage_cache": {
Type: schema.TypeBool,
Optional: true,
},
},
},
},
"client_certificate_id": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -133,10 +156,11 @@ func resourceStageCreate(d *schema.ResourceData, meta interface{}) error {

respApiId := d.Get("rest_api_id").(string)
stageName := d.Get("stage_name").(string)
deploymentId := d.Get("deployment_id").(string)
input := &apigateway.CreateStageInput{
RestApiId: aws.String(respApiId),
StageName: aws.String(stageName),
DeploymentId: aws.String(d.Get("deployment_id").(string)),
DeploymentId: aws.String(deploymentId),
}

waitForCache := false
Expand All @@ -161,6 +185,10 @@ func resourceStageCreate(d *schema.ResourceData, meta interface{}) error {
input.TracingEnabled = aws.Bool(v.(bool))
}

if v, ok := d.GetOk("canary_settings"); ok {
input.CanarySettings = expandApiGatewayStageCanarySettings(v.([]interface{}), deploymentId)
}

if len(tags) > 0 {
input.Tags = Tags(tags.IgnoreAWS())
}
Expand Down Expand Up @@ -232,6 +260,10 @@ func resourceStageRead(d *schema.ResourceData, meta interface{}) error {
d.Set("xray_tracing_enabled", stage.TracingEnabled)
d.Set("web_acl_arn", stage.WebAclArn)

if err := d.Set("canary_settings", flattenApiGatewayStageCanarySettings(stage.CanarySettings)); err != nil {
return fmt.Errorf("error setting canary_settings: %w", err)
}

tags := KeyValueTags(stage.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig)

//lintignore:AWSR002
Expand Down Expand Up @@ -314,12 +346,27 @@ func resourceStageUpdate(d *schema.ResourceData, meta interface{}) error {
Value: aws.String(d.Get("client_certificate_id").(string)),
})
}
if d.HasChange("canary_settings") {
oldCanarySettingsRaw, newCanarySettingsRaw := d.GetChange("canary_settings")
operations = appendCanarySettingsPatchOperations(operations,
oldCanarySettingsRaw.([]interface{}),
newCanarySettingsRaw.([]interface{}),
)
}
if d.HasChange("deployment_id") {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String(apigateway.OpReplace),
Path: aws.String("/deploymentId"),
Value: aws.String(d.Get("deployment_id").(string)),
})

if _, ok := d.GetOk("canary_settings"); ok {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String(apigateway.OpReplace),
Path: aws.String("/canarySettings/deploymentId"),
Value: aws.String(d.Get("deployment_id").(string)),
})
}
}
if d.HasChange("description") {
operations = append(operations, &apigateway.PatchOperation{
Expand All @@ -346,7 +393,7 @@ func resourceStageUpdate(d *schema.ResourceData, meta interface{}) error {
o, n := d.GetChange("variables")
oldV := o.(map[string]interface{})
newV := n.(map[string]interface{})
operations = append(operations, diffVariablesOps(oldV, newV)...)
operations = append(operations, diffVariablesOps(oldV, newV, "/variables/")...)
}
if d.HasChange("access_log_settings") {
accessLogSettings := d.Get("access_log_settings").([]interface{})
Expand Down Expand Up @@ -393,9 +440,8 @@ func resourceStageUpdate(d *schema.ResourceData, meta interface{}) error {
return resourceStageRead(d, meta)
}

func diffVariablesOps(oldVars, newVars map[string]interface{}) []*apigateway.PatchOperation {
func diffVariablesOps(oldVars, newVars map[string]interface{}, prefix string) []*apigateway.PatchOperation {
ops := make([]*apigateway.PatchOperation, 0)
prefix := "/variables/"

for k := range oldVars {
if _, ok := newVars[k]; !ok {
Expand Down Expand Up @@ -455,3 +501,95 @@ func flattenApiGatewayStageAccessLogSettings(accessLogSettings *apigateway.Acces
}
return result
}

func expandApiGatewayStageCanarySettings(l []interface{}, deploymentId string) *apigateway.CanarySettings {
if len(l) == 0 {
return nil
}

m := l[0].(map[string]interface{})

canarySettings := &apigateway.CanarySettings{
DeploymentId: aws.String(deploymentId),
}

if v, ok := m["percent_traffic"].(float64); ok {
canarySettings.PercentTraffic = aws.Float64(v)
}

if v, ok := m["use_stage_cache"].(bool); ok {
canarySettings.UseStageCache = aws.Bool(v)
}

if v, ok := m["stage_variable_overrides"].(map[string]interface{}); ok && len(v) > 0 {
canarySettings.StageVariableOverrides = flex.ExpandStringMap(v)
}

return canarySettings
}

func flattenApiGatewayStageCanarySettings(canarySettings *apigateway.CanarySettings) []interface{} {
settings := make(map[string]interface{})

if canarySettings == nil {
return nil
}

overrides := aws.StringValueMap(canarySettings.StageVariableOverrides)

if len(overrides) > 0 {
settings["stage_variable_overrides"] = overrides
}

settings["percent_traffic"] = canarySettings.PercentTraffic
settings["use_stage_cache"] = canarySettings.UseStageCache

return []interface{}{settings}
}

func appendCanarySettingsPatchOperations(operations []*apigateway.PatchOperation, oldCanarySettingsRaw, newCanarySettingsRaw []interface{}) []*apigateway.PatchOperation {
if len(newCanarySettingsRaw) == 0 { // Schema guarantees either 0 or 1
return append(operations, &apigateway.PatchOperation{
Op: aws.String("remove"),
Path: aws.String("/canarySettings"),
})
}
newSettings := newCanarySettingsRaw[0].(map[string]interface{})

var oldSettings map[string]interface{}
if len(oldCanarySettingsRaw) == 1 { // Schema guarantees either 0 or 1
oldSettings = oldCanarySettingsRaw[0].(map[string]interface{})
} else {
oldSettings = map[string]interface{}{
"percent_traffic": 0.0,
"stage_variable_overrides": make(map[string]interface{}),
"use_stage_cache": false,
}
}

oldOverrides := oldSettings["stage_variable_overrides"].(map[string]interface{})
newOverrides := newSettings["stage_variable_overrides"].(map[string]interface{})
operations = append(operations, diffVariablesOps(oldOverrides, newOverrides, "/canarySettings/stageVariableOverrides/")...)

oldPercentTraffic := oldSettings["percent_traffic"].(float64)
newPercentTraffic := newSettings["percent_traffic"].(float64)
if oldPercentTraffic != newPercentTraffic {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String(apigateway.OpReplace),
Path: aws.String("/canarySettings/percentTraffic"),
Value: aws.String(fmt.Sprintf("%f", newPercentTraffic)),
})
}

oldUseStageCache := oldSettings["use_stage_cache"].(bool)
newUseStageCache := newSettings["use_stage_cache"].(bool)
if oldUseStageCache != newUseStageCache {
operations = append(operations, &apigateway.PatchOperation{
Op: aws.String(apigateway.OpReplace),
Path: aws.String("/canarySettings/useStageCache"),
Value: aws.String(fmt.Sprintf("%t", newUseStageCache)),
})
}

return operations
}
95 changes: 95 additions & 0 deletions internal/service/apigateway/stage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestAccAPIGatewayStage_basic(t *testing.T) {
resource.TestCheckResourceAttrSet(resourceName, "invoke_url"),
resource.TestCheckResourceAttr(resourceName, "description", ""),
resource.TestCheckResourceAttr(resourceName, "variables.%", "0"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
resource.TestCheckResourceAttr(resourceName, "xray_tracing_enabled", "false"),
resource.TestCheckResourceAttr(resourceName, "tags.%", "0"),
),
Expand All @@ -55,6 +56,7 @@ func TestAccAPIGatewayStage_basic(t *testing.T) {
resource.TestCheckResourceAttrSet(resourceName, "execution_arn"),
resource.TestCheckResourceAttrSet(resourceName, "invoke_url"),
resource.TestCheckResourceAttr(resourceName, "description", "Hello world"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
resource.TestCheckResourceAttr(resourceName, "variables.%", "2"),
resource.TestCheckResourceAttr(resourceName, "variables.one", "1"),
resource.TestCheckResourceAttr(resourceName, "variables.three", "3"),
Expand All @@ -70,6 +72,7 @@ func TestAccAPIGatewayStage_basic(t *testing.T) {
resource.TestCheckResourceAttrSet(resourceName, "execution_arn"),
resource.TestCheckResourceAttrSet(resourceName, "invoke_url"),
resource.TestCheckResourceAttr(resourceName, "description", ""),
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
resource.TestCheckResourceAttr(resourceName, "variables.%", "0"),
resource.TestCheckResourceAttr(resourceName, "xray_tracing_enabled", "false"),
),
Expand Down Expand Up @@ -468,6 +471,54 @@ func TestAccAPIGatewayStage_waf(t *testing.T) {
})
}

func TestAccAPIGatewayStage_canarySettings(t *testing.T) {
var conf apigateway.Stage
rName := sdkacctest.RandString(5)
resourceName := "aws_api_gateway_stage.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckAPIGatewayTypeEDGE(t) },
ErrorCheck: acctest.ErrorCheck(t, apigateway.EndpointsID),
Providers: acctest.Providers,
CheckDestroy: testAccCheckStageDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSAPIGatewayStageConfig_canarySettings(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckStageExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "variables.one", "1"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.percent_traffic", "33.33"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.stage_variable_overrides.one", "3"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.use_stage_cache", "true"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateIdFunc: testAccStageImportStateIdFunc(resourceName),
ImportStateVerify: true,
},
{
Config: testAccStageConfigBasic(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckStageExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
),
},
{
Config: testAccAWSAPIGatewayStageConfig_canarySettingsUpdated(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckStageExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "variables.one", "1"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.percent_traffic", "66.66"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.stage_variable_overrides.four", "5"),
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.use_stage_cache", "false"),
),
},
},
})
}

func testAccCheckStageExists(n string, res *apigateway.Stage) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
Expand Down Expand Up @@ -830,3 +881,47 @@ resource "aws_wafregional_web_acl_association" "test" {
}
`, rName)
}

func testAccAWSAPIGatewayStageConfig_canarySettings(rName string) string {
return testAccStageConfig_base(rName) + `
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
canary_settings {
percent_traffic = "33.33"
stage_variable_overrides = {
one = "3"
}
use_stage_cache = "true"
}
variables = {
one = "1"
two = "2"
}
}
`
}

func testAccAWSAPIGatewayStageConfig_canarySettingsUpdated(rName string) string {
return testAccStageConfig_base(rName) + `
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
canary_settings {
percent_traffic = "66.66"
stage_variable_overrides = {
four = "5"
}
use_stage_cache = "false"
}
variables = {
one = "1"
two = "2"
}
}
`
}
17 changes: 11 additions & 6 deletions website/docs/r/api_gateway_stage.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ resource "aws_api_gateway_method_settings" "example" {

API Gateway provides the ability to [enable CloudWatch API logging](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html). To manage the CloudWatch Log Group when this feature is enabled, the [`aws_cloudwatch_log_group` resource](/docs/providers/aws/r/cloudwatch_log_group.html) can be used where the name matches the API Gateway naming convention. If the CloudWatch Log Group previously exists, the [`aws_cloudwatch_log_group` resource can be imported into Terraform](/docs/providers/aws/r/cloudwatch_log_group.html#import) as a one time operation and recreation of the environment can occur without import.

-> The below configuration uses [`depends_on`](https://www.terraform.io/docs/configuration/meta-arguments/depends_on.html) to prevent ordering issues with API Gateway automatically creating the log group first and a variable for naming consistency. Other ordering and naming methodologies may be more appropriate for your environment.
-> The below configuration uses [`depends_on`](https://www.terraform.io/language/meta-arguments/depends_on) to prevent ordering issues with API Gateway automatically creating the log group first and a variable for naming consistency. Other ordering and naming methodologies may be more appropriate for your environment.

```terraform
variable "stage_name" {
Expand Down Expand Up @@ -106,24 +106,29 @@ 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
* `access_log_settings` - (Optional) Enables access logs for the API stage. Detailed below.
* `access_log_settings` - (Optional) Enables access logs for the API stage. See [Access Log Settings](#access-log-settings) below.
* `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`.
* `canary_settings` - (Optional) Configuration settings of a canary deployment. See [Canary Settings](#canary-settings) below.
* `client_certificate_id` - (Optional) The identifier of a client certificate for the stage.
* `description` - (Optional) The description of 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
* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level.
* `xray_tracing_enabled` - (Optional) Whether active tracing with X-ray is enabled. Defaults to `false`.

### Nested Blocks

#### `access_log_settings`
### Access Log Settings

* `destination_arn` - (Required) The Amazon Resource Name (ARN) of the CloudWatch Logs log group or Kinesis Data Firehose delivery stream to receive access logs. If you specify a Kinesis Data Firehose delivery stream, the stream name must begin with `amazon-apigateway-`. Automatically removes trailing `:*` if present.
* `format` - (Required) The formatting and values recorded in the logs.
For more information on configuring the log format rules visit the AWS [documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)

### Canary Settings

* `percent_traffic` - (Optional) The percent `0.0` - `100.0` of traffic to divert to the canary deployment.
* `stage_variable_overrides` - (Optional) A map of overridden stage `variables` (including new variables) for the canary deployment.
* `use_stage_cache` - (Optional) Whether the canary deployment uses the stage cache. Defaults to false.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:
Expand Down

0 comments on commit ecb0f36

Please sign in to comment.