Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider/aws: AWS WAF Regional IPSet + ByteMatchSet support #13705

Merged
merged 14 commits into from
May 13, 2017
Merged
3 changes: 3 additions & 0 deletions builtin/providers/aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import (
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go/service/waf"
"github.com/aws/aws-sdk-go/service/wafregional"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-cleanhttp"
Expand Down Expand Up @@ -161,6 +162,7 @@ type AWSClient struct {
sfnconn *sfn.SFN
ssmconn *ssm.SSM
wafconn *waf.WAF
wafregionalconn *wafregional.WAFRegional
}

func (c *AWSClient) S3() *s3.S3 {
Expand Down Expand Up @@ -356,6 +358,7 @@ func (c *Config) Client() (interface{}, error) {
client.sqsconn = sqs.New(sess)
client.ssmconn = ssm.New(sess)
client.wafconn = waf.New(sess)
client.wafregionalconn = wafregional.New(sess)

return &client, nil
}
Expand Down
2 changes: 2 additions & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ func Provider() terraform.ResourceProvider {
"aws_waf_web_acl": resourceAwsWafWebAcl(),
"aws_waf_xss_match_set": resourceAwsWafXssMatchSet(),
"aws_waf_sql_injection_match_set": resourceAwsWafSqlInjectionMatchSet(),
"aws_wafregional_byte_match_set": resourceAwsWafRegionalByteMatchSet(),
"aws_wafregional_ipset": resourceAwsWafRegionalIPSet(),
},
ConfigureFunc: providerConfigure,
}
Expand Down
261 changes: 261 additions & 0 deletions builtin/providers/aws/resource_aws_wafregional_byte_match_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package aws

import (
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/waf"
"github.com/aws/aws-sdk-go/service/wafregional"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsWafRegionalByteMatchSet() *schema.Resource {
return &schema.Resource{
Create: resourceAwsWafRegionalByteMatchSetCreate,
Read: resourceAwsWafRegionalByteMatchSetRead,
Update: resourceAwsWafRegionalByteMatchSetUpdate,
Delete: resourceAwsWafRegionalByteMatchSetDelete,

Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"byte_match_tuple": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"field_to_match": {
Type: schema.TypeSet,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"data": {
Type: schema.TypeString,
Optional: true,
},
"type": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"positional_constraint": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"target_string": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"text_transformation": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
},
},
}
}

func resourceAwsWafRegionalByteMatchSetCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).wafregionalconn
region := meta.(*AWSClient).region

log.Printf("[INFO] Creating ByteMatchSet: %s", d.Get("name").(string))

wr := newWafRegionalRetryer(conn, region)
out, err := wr.RetryWithToken(func(token *string) (interface{}, error) {
params := &waf.CreateByteMatchSetInput{
ChangeToken: token,
Name: aws.String(d.Get("name").(string)),
}
return conn.CreateByteMatchSet(params)
})

if err != nil {
return errwrap.Wrapf("[ERROR] Error creating ByteMatchSet: {{err}}", err)
}
resp := out.(*waf.CreateByteMatchSetOutput)

d.SetId(*resp.ByteMatchSet.ByteMatchSetId)

return resourceAwsWafRegionalByteMatchSetUpdate(d, meta)
}

func resourceAwsWafRegionalByteMatchSetRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).wafregionalconn

log.Printf("[INFO] Reading ByteMatchSet: %s", d.Get("name").(string))

params := &waf.GetByteMatchSetInput{
ByteMatchSetId: aws.String(d.Id()),
}

resp, err := conn.GetByteMatchSet(params)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "WAFNonexistentItemException" {
log.Printf("[WARN] WAF IPSet (%s) not found, error code (404)", d.Id())
d.SetId("")
return nil
}

return err
}

var tuples []interface{}

for _, tuple := range resp.ByteMatchSet.ByteMatchTuples {
field_to_match := tuple.FieldToMatch
m := map[string]interface{}{
"type": *field_to_match.Type,
}

if field_to_match.Data == nil {
m["data"] = ""
} else {
m["data"] = *field_to_match.Data
}

var ms []map[string]interface{}
ms = append(ms, m)

tuple := map[string]interface{}{
"field_to_match": ms,
"positional_constraint": *tuple.PositionalConstraint,
"target_string": tuple.TargetString,
"text_transformation": *tuple.TextTransformation,
}
tuples = append(tuples, tuple)
}
d.Set("byte_match_tuple", tuples)
d.Set("name", resp.ByteMatchSet.Name)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're missing some fields here, specifically byte_match_tuples and all nested fields under it. The expectation from the Terraform user is that for any resource Terraform will detect drifts from the configuration. In order to do that we need to set all the available data from the API via d.Set() here in Read func.

