Skip to content

Commit fe7fc3b

Browse files
authored
Merge pull request ekristen#510 from ekristen/cloudcontrol-ratelimit
feat(cloudcontrol): implement rate limit w/ better logging
2 parents 5f9ab5f + df705ae commit fe7fc3b

File tree

2 files changed

+40
-17
lines changed

2 files changed

+40
-17
lines changed

resources/cloudcontrol.go

+34-15
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"time"
78

89
"github.com/google/uuid"
10+
"github.com/gotidy/ptr"
911
"github.com/pkg/errors"
1012
"github.com/sirupsen/logrus"
13+
"go.uber.org/ratelimit"
1114

12-
"github.com/aws/aws-sdk-go/aws"
1315
"github.com/aws/aws-sdk-go/aws/awserr"
1416
"github.com/aws/aws-sdk-go/service/cloudcontrolapi"
1517

@@ -53,6 +55,15 @@ func init() {
5355
RegisterCloudControl("AWS::NetworkFirewall::RuleGroup")
5456
}
5557

58+
// describeRateLimit is a rate limiter to avoid throttling when describing resources via the cloud control api.
59+
// AWS does not publish the rate limits for the cloud control api, the rate seems to be 60 reqs/minute, setting to
60+
// 55 and setting no slack to avoid throttling.
61+
var describeRateLimit = ratelimit.New(55, ratelimit.Per(time.Minute), ratelimit.WithoutSlack)
62+
63+
// RegisterCloudControl registers a resource type for the Cloud Control API. This is a unique function that is used
64+
// in two different places. The first place is in the init() function of this file, where it is used to register
65+
// a select subset of Cloud Control API resource types. The second place is in nuke command file, where it is used
66+
// to dynamically register any resource type provided via the `--cloud-control` flag.
5667
func RegisterCloudControl(typeName string) {
5768
registry.Register(&registry.Registration{
5869
Name: typeName,
@@ -66,26 +77,32 @@ func RegisterCloudControl(typeName string) {
6677

6778
type CloudControlResourceLister struct {
6879
TypeName string
80+
81+
logger *logrus.Entry
6982
}
7083

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

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

7691
params := &cloudcontrolapi.ListResourcesInput{
77-
TypeName: aws.String(l.TypeName),
92+
TypeName: ptr.String(l.TypeName),
93+
MaxResults: ptr.Int64(100),
7894
}
79-
resources := make([]resource.Resource, 0)
95+
8096
if err := svc.ListResourcesPages(params, func(page *cloudcontrolapi.ListResourcesOutput, lastPage bool) bool {
81-
for _, desc := range page.ResourceDescriptions {
82-
identifier := aws.StringValue(desc.Identifier)
97+
dt := describeRateLimit.Take()
98+
l.logger.Debugf("rate limit time: %s", dt)
8399

84-
properties, err := cloudControlParseProperties(aws.StringValue(desc.Properties))
100+
for _, desc := range page.ResourceDescriptions {
101+
identifier := ptr.ToString(desc.Identifier)
102+
properties, err := l.cloudControlParseProperties(ptr.ToString(desc.Properties))
85103
if err != nil {
86-
logrus.
104+
l.logger.
87105
WithError(errors.WithStack(err)).
88-
WithField("type-name", l.TypeName).
89106
WithField("identifier", identifier).
90107
Error("failed to parse cloud control properties")
91108
continue
@@ -117,17 +134,19 @@ func (l *CloudControlResourceLister) List(_ context.Context, o interface{}) ([]r
117134
return resources, nil
118135
}
119136

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

142+
properties := types.NewProperties()
125143
propMap := map[string]interface{}{}
144+
126145
err := json.Unmarshal([]byte(payload), &propMap)
127146
if err != nil {
128-
return nil, err
147+
return properties, err
129148
}
130-
properties := types.NewProperties()
149+
131150
for name, value := range propMap {
132151
switch v := value.(type) {
133152
case string:
@@ -147,12 +166,12 @@ func cloudControlParseProperties(payload string) (types.Properties, error) {
147166
v2["Value"],
148167
)
149168
} else {
150-
logrus.
169+
l.logger.
151170
WithField("value", fmt.Sprintf("%q", v)).
152171
Debugf("nested cloud control property type []%T is not supported", value)
153172
}
154173
default:
155-
logrus.
174+
l.logger.
156175
WithField("value", fmt.Sprintf("%q", v)).
157176
Debugf("nested cloud control property type []%T is not supported", value)
158177
}
@@ -163,9 +182,9 @@ func cloudControlParseProperties(payload string) (types.Properties, error) {
163182
// properties.Set, because it would fall back to
164183
// fmt.Sprintf. Since the cloud control properties are
165184
// nested it would create properties that are not
166-
// suitable for filtering. Therefore we have to
185+
// suitable for filtering. Therefore, we have to
167186
// implemented more sophisticated parsing.
168-
logrus.
187+
l.logger.
169188
WithField("value", fmt.Sprintf("%q", v)).
170189
Debugf("cloud control property type %T is not supported", v)
171190
}

resources/cloudcontrol_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestCloudControlParseProperties(t *testing.T) {
1616
want []string
1717
}{
1818
{
19-
name: "ActualEC2VPC",
19+
name: "AWS::EC2::VPC",
2020
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
2121
want: []string{
2222
`CidrBlock: "10.10.0.0/16"`,
@@ -31,7 +31,11 @@ func TestCloudControlParseProperties(t *testing.T) {
3131

3232
for _, tc := range cases {
3333
t.Run(tc.name, func(t *testing.T) {
34-
result, err := cloudControlParseProperties(tc.payload)
34+
lister := CloudControlResourceLister{
35+
TypeName: tc.name,
36+
}
37+
38+
result, err := lister.cloudControlParseProperties(tc.payload)
3539
assert.NoError(t, err)
3640
for _, w := range tc.want {
3741
assert.Contains(t, result.String(), w)

0 commit comments

Comments
 (0)