Skip to content

Commit ecb0f36

Browse files
authored
Merge pull request #23754 from DrFaust92/apigw-stage-canary
r/apigateway_stage - add `canary_settings` argument
2 parents 006f7c2 + 4319f8c commit ecb0f36

File tree

4 files changed

+251
-10
lines changed

4 files changed

+251
-10
lines changed

.changelog/23754.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/apigateway_stage: Add `canary_settings` argument.
3+
```

internal/service/apigateway/stage.go

+142-4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,29 @@ func ResourceStage() *schema.Resource {
6767
Optional: true,
6868
ValidateFunc: validation.StringInSlice(apigateway.CacheClusterSize_Values(), true),
6969
},
70+
"canary_settings": {
71+
Type: schema.TypeList,
72+
Optional: true,
73+
MaxItems: 1,
74+
Elem: &schema.Resource{
75+
Schema: map[string]*schema.Schema{
76+
"percent_traffic": {
77+
Type: schema.TypeFloat,
78+
Optional: true,
79+
Default: 0.0,
80+
},
81+
"stage_variable_overrides": {
82+
Type: schema.TypeMap,
83+
Elem: schema.TypeString,
84+
Optional: true,
85+
},
86+
"use_stage_cache": {
87+
Type: schema.TypeBool,
88+
Optional: true,
89+
},
90+
},
91+
},
92+
},
7093
"client_certificate_id": {
7194
Type: schema.TypeString,
7295
Optional: true,
@@ -133,10 +156,11 @@ func resourceStageCreate(d *schema.ResourceData, meta interface{}) error {
133156

134157
respApiId := d.Get("rest_api_id").(string)
135158
stageName := d.Get("stage_name").(string)
159+
deploymentId := d.Get("deployment_id").(string)
136160
input := &apigateway.CreateStageInput{
137161
RestApiId: aws.String(respApiId),
138162
StageName: aws.String(stageName),
139-
DeploymentId: aws.String(d.Get("deployment_id").(string)),
163+
DeploymentId: aws.String(deploymentId),
140164
}
141165

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

188+
if v, ok := d.GetOk("canary_settings"); ok {
189+
input.CanarySettings = expandApiGatewayStageCanarySettings(v.([]interface{}), deploymentId)
190+
}
191+
164192
if len(tags) > 0 {
165193
input.Tags = Tags(tags.IgnoreAWS())
166194
}
@@ -232,6 +260,10 @@ func resourceStageRead(d *schema.ResourceData, meta interface{}) error {
232260
d.Set("xray_tracing_enabled", stage.TracingEnabled)
233261
d.Set("web_acl_arn", stage.WebAclArn)
234262

263+
if err := d.Set("canary_settings", flattenApiGatewayStageCanarySettings(stage.CanarySettings)); err != nil {
264+
return fmt.Errorf("error setting canary_settings: %w", err)
265+
}
266+
235267
tags := KeyValueTags(stage.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig)
236268

237269
//lintignore:AWSR002
@@ -314,12 +346,27 @@ func resourceStageUpdate(d *schema.ResourceData, meta interface{}) error {
314346
Value: aws.String(d.Get("client_certificate_id").(string)),
315347
})
316348
}
349+
if d.HasChange("canary_settings") {
350+
oldCanarySettingsRaw, newCanarySettingsRaw := d.GetChange("canary_settings")
351+
operations = appendCanarySettingsPatchOperations(operations,
352+
oldCanarySettingsRaw.([]interface{}),
353+
newCanarySettingsRaw.([]interface{}),
354+
)
355+
}
317356
if d.HasChange("deployment_id") {
318357
operations = append(operations, &apigateway.PatchOperation{
319358
Op: aws.String(apigateway.OpReplace),
320359
Path: aws.String("/deploymentId"),
321360
Value: aws.String(d.Get("deployment_id").(string)),
322361
})
362+
363+
if _, ok := d.GetOk("canary_settings"); ok {
364+
operations = append(operations, &apigateway.PatchOperation{
365+
Op: aws.String(apigateway.OpReplace),
366+
Path: aws.String("/canarySettings/deploymentId"),
367+
Value: aws.String(d.Get("deployment_id").(string)),
368+
})
369+
}
323370
}
324371
if d.HasChange("description") {
325372
operations = append(operations, &apigateway.PatchOperation{
@@ -346,7 +393,7 @@ func resourceStageUpdate(d *schema.ResourceData, meta interface{}) error {
346393
o, n := d.GetChange("variables")
347394
oldV := o.(map[string]interface{})
348395
newV := n.(map[string]interface{})
349-
operations = append(operations, diffVariablesOps(oldV, newV)...)
396+
operations = append(operations, diffVariablesOps(oldV, newV, "/variables/")...)
350397
}
351398
if d.HasChange("access_log_settings") {
352399
accessLogSettings := d.Get("access_log_settings").([]interface{})
@@ -393,9 +440,8 @@ func resourceStageUpdate(d *schema.ResourceData, meta interface{}) error {
393440
return resourceStageRead(d, meta)
394441
}
395442

396-
func diffVariablesOps(oldVars, newVars map[string]interface{}) []*apigateway.PatchOperation {
443+
func diffVariablesOps(oldVars, newVars map[string]interface{}, prefix string) []*apigateway.PatchOperation {
397444
ops := make([]*apigateway.PatchOperation, 0)
398-
prefix := "/variables/"
399445

400446
for k := range oldVars {
401447
if _, ok := newVars[k]; !ok {
@@ -455,3 +501,95 @@ func flattenApiGatewayStageAccessLogSettings(accessLogSettings *apigateway.Acces
455501
}
456502
return result
457503
}
504+
505+
func expandApiGatewayStageCanarySettings(l []interface{}, deploymentId string) *apigateway.CanarySettings {
506+
if len(l) == 0 {
507+
return nil
508+
}
509+
510+
m := l[0].(map[string]interface{})
511+
512+
canarySettings := &apigateway.CanarySettings{
513+
DeploymentId: aws.String(deploymentId),
514+
}
515+
516+
if v, ok := m["percent_traffic"].(float64); ok {
517+
canarySettings.PercentTraffic = aws.Float64(v)
518+
}
519+
520+
if v, ok := m["use_stage_cache"].(bool); ok {
521+
canarySettings.UseStageCache = aws.Bool(v)
522+
}
523+
524+
if v, ok := m["stage_variable_overrides"].(map[string]interface{}); ok && len(v) > 0 {
525+
canarySettings.StageVariableOverrides = flex.ExpandStringMap(v)
526+
}
527+
528+
return canarySettings
529+
}
530+
531+
func flattenApiGatewayStageCanarySettings(canarySettings *apigateway.CanarySettings) []interface{} {
532+
settings := make(map[string]interface{})
533+
534+
if canarySettings == nil {
535+
return nil
536+
}
537+
538+
overrides := aws.StringValueMap(canarySettings.StageVariableOverrides)
539+
540+
if len(overrides) > 0 {
541+
settings["stage_variable_overrides"] = overrides
542+
}
543+
544+
settings["percent_traffic"] = canarySettings.PercentTraffic
545+
settings["use_stage_cache"] = canarySettings.UseStageCache
546+
547+
return []interface{}{settings}
548+
}
549+
550+
func appendCanarySettingsPatchOperations(operations []*apigateway.PatchOperation, oldCanarySettingsRaw, newCanarySettingsRaw []interface{}) []*apigateway.PatchOperation {
551+
if len(newCanarySettingsRaw) == 0 { // Schema guarantees either 0 or 1
552+
return append(operations, &apigateway.PatchOperation{
553+
Op: aws.String("remove"),
554+
Path: aws.String("/canarySettings"),
555+
})
556+
}
557+
newSettings := newCanarySettingsRaw[0].(map[string]interface{})
558+
559+
var oldSettings map[string]interface{}
560+
if len(oldCanarySettingsRaw) == 1 { // Schema guarantees either 0 or 1
561+
oldSettings = oldCanarySettingsRaw[0].(map[string]interface{})
562+
} else {
563+
oldSettings = map[string]interface{}{
564+
"percent_traffic": 0.0,
565+
"stage_variable_overrides": make(map[string]interface{}),
566+
"use_stage_cache": false,
567+
}
568+
}
569+
570+
oldOverrides := oldSettings["stage_variable_overrides"].(map[string]interface{})
571+
newOverrides := newSettings["stage_variable_overrides"].(map[string]interface{})
572+
operations = append(operations, diffVariablesOps(oldOverrides, newOverrides, "/canarySettings/stageVariableOverrides/")...)
573+
574+
oldPercentTraffic := oldSettings["percent_traffic"].(float64)
575+
newPercentTraffic := newSettings["percent_traffic"].(float64)
576+
if oldPercentTraffic != newPercentTraffic {
577+
operations = append(operations, &apigateway.PatchOperation{
578+
Op: aws.String(apigateway.OpReplace),
579+
Path: aws.String("/canarySettings/percentTraffic"),
580+
Value: aws.String(fmt.Sprintf("%f", newPercentTraffic)),
581+
})
582+
}
583+
584+
oldUseStageCache := oldSettings["use_stage_cache"].(bool)
585+
newUseStageCache := newSettings["use_stage_cache"].(bool)
586+
if oldUseStageCache != newUseStageCache {
587+
operations = append(operations, &apigateway.PatchOperation{
588+
Op: aws.String(apigateway.OpReplace),
589+
Path: aws.String("/canarySettings/useStageCache"),
590+
Value: aws.String(fmt.Sprintf("%t", newUseStageCache)),
591+
})
592+
}
593+
594+
return operations
595+
}

internal/service/apigateway/stage_test.go

+95
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func TestAccAPIGatewayStage_basic(t *testing.T) {
3636
resource.TestCheckResourceAttrSet(resourceName, "invoke_url"),
3737
resource.TestCheckResourceAttr(resourceName, "description", ""),
3838
resource.TestCheckResourceAttr(resourceName, "variables.%", "0"),
39+
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
3940
resource.TestCheckResourceAttr(resourceName, "xray_tracing_enabled", "false"),
4041
resource.TestCheckResourceAttr(resourceName, "tags.%", "0"),
4142
),
@@ -55,6 +56,7 @@ func TestAccAPIGatewayStage_basic(t *testing.T) {
5556
resource.TestCheckResourceAttrSet(resourceName, "execution_arn"),
5657
resource.TestCheckResourceAttrSet(resourceName, "invoke_url"),
5758
resource.TestCheckResourceAttr(resourceName, "description", "Hello world"),
59+
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
5860
resource.TestCheckResourceAttr(resourceName, "variables.%", "2"),
5961
resource.TestCheckResourceAttr(resourceName, "variables.one", "1"),
6062
resource.TestCheckResourceAttr(resourceName, "variables.three", "3"),
@@ -70,6 +72,7 @@ func TestAccAPIGatewayStage_basic(t *testing.T) {
7072
resource.TestCheckResourceAttrSet(resourceName, "execution_arn"),
7173
resource.TestCheckResourceAttrSet(resourceName, "invoke_url"),
7274
resource.TestCheckResourceAttr(resourceName, "description", ""),
75+
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
7376
resource.TestCheckResourceAttr(resourceName, "variables.%", "0"),
7477
resource.TestCheckResourceAttr(resourceName, "xray_tracing_enabled", "false"),
7578
),
@@ -468,6 +471,54 @@ func TestAccAPIGatewayStage_waf(t *testing.T) {
468471
})
469472
}
470473

474+
func TestAccAPIGatewayStage_canarySettings(t *testing.T) {
475+
var conf apigateway.Stage
476+
rName := sdkacctest.RandString(5)
477+
resourceName := "aws_api_gateway_stage.test"
478+
479+
resource.ParallelTest(t, resource.TestCase{
480+
PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckAPIGatewayTypeEDGE(t) },
481+
ErrorCheck: acctest.ErrorCheck(t, apigateway.EndpointsID),
482+
Providers: acctest.Providers,
483+
CheckDestroy: testAccCheckStageDestroy,
484+
Steps: []resource.TestStep{
485+
{
486+
Config: testAccAWSAPIGatewayStageConfig_canarySettings(rName),
487+
Check: resource.ComposeTestCheckFunc(
488+
testAccCheckStageExists(resourceName, &conf),
489+
resource.TestCheckResourceAttr(resourceName, "variables.one", "1"),
490+
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.percent_traffic", "33.33"),
491+
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.stage_variable_overrides.one", "3"),
492+
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.use_stage_cache", "true"),
493+
),
494+
},
495+
{
496+
ResourceName: resourceName,
497+
ImportState: true,
498+
ImportStateIdFunc: testAccStageImportStateIdFunc(resourceName),
499+
ImportStateVerify: true,
500+
},
501+
{
502+
Config: testAccStageConfigBasic(rName),
503+
Check: resource.ComposeTestCheckFunc(
504+
testAccCheckStageExists(resourceName, &conf),
505+
resource.TestCheckResourceAttr(resourceName, "canary_settings.#", "0"),
506+
),
507+
},
508+
{
509+
Config: testAccAWSAPIGatewayStageConfig_canarySettingsUpdated(rName),
510+
Check: resource.ComposeTestCheckFunc(
511+
testAccCheckStageExists(resourceName, &conf),
512+
resource.TestCheckResourceAttr(resourceName, "variables.one", "1"),
513+
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.percent_traffic", "66.66"),
514+
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.stage_variable_overrides.four", "5"),
515+
resource.TestCheckResourceAttr(resourceName, "canary_settings.0.use_stage_cache", "false"),
516+
),
517+
},
518+
},
519+
})
520+
}
521+
471522
func testAccCheckStageExists(n string, res *apigateway.Stage) resource.TestCheckFunc {
472523
return func(s *terraform.State) error {
473524
rs, ok := s.RootModule().Resources[n]
@@ -830,3 +881,47 @@ resource "aws_wafregional_web_acl_association" "test" {
830881
}
831882
`, rName)
832883
}
884+
885+
func testAccAWSAPIGatewayStageConfig_canarySettings(rName string) string {
886+
return testAccStageConfig_base(rName) + `
887+
resource "aws_api_gateway_stage" "test" {
888+
rest_api_id = aws_api_gateway_rest_api.test.id
889+
stage_name = "prod"
890+
deployment_id = aws_api_gateway_deployment.dev.id
891+
892+
canary_settings {
893+
percent_traffic = "33.33"
894+
stage_variable_overrides = {
895+
one = "3"
896+
}
897+
use_stage_cache = "true"
898+
}
899+
variables = {
900+
one = "1"
901+
two = "2"
902+
}
903+
}
904+
`
905+
}
906+
907+
func testAccAWSAPIGatewayStageConfig_canarySettingsUpdated(rName string) string {
908+
return testAccStageConfig_base(rName) + `
909+
resource "aws_api_gateway_stage" "test" {
910+
rest_api_id = aws_api_gateway_rest_api.test.id
911+
stage_name = "prod"
912+
deployment_id = aws_api_gateway_deployment.dev.id
913+
914+
canary_settings {
915+
percent_traffic = "66.66"
916+
stage_variable_overrides = {
917+
four = "5"
918+
}
919+
use_stage_cache = "false"
920+
}
921+
variables = {
922+
one = "1"
923+
two = "2"
924+
}
925+
}
926+
`
927+
}