Sets can be slightly trickier to work with, I will try and fix the WAF resources first and point you there for reference instead of trying to explain how this can be done. 😅

It's probably the cause of some of the reported WAF bugs I saw earlier, so it's worth fixing there either way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw. this would also break import functionality when it's actually implemented as it leverages Read() (at least the default implementation)

return nil
}

func resourceAwsWafRegionalByteMatchSetUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).wafregionalconn
region := meta.(*AWSClient).region
log.Printf("[INFO] Updating ByteMatchSet: %s", d.Get("name").(string))

if d.HasChange("byte_match_tuple") {
o, n := d.GetChange("byte_match_tuple")
oldT, newT := o.(*schema.Set).List(), n.(*schema.Set).List()

err := updateByteMatchSetResourceWR(d, oldT, newT, conn, region)
if err != nil {
return errwrap.Wrapf("[ERROR] Error updating ByteMatchSet: {{err}}", err)
}
}
return resourceAwsWafRegionalByteMatchSetRead(d, meta)
}

func resourceAwsWafRegionalByteMatchSetDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).wafregionalconn
region := meta.(*AWSClient).region

log.Printf("[INFO] Deleting ByteMatchSet: %s", d.Get("name").(string))

oldT := d.Get("byte_match_tuple").(*schema.Set).List()

if len(oldT) > 0 {
var newT []interface{}

err := updateByteMatchSetResourceWR(d, oldT, newT, conn, region)
if err != nil {
return errwrap.Wrapf("[ERROR] Error deleting ByteMatchSet: {{err}}", err)
}
}

wr := newWafRegionalRetryer(conn, region)
_, err := wr.RetryWithToken(func(token *string) (interface{}, error) {
req := &waf.DeleteByteMatchSetInput{
ChangeToken: token,
ByteMatchSetId: aws.String(d.Id()),
}
return conn.DeleteByteMatchSet(req)
})
if err != nil {
return errwrap.Wrapf("[ERROR] Error deleting ByteMatchSet: {{err}}", err)
}

return nil
}

func updateByteMatchSetResourceWR(d *schema.ResourceData, oldT, newT []interface{}, conn *wafregional.WAFRegional, region string) error {
wr := newWafRegionalRetryer(conn, region)
_, err := wr.RetryWithToken(func(token *string) (interface{}, error) {
req := &waf.UpdateByteMatchSetInput{
ChangeToken: token,
ByteMatchSetId: aws.String(d.Id()),
Updates: diffByteMatchSetTuple(oldT, newT),
}

return conn.UpdateByteMatchSet(req)
})
if err != nil {
return errwrap.Wrapf("[ERROR] Error updating ByteMatchSet: {{err}}", err)
}

return nil
}

func expandFieldToMatchWR(d map[string]interface{}) *waf.FieldToMatch {
return &waf.FieldToMatch{
Type: aws.String(d["type"].(string)),
Data: aws.String(d["data"].(string)),
}
}

func flattenFieldToMatchWR(fm *waf.FieldToMatch) map[string]interface{} {
m := make(map[string]interface{})
m["data"] = *fm.Data
m["type"] = *fm.Type
return m
}

func diffByteMatchSetTuple(oldT, newT []interface{}) []*waf.ByteMatchSetUpdate {
updates := make([]*waf.ByteMatchSetUpdate, 0)

for _, ot := range oldT {
tuple := ot.(map[string]interface{})

if idx, contains := sliceContainsMap(newT, tuple); contains {
newT = append(newT[:idx], newT[idx+1:]...)
continue
}

updates = append(updates, &waf.ByteMatchSetUpdate{
Action: aws.String(waf.ChangeActionDelete),
ByteMatchTuple: &waf.ByteMatchTuple{
FieldToMatch: expandFieldToMatch(tuple["field_to_match"].(*schema.Set).List()[0].(map[string]interface{})),
PositionalConstraint: aws.String(tuple["positional_constraint"].(string)),
TargetString: []byte(tuple["target_string"].(string)),
TextTransformation: aws.String(tuple["text_transformation"].(string)),
},
})
}

for _, nt := range newT {
tuple := nt.(map[string]interface{})

updates = append(updates, &waf.ByteMatchSetUpdate{
Action: aws.String(waf.ChangeActionInsert),
ByteMatchTuple: &waf.ByteMatchTuple{
FieldToMatch: expandFieldToMatch(tuple["field_to_match"].(*schema.Set).List()[0].(map[string]interface{})),
PositionalConstraint: aws.String(tuple["positional_constraint"].(string)),
TargetString: []byte(tuple["target_string"].(string)),
TextTransformation: aws.String(tuple["text_transformation"].(string)),
},
})
}
return updates
}
Loading