Skip to content

Commit

Permalink
[WIP] Feature: Support Nuking Lambda Layers/Versions (#605)
Browse files Browse the repository at this point in the history
* add initial resources for lambda_version

* update readme for running tests in aws/resources

* refactor to lambda layer, list cmd working

* rm debugs

* layer to version fetching

* add tests for get all

* add nuke lamda layers

* add tests for nuke lambda

* clean up

* clean up logs

* fix naming

* address comments

* add comment denoting nuances of name output
  • Loading branch information
derekrliang authored Nov 22, 2023
1 parent 5e1c221 commit cf3cdee
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 6 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,14 @@ cd aws
go test -v -run TestListAMIs
```


And to run a specific test, such as `TestLambdaFunction_GetAll` in package `aws/resources`:

```bash
cd aws/resources
go test -v -run TestLambdaFunction_GetAll
```

Use env-vars to opt-in to special tests, which are expensive to run:

```bash
Expand Down
4 changes: 3 additions & 1 deletion aws/resource_registry.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package aws

import (
"reflect"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/gruntwork-io/cloud-nuke/aws/resources"
"reflect"
)

const Global = "global"
Expand Down Expand Up @@ -84,6 +85,7 @@ func getRegisteredRegionalResources() []AwsResources {
&resources.KinesisStreams{},
&resources.KmsCustomerKeys{},
&resources.LambdaFunctions{},
&resources.LambdaLayers{},
&resources.LaunchConfigs{},
&resources.LaunchTemplates{},
&resources.MacieMember{},
Expand Down
4 changes: 2 additions & 2 deletions aws/resources/lambda.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package resources

import (
"context"
"github.com/gruntwork-io/cloud-nuke/telemetry"
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
"time"

"github.com/aws/aws-sdk-go/aws"
Expand All @@ -12,6 +10,8 @@ import (
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/cloud-nuke/report"
"github.com/gruntwork-io/cloud-nuke/telemetry"
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
)

func (lf *LambdaFunctions) getAll(c context.Context, configObj config.Config) ([]*string, error) {
Expand Down
148 changes: 148 additions & 0 deletions aws/resources/lambda_layer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package resources

import (
"context"
"time"

"github.com/aws/aws-sdk-go/aws"
awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/cloud-nuke/report"
"github.com/gruntwork-io/cloud-nuke/telemetry"
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
)

func (ll *LambdaLayers) getAll(c context.Context, configObj config.Config) ([]*string, error) {
var layers []*lambda.LayersListItem
var names []*string

err := ll.Client.ListLayersPages(
&lambda.ListLayersInput{}, func(page *lambda.ListLayersOutput, lastPage bool) bool {
for _, layer := range page.Layers {
logging.Logger.Debugf("Found layer! %s", layer)

if ll.shouldInclude(layer, configObj) {
layers = append(layers, layer)
}
}

return !lastPage
})

if err != nil {
return nil, err
}

for _, layer := range layers {
err := ll.Client.ListLayerVersionsPages(
&lambda.ListLayerVersionsInput{
LayerName: layer.LayerName,
}, func(page *lambda.ListLayerVersionsOutput, lastPage bool) bool {
for _, version := range page.LayerVersions {
logging.Logger.Debugf("Found layer version! %s", version)

// Currently the output is just the identifier which is the layer's name.
// There could be potentially multiple rows of the same identifier or
// layer name since there can be multiple versions of it.
names = append(names, layer.LayerName)
}

return !lastPage
})

if err != nil {
return nil, err
}
}

return names, nil
}

func (ll *LambdaLayers) shouldInclude(lambdaLayer *lambda.LayersListItem, configObj config.Config) bool {
if lambdaLayer == nil {
return false
}

// Lambda layers are immutable, so the created date of the latest version
// is on par with last modified
fnLastModified := aws.StringValue(lambdaLayer.LatestMatchingVersion.CreatedDate)
fnName := lambdaLayer.LayerName
layout := "2006-01-02T15:04:05.000+0000"
lastModifiedDateTime, err := time.Parse(layout, fnLastModified)
if err != nil {
logging.Logger.Debugf("Could not parse last modified timestamp (%s) of Lambda layer %s. Excluding from delete.", fnLastModified, *fnName)
return false
}

return configObj.LambdaLayer.ShouldInclude(config.ResourceValue{
Time: &lastModifiedDateTime,
Name: fnName,
})
}

func (ll *LambdaLayers) nukeAll(names []*string) error {
if len(names) == 0 {
logging.Logger.Debugf("No Lambda Layers to nuke in region %s", ll.Region)
return nil
}

logging.Logger.Debugf("Deleting all Lambda Layers in region %s", ll.Region)
deletedNames := []*string{}
deleteLayerVersions := []*lambda.DeleteLayerVersionInput{}

for _, name := range names {
err := ll.Client.ListLayerVersionsPages(
&lambda.ListLayerVersionsInput{
LayerName: name,
}, func(page *lambda.ListLayerVersionsOutput, lastPage bool) bool {
for _, version := range page.LayerVersions {
logging.Logger.Debugf("Found layer version! %s", version)
params := &lambda.DeleteLayerVersionInput{
LayerName: name,
VersionNumber: version.Version,
}
deleteLayerVersions = append(deleteLayerVersions, params)
}

return !lastPage
})

if err != nil {
return err
}
}

for _, params := range deleteLayerVersions {

_, err := ll.Client.DeleteLayerVersion(params)

if err != nil {
return err
}

// Record status of this resource
e := report.Entry{
Identifier: aws.StringValue(params.LayerName),
ResourceType: "Lambda layer",
Error: err,
}
report.Record(e)

if err != nil {
logging.Logger.Errorf("[Failed] %s: %s", *params.LayerName, err)
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Error Nuking Lambda Layer",
}, map[string]interface{}{
"region": ll.Region,
})
} else {
deletedNames = append(deletedNames, params.LayerName)
logging.Logger.Debugf("Deleted Lambda Layer: %s", awsgo.StringValue(params.LayerName))
}
}

logging.Logger.Debugf("[OK] %d Lambda Layer(s) deleted in %s", len(deletedNames), ll.Region)
return nil
}
126 changes: 126 additions & 0 deletions aws/resources/lambda_layer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package resources

import (
"context"
"regexp"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/telemetry"
"github.com/stretchr/testify/require"
)

type mockedLambdaLayer struct {
lambdaiface.LambdaAPI
ListLayersOutput lambda.ListLayersOutput
ListLayerVersionsOutput lambda.ListLayerVersionsOutput
DeleteLayerVersionOutput lambda.DeleteLayerVersionOutput
}

func (m mockedLambdaLayer) ListLayersPages(input *lambda.ListLayersInput, fn func(*lambda.ListLayersOutput, bool) bool) error {
fn(&m.ListLayersOutput, true)
return nil
}

func (m mockedLambdaLayer) ListLayerVersionsPages(input *lambda.ListLayerVersionsInput, fn func(*lambda.ListLayerVersionsOutput, bool) bool) error {
fn(&m.ListLayerVersionsOutput, true)
return nil
}

func TestLambdaLayer_GetAll(t *testing.T) {
telemetry.InitTelemetry("cloud-nuke", "")
t.Parallel()

testName1 := "test-lambda-layer1"
testName1Version1 := int64(1)

testName2 := "test-lambda-layer2"

testTime := time.Now()

layout := "2006-01-02T15:04:05.000+0000"
testTimeStr := "2023-07-28T12:34:56.789+0000"
testTime, err := time.Parse(layout, testTimeStr)
require.NoError(t, err)

ll := LambdaLayers{
Client: mockedLambdaLayer{
ListLayersOutput: lambda.ListLayersOutput{
Layers: []*lambda.LayersListItem{
{
LayerName: aws.String(testName1),
LatestMatchingVersion: &lambda.LayerVersionsListItem{
CreatedDate: aws.String(testTimeStr),
},
},
{
LayerName: aws.String(testName2),
LatestMatchingVersion: &lambda.LayerVersionsListItem{
CreatedDate: aws.String(testTimeStr),
},
},
},
},
ListLayerVersionsOutput: lambda.ListLayerVersionsOutput{
LayerVersions: []*lambda.LayerVersionsListItem{
{
Version: &testName1Version1,
},
},
},
},
}

tests := map[string]struct {
configObj config.ResourceType
expected []string
}{
"emptyFilter": {
configObj: config.ResourceType{},
expected: []string{testName1, testName2},
},
"nameExclusionFilter": {
configObj: config.ResourceType{
ExcludeRule: config.FilterRule{
NamesRegExp: []config.Expression{{
RE: *regexp.MustCompile(testName1),
}}},
},
expected: []string{testName2},
},
"timeAfterExclusionFilter": {
configObj: config.ResourceType{
ExcludeRule: config.FilterRule{
TimeAfter: aws.Time(testTime.Add(-2 * time.Hour)),
}},
expected: []string{},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
names, err := ll.getAll(context.Background(), config.Config{
LambdaLayer: tc.configObj,
})
require.NoError(t, err)
require.Equal(t, tc.expected, aws.StringValueSlice(names))
})
}
}

func TestLambdaLayer_NukeAll(t *testing.T) {
telemetry.InitTelemetry("cloud-nuke", "")
t.Parallel()

ll := LambdaLayers{
Client: mockedLambdaLayer{
DeleteLayerVersionOutput: lambda.DeleteLayerVersionOutput{},
},
}

err := ll.nukeAll([]*string{aws.String("test")})
require.NoError(t, err)
}
63 changes: 63 additions & 0 deletions aws/resources/lambda_layer_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package resources

import (
"context"

awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/go-commons/errors"
)

type LambdaLayers struct {
Client lambdaiface.LambdaAPI
Region string
LambdaFunctionNames []string
}

func (lf *LambdaLayers) Init(session *session.Session) {
lf.Client = lambda.New(session)
}

func (lf *LambdaLayers) ResourceName() string {
return "lambda_layer"
}

// ResourceIdentifiers - The names of the lambda functions
func (lf *LambdaLayers) ResourceIdentifiers() []string {
return lf.LambdaFunctionNames
}

func (lf *LambdaLayers) MaxBatchSize() int {
// Tentative batch size to ensure AWS doesn't throttle
return 49
}

func (lf *LambdaLayers) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
identifiers, err := lf.getAll(c, configObj)
if err != nil {
return nil, err
}

lf.LambdaFunctionNames = awsgo.StringValueSlice(identifiers)
return lf.LambdaFunctionNames, nil
}

// Nuke - nuke 'em all!!!
func (lf *LambdaLayers) Nuke(identifiers []string) error {
if err := lf.nukeAll(awsgo.StringSlice(identifiers)); err != nil {
return errors.WithStackTrace(err)
}

return nil
}

type LambdaVersionDeleteError struct {
name string
}

func (e LambdaVersionDeleteError) Error() string {
return "Lambda Function:" + e.name + "was not deleted"
}
Loading

0 comments on commit cf3cdee

Please sign in to comment.