-
-
Notifications
You must be signed in to change notification settings - Fork 356
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WIP] Feature: Support Nuking Lambda Layers/Versions (#605)
* 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
1 parent
5e1c221
commit cf3cdee
Showing
10 changed files
with
357 additions
and
6 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,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 | ||
} |
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,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) | ||
} |
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,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" | ||
} |
Oops, something went wrong.