diff --git a/go.mod b/go.mod index f62f436f..895816f3 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21.6 require ( github.com/aws/aws-sdk-go v1.50.24 - github.com/ekristen/libnuke v0.11.0 + github.com/ekristen/libnuke v0.12.0 github.com/fatih/color v1.16.0 github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 01bbe06e..5fb7f1ee 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ekristen/libnuke v0.11.0 h1:SdSaAVnuAdGDqNlV82uAa+2FD9NeQyH96rIMduheh6o= github.com/ekristen/libnuke v0.11.0/go.mod h1:sBdA04l9IMMejQw5gO9k6o/a0GffSYhgZYaUSdRjIac= +github.com/ekristen/libnuke v0.12.0 h1:Dsk+ckT9sh9QZTLq5m8kOA1KFJGJxSv0TLnfe3YeL1o= +github.com/ekristen/libnuke v0.12.0/go.mod h1:sBdA04l9IMMejQw5gO9k6o/a0GffSYhgZYaUSdRjIac= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= diff --git a/resources/elbv2-listenerrule.go b/resources/elbv2-listenerrule.go new file mode 100644 index 00000000..5f01fd3f --- /dev/null +++ b/resources/elbv2-listenerrule.go @@ -0,0 +1,158 @@ +package resources + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + + "github.com/aws/aws-sdk-go/service/elbv2" + + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/slices" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/aws-nuke/pkg/nuke" +) + +var elbv2ListenerRulePageSize int64 = 400 // AWS has a limit of 100 rules per listener + +const ELBv2ListenerRuleResource = "ELBv2ListenerRule" + +func init() { + registry.Register(®istry.Registration{ + Name: ELBv2ListenerRuleResource, + Scope: nuke.Account, + Lister: &ELBv2ListenerRuleLister{}, + }) +} + +type ELBv2ListenerRuleLister struct{} + +func (l *ELBv2ListenerRuleLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + + svc := elbv2.New(opts.Session) + + // We need to retrieve ELBs then Listeners then Rules + lbs := make([]*elbv2.LoadBalancer, 0) + err := svc.DescribeLoadBalancersPages( + nil, + func(page *elbv2.DescribeLoadBalancersOutput, lastPage bool) bool { + lbs = append(lbs, page.LoadBalancers...) + + return !lastPage + }, + ) + if err != nil { + return nil, err + } + + // Required for batched tag retrieval later + ruleArns := make([]*string, 0) + ruleArnToResource := make(map[string]*ELBv2ListenerRule) + + resources := make([]resource.Resource, 0) + for _, lb := range lbs { + err := svc.DescribeListenersPages( + &elbv2.DescribeListenersInput{ + LoadBalancerArn: lb.LoadBalancerArn, + }, + func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { + for _, listener := range page.Listeners { + rules, err := svc.DescribeRules(&elbv2.DescribeRulesInput{ + ListenerArn: listener.ListenerArn, + PageSize: &elbv2ListenerRulePageSize, + }) + if err == nil { + for _, rule := range rules.Rules { + // Skip default rules as they cannot be deleted + if rule.IsDefault != nil && *rule.IsDefault { + continue + } + + listenerRule := &ELBv2ListenerRule{ + svc: svc, + ruleArn: rule.RuleArn, + lbName: lb.LoadBalancerName, + listenerArn: listener.ListenerArn, + } + + ruleArns = append(ruleArns, rule.RuleArn) + resources = append(resources, listenerRule) + ruleArnToResource[*rule.RuleArn] = listenerRule + } + } else { + logrus. + WithError(err). + WithField("listenerArn", listener.ListenerArn). + Error("Failed to list listener rules for listener") + } + } + + return !lastPage + }, + ) + if err != nil { + logrus. + WithError(err). + WithField("loadBalancerArn", lb.LoadBalancerArn). + Error("Failed to list listeners for load balancer") + } + } + + // Tags for Rules need to be fetched separately. We can only specify up to 20 in a single call. + // See: https://github.com/aws/aws-sdk-go/blob/0e8c61841163762f870f6976775800ded4a789b0/service/elbv2/api.go#L5398 + for _, ruleChunk := range slices.Chunk(ruleArns, 20) { + tagResp, err := svc.DescribeTags(&elbv2.DescribeTagsInput{ + ResourceArns: ruleChunk, + }) + if err != nil { + return nil, err + } + for _, elbv2TagInfo := range tagResp.TagDescriptions { + rule := ruleArnToResource[*elbv2TagInfo.ResourceArn] + rule.tags = elbv2TagInfo.Tags + } + } + + return resources, nil +} + +type ELBv2ListenerRule struct { + svc *elbv2.ELBV2 + ruleArn *string + lbName *string + listenerArn *string + tags []*elbv2.Tag +} + +func (e *ELBv2ListenerRule) Remove(_ context.Context) error { + _, err := e.svc.DeleteRule(&elbv2.DeleteRuleInput{ + RuleArn: e.ruleArn, + }) + if err != nil { + return err + } + + return nil +} + +func (e *ELBv2ListenerRule) Properties() types.Properties { + properties := types.NewProperties() + + properties.Set("ARN", e.ruleArn) + properties.Set("ListenerARN", e.listenerArn) + properties.Set("LoadBalancerName", e.lbName) + + for _, tagValue := range e.tags { + properties.SetTag(tagValue.Key, tagValue.Value) + } + + return properties +} + +func (e *ELBv2ListenerRule) String() string { + return fmt.Sprintf("%s -> %s", *e.lbName, *e.ruleArn) +}