Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions resources/cloudcontrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/google/uuid"
"github.com/gotidy/ptr"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"go.uber.org/ratelimit"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/cloudcontrolapi"

Expand Down Expand Up @@ -53,6 +55,15 @@ func init() {
RegisterCloudControl("AWS::NetworkFirewall::RuleGroup")
}

// describeRateLimit is a rate limiter to avoid throttling when describing resources via the cloud control api.
// AWS does not publish the rate limits for the cloud control api, the rate seems to be 60 reqs/minute, setting to
// 55 and setting no slack to avoid throttling.
var describeRateLimit = ratelimit.New(55, ratelimit.Per(time.Minute), ratelimit.WithoutSlack)

// RegisterCloudControl registers a resource type for the Cloud Control API. This is a unique function that is used
// in two different places. The first place is in the init() function of this file, where it is used to register
// a select subset of Cloud Control API resource types. The second place is in nuke command file, where it is used
// to dynamically register any resource type provided via the `--cloud-control` flag.
func RegisterCloudControl(typeName string) {
registry.Register(&registry.Registration{
Name: typeName,
Expand All @@ -66,26 +77,32 @@ func RegisterCloudControl(typeName string) {

type CloudControlResourceLister struct {
TypeName string

logger *logrus.Entry
}

func (l *CloudControlResourceLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) {
opts := o.(*nuke.ListerOpts)
l.logger = opts.Logger.WithField("type-name", l.TypeName)

svc := cloudcontrolapi.New(opts.Session)
resources := make([]resource.Resource, 0)

params := &cloudcontrolapi.ListResourcesInput{
TypeName: aws.String(l.TypeName),
TypeName: ptr.String(l.TypeName),
MaxResults: ptr.Int64(100),
}
resources := make([]resource.Resource, 0)

if err := svc.ListResourcesPages(params, func(page *cloudcontrolapi.ListResourcesOutput, lastPage bool) bool {
for _, desc := range page.ResourceDescriptions {
identifier := aws.StringValue(desc.Identifier)
dt := describeRateLimit.Take()
l.logger.Debugf("rate limit time: %s", dt)

properties, err := cloudControlParseProperties(aws.StringValue(desc.Properties))
for _, desc := range page.ResourceDescriptions {
identifier := ptr.ToString(desc.Identifier)
properties, err := l.cloudControlParseProperties(ptr.ToString(desc.Properties))
if err != nil {
logrus.
l.logger.
WithError(errors.WithStack(err)).
WithField("type-name", l.TypeName).
WithField("identifier", identifier).
Error("failed to parse cloud control properties")
continue
Expand Down Expand Up @@ -117,17 +134,19 @@ func (l *CloudControlResourceLister) List(_ context.Context, o interface{}) ([]r
return resources, nil
}

func cloudControlParseProperties(payload string) (types.Properties, error) {
func (l *CloudControlResourceLister) cloudControlParseProperties(payload string) (types.Properties, error) {
// Warning: The implementation of this function is not very straightforward,
// because the aws-nuke filter functions expect a very rigid structure and
// the properties from the Cloud Control API are very dynamic.

properties := types.NewProperties()
propMap := map[string]interface{}{}

err := json.Unmarshal([]byte(payload), &propMap)
if err != nil {
return nil, err
return properties, err
}
properties := types.NewProperties()

for name, value := range propMap {
switch v := value.(type) {
case string:
Expand All @@ -147,12 +166,12 @@ func cloudControlParseProperties(payload string) (types.Properties, error) {
v2["Value"],
)
} else {
logrus.
l.logger.
WithField("value", fmt.Sprintf("%q", v)).
Debugf("nested cloud control property type []%T is not supported", value)
}
default:
logrus.
l.logger.
WithField("value", fmt.Sprintf("%q", v)).
Debugf("nested cloud control property type []%T is not supported", value)
}
Expand All @@ -163,9 +182,9 @@ func cloudControlParseProperties(payload string) (types.Properties, error) {
// properties.Set, because it would fall back to
// fmt.Sprintf. Since the cloud control properties are
// nested it would create properties that are not
// suitable for filtering. Therefore we have to
// suitable for filtering. Therefore, we have to
// implemented more sophisticated parsing.
logrus.
l.logger.
WithField("value", fmt.Sprintf("%q", v)).
Debugf("cloud control property type %T is not supported", v)
}
Expand Down
8 changes: 6 additions & 2 deletions resources/cloudcontrol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestCloudControlParseProperties(t *testing.T) {
want []string
}{
{
name: "ActualEC2VPC",
name: "AWS::EC2::VPC",
payload: `{"VpcId":"vpc-456","InstanceTenancy":"default","CidrBlockAssociations":["vpc-cidr-assoc-1234", "vpc-cidr-assoc-5678"],"CidrBlock":"10.10.0.0/16","Tags":[{"Value":"Kubernetes VPC","Key":"Name"}]}`, //nolint:lll
want: []string{
`CidrBlock: "10.10.0.0/16"`,
Expand All @@ -31,7 +31,11 @@ func TestCloudControlParseProperties(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result, err := cloudControlParseProperties(tc.payload)
lister := CloudControlResourceLister{
TypeName: tc.name,
}

result, err := lister.cloudControlParseProperties(tc.payload)
assert.NoError(t, err)
for _, w := range tc.want {
assert.Contains(t, result.String(), w)
Expand Down
Loading