website/docs/r/api_gateway_stage.html.markdown

+11-6
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ resource "aws_api_gateway_method_settings" "example" {
7373

7474
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.
7575

76-
-> 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.
76+
-> 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.
7777

7878
```terraform
7979
variable "stage_name" {
@@ -106,24 +106,29 @@ The following arguments are supported:
106106
* `rest_api_id` - (Required) The ID of the associated REST API
107107
* `stage_name` - (Required) The name of the stage
108108
* `deployment_id` - (Required) The ID of the deployment that the stage points to
109-
* `access_log_settings` - (Optional) Enables access logs for the API stage. Detailed below.
109+
* `access_log_settings` - (Optional) Enables access logs for the API stage. See [Access Log Settings](#access-log-settings) below.
110110
* `cache_cluster_enabled` - (Optional) Specifies whether a cache cluster is enabled for the stage
111111
* `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`.
112+
* `canary_settings` - (Optional) Configuration settings of a canary deployment. See [Canary Settings](#canary-settings) below.
112113
* `client_certificate_id` - (Optional) The identifier of a client certificate for the stage.
113-
* `description` - (Optional) The description of the stage
114+
* `description` - (Optional) The description of the stage.
114115
* `documentation_version` - (Optional) The version of the associated API documentation
115116
* `variables` - (Optional) A map that defines the stage variables
116117
* `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.
117118
* `xray_tracing_enabled` - (Optional) Whether active tracing with X-ray is enabled. Defaults to `false`.
118119

119-
### Nested Blocks
120-
121-
#### `access_log_settings`
120+
### Access Log Settings
122121

123122
* `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.
124123
* `format` - (Required) The formatting and values recorded in the logs.
125124
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)
126125

126+
### Canary Settings
127+
128+
* `percent_traffic` - (Optional) The percent `0.0` - `100.0` of traffic to divert to the canary deployment.
129+
* `stage_variable_overrides` - (Optional) A map of overridden stage `variables` (including new variables) for the canary deployment.
130+
* `use_stage_cache` - (Optional) Whether the canary deployment uses the stage cache. Defaults to false.
131+
127132
## Attributes Reference
128133

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

0 commit comments

Comments
 (0)