-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1211 from Vlaaaaaaad/waf-v2-clean
WAFv2 support
- Loading branch information
Showing
14 changed files
with
3,451 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package lb | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/albctx" | ||
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws" | ||
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/ingress/annotations" | ||
"github.com/pkg/errors" | ||
extensions "k8s.io/api/extensions/v1beta1" | ||
"k8s.io/apimachinery/pkg/util/cache" | ||
) | ||
|
||
const ( | ||
webACLARNForLBCacheMaxSize = 1024 | ||
webACLARNForLBCacheTTL = 10 * time.Minute | ||
) | ||
|
||
// WAFCV2ontroller provides functionality to manage ALB's WAF V2 associations. | ||
type WAFV2Controller interface { | ||
Reconcile(ctx context.Context, lbArn string, ingress *extensions.Ingress) error | ||
} | ||
|
||
func NewWAFV2Controller(cloud aws.CloudAPI) WAFV2Controller { | ||
return &defaultWAFV2Controller{ | ||
cloud: cloud, | ||
webACLARNForLBCache: cache.NewLRUExpireCache(webACLARNForLBCacheMaxSize), | ||
} | ||
} | ||
|
||
type defaultWAFV2Controller struct { | ||
cloud aws.CloudAPI | ||
|
||
// cache that stores webACLARNForLBCache for LoadBalancerARN. | ||
// The cache value is string, while "" represents no webACL. | ||
webACLARNForLBCache *cache.LRUExpireCache | ||
} | ||
|
||
func (c *defaultWAFV2Controller) Reconcile(ctx context.Context, lbArn string, ing *extensions.Ingress) error { | ||
var desiredWebACLARN string | ||
|
||
_ = annotations.LoadStringAnnotation("wafv2-acl-arn", &desiredWebACLARN, ing.Annotations) | ||
|
||
currentWebACLId, err := c.getCurrentWebACLARN(ctx, lbArn) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
switch { | ||
case desiredWebACLARN == "" && currentWebACLId != "": | ||
albctx.GetLogger(ctx).Infof("disassociate WAFv2 webACL on %v", lbArn) | ||
if _, err := c.cloud.DisassociateWAFV2(ctx, aws.String(lbArn)); err != nil { | ||
return errors.Wrapf(err, "failed to disassociate WAFv2 webACL on LoadBalancer %v", lbArn) | ||
} | ||
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL) | ||
case desiredWebACLARN != "" && currentWebACLId != "" && desiredWebACLARN != currentWebACLId: | ||
albctx.GetLogger(ctx).Infof("change WAFv2 webACL on %v from %v to %v", lbArn, currentWebACLId, desiredWebACLARN) | ||
if _, err := c.cloud.AssociateWAFV2(ctx, aws.String(lbArn), aws.String(desiredWebACLARN)); err != nil { | ||
return errors.Wrapf(err, "failed to associate WAFv2 webACL on LoadBalancer %v", lbArn) | ||
} | ||
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL) | ||
case desiredWebACLARN != "" && currentWebACLId == "": | ||
albctx.GetLogger(ctx).Infof("associate WAFv2 webACL %v on %v", desiredWebACLARN, lbArn) | ||
if _, err := c.cloud.AssociateWAFV2(ctx, aws.String(lbArn), aws.String(desiredWebACLARN)); err != nil { | ||
return errors.Wrapf(err, "failed to associate WAFv2 webACL on LoadBalancer %v", lbArn) | ||
} | ||
c.webACLARNForLBCache.Add(lbArn, desiredWebACLARN, webACLARNForLBCacheTTL) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *defaultWAFV2Controller) getCurrentWebACLARN(ctx context.Context, lbArn string) (string, error) { | ||
cachedWebACLARN, exists := c.webACLARNForLBCache.Get(lbArn) | ||
if exists { | ||
return cachedWebACLARN.(string), nil | ||
} | ||
|
||
webACL, err := c.cloud.GetWAFV2WebACLSummary(ctx, aws.String(lbArn)) | ||
if err != nil { | ||
return "", errors.Wrapf(err, "failed get WAFv2 webACL for load balancer %v", lbArn) | ||
} | ||
|
||
var webACLARN string | ||
if webACL != nil { | ||
webACLARN = aws.StringValue(webACL.ARN) | ||
} | ||
|
||
c.webACLARNForLBCache.Add(lbArn, webACLARN, webACLARNForLBCacheTTL) | ||
return webACLARN, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
package lb | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"k8s.io/apimachinery/pkg/util/intstr" | ||
|
||
"github.com/aws/aws-sdk-go/service/wafv2" | ||
"github.com/kubernetes-sigs/aws-alb-ingress-controller/internal/aws" | ||
"github.com/kubernetes-sigs/aws-alb-ingress-controller/mocks" | ||
"github.com/stretchr/testify/assert" | ||
apiv1 "k8s.io/api/core/v1" | ||
extensions "k8s.io/api/extensions/v1beta1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
func buildWAFV2TestIngress(wafIngressAnnotations map[string]string) *extensions.Ingress { | ||
defaultBackend := extensions.IngressBackend{ | ||
ServiceName: "default-backend", | ||
ServicePort: intstr.FromInt(80), | ||
} | ||
|
||
return &extensions.Ingress{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: "foo", | ||
Namespace: apiv1.NamespaceDefault, | ||
Annotations: wafIngressAnnotations, | ||
}, | ||
Spec: extensions.IngressSpec{ | ||
Backend: &extensions.IngressBackend{ | ||
ServiceName: "default-backend", | ||
ServicePort: intstr.FromInt(80), | ||
}, | ||
Rules: []extensions.IngressRule{ | ||
{ | ||
Host: "foo.bar.com", | ||
IngressRuleValue: extensions.IngressRuleValue{ | ||
HTTP: &extensions.HTTPIngressRuleValue{ | ||
Paths: []extensions.HTTPIngressPath{ | ||
{ | ||
Path: "/foo", | ||
Backend: defaultBackend, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func Test_defaultWAFV2Controller_Reconcile(t *testing.T) { | ||
for _, tc := range []struct { | ||
Name string | ||
GetWAFV2WebACLSummaryResponse *wafv2.WebACL | ||
GetWAFV2WebACLSummaryError error | ||
AssociateWAFV2Response *wafv2.AssociateWebACLOutput | ||
AssociateWAFV2Error error | ||
DisassociateWAFV2Response *wafv2.DisassociateWebACLOutput | ||
DisassociateWAFV2Error error | ||
Expected error | ||
ExpectedError error | ||
LoadBalancerARN string | ||
IngressAnnotations *extensions.Ingress | ||
DesiredWebACLARN string | ||
GetWAFV2WebACLSummaryTimesCalled int | ||
AssociateWAFV2TimesCalled int | ||
DisassociateWAFV2TimesCalled int | ||
}{ | ||
{ | ||
Name: "No annotation, confirm nothing is attached", | ||
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{ | ||
ARN: nil, | ||
}, | ||
GetWAFV2WebACLSummaryError: nil, | ||
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{}, | ||
AssociateWAFV2Error: nil, | ||
Expected: nil, | ||
LoadBalancerARN: "arn:lb", | ||
IngressAnnotations: buildWAFV2TestIngress( | ||
map[string]string{}, | ||
), | ||
DesiredWebACLARN: "", | ||
GetWAFV2WebACLSummaryTimesCalled: 1, | ||
AssociateWAFV2TimesCalled: 0, | ||
DisassociateWAFV2TimesCalled: 0, | ||
}, | ||
{ | ||
Name: "Empty WAFv2 annotation", | ||
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{ | ||
ARN: nil, | ||
}, | ||
GetWAFV2WebACLSummaryError: nil, | ||
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{}, | ||
AssociateWAFV2Error: nil, | ||
Expected: nil, | ||
LoadBalancerARN: "arn:lb", | ||
IngressAnnotations: buildWAFV2TestIngress( | ||
map[string]string{ | ||
"alb.ingress.kubernetes.io/wafv2-acl-arn": "", | ||
}, | ||
), | ||
DesiredWebACLARN: "", | ||
GetWAFV2WebACLSummaryTimesCalled: 1, | ||
AssociateWAFV2TimesCalled: 0, | ||
DisassociateWAFV2TimesCalled: 0, | ||
}, | ||
{ | ||
Name: "No annotation, detach WAFv2", | ||
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{ | ||
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a"), | ||
}, | ||
GetWAFV2WebACLSummaryError: nil, | ||
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{}, | ||
AssociateWAFV2Error: nil, | ||
Expected: nil, | ||
LoadBalancerARN: "arn:lb", | ||
IngressAnnotations: buildWAFV2TestIngress( | ||
map[string]string{}, | ||
), | ||
DesiredWebACLARN: "", | ||
GetWAFV2WebACLSummaryTimesCalled: 1, | ||
AssociateWAFV2TimesCalled: 0, | ||
DisassociateWAFV2TimesCalled: 1, | ||
}, | ||
{ | ||
Name: "Empty annotation, dissassociate WAFv2", | ||
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{ | ||
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a"), | ||
}, | ||
GetWAFV2WebACLSummaryError: nil, | ||
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{}, | ||
AssociateWAFV2Error: nil, | ||
Expected: nil, | ||
LoadBalancerARN: "arn:lb", | ||
IngressAnnotations: buildWAFV2TestIngress( | ||
map[string]string{ | ||
"alb.ingress.kubernetes.io/wafv2-acl-arn": "", | ||
}, | ||
), | ||
DesiredWebACLARN: "", | ||
GetWAFV2WebACLSummaryTimesCalled: 1, | ||
AssociateWAFV2TimesCalled: 0, | ||
DisassociateWAFV2TimesCalled: 1, | ||
}, | ||
{ | ||
Name: "Annotation, associate WAFv2", | ||
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{ | ||
ARN: nil, | ||
}, | ||
GetWAFV2WebACLSummaryError: nil, | ||
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{}, | ||
AssociateWAFV2Error: nil, | ||
Expected: nil, | ||
LoadBalancerARN: "arn:lb", | ||
IngressAnnotations: buildWAFV2TestIngress( | ||
map[string]string{ | ||
"alb.ingress.kubernetes.io/wafv2-acl-arn": "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a", | ||
}, | ||
), | ||
DesiredWebACLARN: "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a", | ||
GetWAFV2WebACLSummaryTimesCalled: 1, | ||
AssociateWAFV2TimesCalled: 1, | ||
DisassociateWAFV2TimesCalled: 0, | ||
}, | ||
{ | ||
Name: "Annotation, change WAFv2", | ||
GetWAFV2WebACLSummaryResponse: &wafv2.WebACL{ | ||
ARN: aws.String("arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0bb00000-00b0-00b0-b0b0-0b0000a0000b"), | ||
}, | ||
GetWAFV2WebACLSummaryError: nil, | ||
Expected: nil, | ||
AssociateWAFV2Response: &wafv2.AssociateWebACLOutput{}, | ||
AssociateWAFV2Error: nil, | ||
LoadBalancerARN: "arn:lb", | ||
IngressAnnotations: buildWAFV2TestIngress( | ||
map[string]string{ | ||
"alb.ingress.kubernetes.io/wafv2-acl-arn": "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a", | ||
}, | ||
), | ||
DesiredWebACLARN: "arn:aws:wafv2:us-east-1:000000000000:regional/webacl/name/0aa00000-00a0-00a0-a0a0-0a0000a0000a", | ||
GetWAFV2WebACLSummaryTimesCalled: 1, | ||
AssociateWAFV2TimesCalled: 1, | ||
DisassociateWAFV2TimesCalled: 0, | ||
}, | ||
} { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
ctx := context.Background() | ||
cloud := &mocks.CloudAPI{} | ||
|
||
cloud.On("GetWAFV2WebACLSummary", ctx, aws.String(tc.LoadBalancerARN)).Return(tc.GetWAFV2WebACLSummaryResponse, tc.GetWAFV2WebACLSummaryError) | ||
cloud.On("AssociateWAFV2", ctx, aws.String(tc.LoadBalancerARN), aws.String(tc.DesiredWebACLARN)).Return(tc.AssociateWAFV2Response, tc.AssociateWAFV2Error) | ||
cloud.On("DisassociateWAFV2", ctx, aws.String(tc.LoadBalancerARN)).Return(tc.DisassociateWAFV2Response, tc.DisassociateWAFV2Error) | ||
|
||
controller := NewWAFV2Controller(cloud) | ||
err := controller.Reconcile(ctx, tc.LoadBalancerARN, tc.IngressAnnotations) | ||
assert.Equal(t, tc.Expected, err) | ||
cloud.AssertNumberOfCalls(t, "GetWAFV2WebACLSummary", tc.GetWAFV2WebACLSummaryTimesCalled) | ||
cloud.AssertNumberOfCalls(t, "AssociateWAFV2", tc.AssociateWAFV2TimesCalled) | ||
cloud.AssertNumberOfCalls(t, "DisassociateWAFV2", tc.DisassociateWAFV2TimesCalled) | ||
}) | ||
} | ||
} |
Oops, something went wrong.