Skip to content

Commit

Permalink
Add activate attribute support for `fastly_service_waf_configuratio…
Browse files Browse the repository at this point in the history
…n` resource (#530)

* Add `activate` attribute support for `fastly_service_waf_configuration` resource

* update comment

* Mark `activate` as recomputed if it will clone

* Add `number` computed attribute

* Add `active` computed attribute

* adjust condition

* Add tests

* generate-docs

* apply suggestion
  • Loading branch information
smaeda-ks authored Jan 21, 2022
1 parent 9e6fc93 commit d0eb562
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 53 deletions.
7 changes: 7 additions & 0 deletions docs/resources/service_waf_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ $ terraform state rm fastly_service_waf_configuration.waf

### Optional

- **activate** (Boolean) Conditionally prevents a new firewall version from being activated. The apply step will continue to create a new draft version but will not activate it if this is set to `false`. Default `true`
- **allowed_http_versions** (String) Allowed HTTP versions
- **allowed_methods** (String) A space-separated list of HTTP method names
- **allowed_request_content_type** (String) Allowed request content types
Expand Down Expand Up @@ -613,6 +614,12 @@ $ terraform state rm fastly_service_waf_configuration.waf
- **warning_anomaly_score** (Number) Score value to add for warning anomalies
- **xss_score_threshold** (Number) XSS attack threshold

### Read-Only

- **active** (Boolean) Whether a specific firewall version is currently deployed
- **cloned_version** (Number) The latest cloned firewall version by the provider
- **number** (Number) The WAF firewall version

<a id="nestedblock--rule"></a>
### Nested Schema for `rule`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func TestAccFastlyServiceWAFVersionV1AddUpdateDeleteRules(t *testing.T) {
Revision: 2,
},
}
wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20)
wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20, true)
rulesTF1 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules1)
wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, rulesTF1, "")

Expand Down
4 changes: 2 additions & 2 deletions fastly/block_fastly_waf_configuration_v1_exclusion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestAccFastlyServiceWAFVersionV1Validation(t *testing.T) {

for _, c := range cases {

wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20)
wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20, true)
exclusionsTF1 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(c.exclusions)

wafVer1 := testAccFastlyServiceWAFVersionV1ComposeConfiguration(wafVerInput, "", exclusionsTF1)
Expand Down Expand Up @@ -238,7 +238,7 @@ func TestAccFastlyServiceWAFVersionV1AddUpdateDeleteExclusions(t *testing.T) {
},
}

wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20)
wafVerInput := testAccFastlyServiceWAFVersionV1BuildConfig(20, true)
rulesTF := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRules(rules)
exclusionsTF1 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(exclusions1)
exclusionsTF2 := testAccCheckFastlyServiceWAFVersionV1ComposeWAFRuleExclusions(exclusions2)
Expand Down
188 changes: 148 additions & 40 deletions fastly/resource_fastly_service_waf_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import (
"context"
"errors"
"fmt"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"log"
"sort"

gofastly "github.com/fastly/go-fastly/v6/fastly"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
Expand All @@ -23,14 +24,43 @@ func resourceServiceWAFConfigurationV1() *schema.Resource {
Importer: &schema.ResourceImporter{
StateContext: resourceServiceWAFConfigurationV1Import,
},
CustomizeDiff: validateWAFConfigurationResource,
CustomizeDiff: customdiff.All(
validateWAFConfigurationResource,
customdiff.ComputedIf("cloned_version", func(_ context.Context, d *schema.ResourceDiff, _ interface{}) bool {
// If anything other than "activate" has changed, the current version will be
// cloned in resourceServiceWAFConfigurationV1Update so set it as recomputed.
for _, changedKey := range d.GetChangedKeysPrefix("") {
if changedKey == "activate" {
continue
}
return true
}
return false
}),
),
Schema: map[string]*schema.Schema{
"waf_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The ID of the Web Application Firewall that the configuration belongs to",
},
"activate": {
Type: schema.TypeBool,
Description: "Conditionally prevents a new firewall version from being activated. The apply step will continue to create a new draft version but will not activate it if this is set to `false`. Default `true`",
Default: true,
Optional: true,
},
"active": {
Type: schema.TypeBool,
Description: "Whether a specific firewall version is currently deployed",
Computed: true,
},
"cloned_version": {
Type: schema.TypeInt,
Computed: true,
Description: "The latest cloned firewall version by the provider",
},
"allowed_http_versions": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -130,6 +160,11 @@ func resourceServiceWAFConfigurationV1() *schema.Resource {
Computed: true,
Description: "The maximum number of arguments allowed",
},
"number": {
Type: schema.TypeInt,
Computed: true,
Description: "The WAF firewall version",
},
"notice_anomaly_score": {
Type: schema.TypeInt,
Optional: true,
Expand Down Expand Up @@ -225,61 +260,98 @@ func resourceServiceWAFConfigurationV1Create(ctx context.Context, d *schema.Reso
func resourceServiceWAFConfigurationV1Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*FastlyClient).conn

latestVersion, err := getLatestVersion(d, meta)
if err != nil {
return diag.FromErr(err)
// If any attributes other than Computed (unconfigurable) or "activate" have changed, clone a new firewall version.
// Otherwise, don't clone but activate a draft version that was previously created with "activate = false".
var needsChange bool
for k, v := range resourceServiceWAFConfigurationV1().Schema {
if (v.Computed && !v.Optional) || k == "activate" {
continue
}
if d.HasChange(k) {
needsChange = true
break
}
}

wafID := d.Get("waf_id").(string)
log.Printf("[INFO] updating configuration for WAF: %s", wafID)
if latestVersion.Locked {
latestVersion, err = conn.CloneWAFVersion(&gofastly.CloneWAFVersionInput{
WAFID: wafID,
WAFVersionNumber: latestVersion.Number,
})

var latestVersion *gofastly.WAFVersion
var err error
if needsChange {
latestVersion, err = getLatestVersion(d, meta)
if err != nil {
return diag.FromErr(err)
}
}

input := buildUpdateInput(d, latestVersion.ID, latestVersion.Number)
if input.HasChanges() {
latestVersion, err = conn.UpdateWAFVersion(input)
if latestVersion.Locked {
latestVersion, err = conn.CloneWAFVersion(&gofastly.CloneWAFVersionInput{
WAFID: wafID,
WAFVersionNumber: latestVersion.Number,
})
if err != nil {
return diag.FromErr(err)
}
}
err = d.Set("cloned_version", latestVersion.Number)
if err != nil {
return diag.FromErr(err)
}
}

if d.HasChange("rule") {
if err := updateRules(d, meta, wafID, latestVersion.Number); err != nil {
input := buildUpdateInput(d, latestVersion.ID, latestVersion.Number)
if input.HasChanges() {
latestVersion, err = conn.UpdateWAFVersion(input)
if err != nil {
return diag.FromErr(err)
}
}

if d.HasChange("rule") {
if err := updateRules(d, meta, wafID, latestVersion.Number); err != nil {
return diag.FromErr(err)
}
}

if d.HasChange("rule_exclusion") {
if err := updateWAFRuleExclusions(d, meta, wafID, latestVersion.Number); err != nil {
return diag.FromErr(err)
}
}
} else {
// Retrieve draft version
versionToRead := d.Get("cloned_version").(int)
latestVersion, err = conn.GetWAFVersion(&gofastly.GetWAFVersionInput{
WAFID: wafID,
WAFVersionNumber: versionToRead,
})
if err != nil {
return diag.FromErr(err)
}
}

if d.HasChange("rule_exclusion") {
if err := updateWAFRuleExclusions(d, meta, wafID, latestVersion.Number); err != nil {
shouldActivate := d.Get("activate").(bool)
versionNotYetActivated := !needsChange && (!latestVersion.Locked && !latestVersion.Active)
if shouldActivate && (needsChange || versionNotYetActivated) {
err := conn.DeployWAFVersion(&gofastly.DeployWAFVersionInput{
WAFID: wafID,
WAFVersionNumber: d.Get("cloned_version").(int),
})
if err != nil {
return diag.FromErr(err)
}
}

err = conn.DeployWAFVersion(&gofastly.DeployWAFVersionInput{
WAFID: wafID,
WAFVersionNumber: latestVersion.Number,
})
if err != nil {
return diag.FromErr(err)
statusCheck := &WAFDeploymentChecker{
Timeout: d.Timeout(schema.TimeoutCreate),
Delay: WAFStatusCheckDelay,
MinTimeout: WAFStatusCheckMinTimeout,
Check: DefaultWAFDeploymentChecker(conn),
}
err = statusCheck.waitForDeployment(ctx, wafID, latestVersion)
if err != nil {
return diag.FromErr(err)
}
}

statusCheck := &WAFDeploymentChecker{
Timeout: d.Timeout(schema.TimeoutCreate),
Delay: WAFStatusCheckDelay,
MinTimeout: WAFStatusCheckMinTimeout,
Check: DefaultWAFDeploymentChecker(conn),
}
err = statusCheck.waitForDeployment(ctx, wafID, latestVersion)
if err != nil {
return diag.FromErr(err)
}
return resourceServiceWAFConfigurationV1Read(ctx, d, meta)
}

Expand All @@ -302,6 +374,31 @@ func resourceServiceWAFConfigurationV1Read(_ context.Context, d *schema.Resource
return diag.FromErr(err)
}

if d.Get("cloned_version").(int) == 0 {
err := d.Set("cloned_version", latestVersion.Number)
if err != nil {
return diag.FromErr(err)
}
}

if d.Get("activate") == false {
conn := meta.(*FastlyClient).conn
wafID := d.Get("waf_id").(string)
versionToRead := d.Get("cloned_version").(int)
latestVersion, err = conn.GetWAFVersion(&gofastly.GetWAFVersionInput{
WAFID: wafID,
WAFVersionNumber: versionToRead,
})
if err != nil {
return diag.FromErr(err)
}
} else {
err := d.Set("cloned_version", latestVersion.Number)
if err != nil {
return diag.FromErr(err)
}
}

log.Printf("[INFO] retrieving WAF version number: %d", latestVersion.Number)
refreshWAFConfig(d, latestVersion)

Expand Down Expand Up @@ -480,6 +577,7 @@ func buildUpdateInput(d *schema.ResourceData, id string, number int) *gofastly.U
}

func refreshWAFConfig(d *schema.ResourceData, version *gofastly.WAFVersion) {
d.Set("active", version.Active)
d.Set("allowed_http_versions", version.AllowedHTTPVersions)
d.Set("allowed_methods", version.AllowedMethods)
d.Set("allowed_request_content_type", version.AllowedRequestContentType)
Expand All @@ -496,6 +594,7 @@ func refreshWAFConfig(d *schema.ResourceData, version *gofastly.WAFVersion) {
d.Set("lfi_score_threshold", version.LFIScoreThreshold)
d.Set("max_file_size", version.MaxFileSize)
d.Set("max_num_args", version.MaxNumArgs)
d.Set("number", version.Number)
d.Set("notice_anomaly_score", version.NoticeAnomalyScore)
d.Set("paranoia_level", version.ParanoiaLevel)
d.Set("php_injection_score_threshold", version.PHPInjectionScoreThreshold)
Expand All @@ -520,10 +619,19 @@ func determineLatestVersion(versions []*gofastly.WAFVersion) (*gofastly.WAFVersi
return versions[i].Number > versions[j].Number
})

return versions[0], nil
// Find the current active firewall version
// or, pick the most recent version if there's no active version yet
latest := versions[0]
for _, v := range versions {
if v.Active {
latest = v
break
}
}

return latest, nil
}

func validateWAFConfigurationResource(_ context.Context, d *schema.ResourceDiff, _ interface{}) error {
err := validateWAFRuleExclusion(d)
return err
return validateWAFRuleExclusion(d)
}
Loading

0 comments on commit d0eb562

Please sign in to comment.