@kennethwkz/serverless-prune-plugin 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +53 -0
- package/LICENSE.md +21 -0
- package/README.md +111 -0
- package/index.js +428 -0
- package/package.json +40 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Node.js CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ master ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v2
|
|
16
|
+
|
|
17
|
+
- name: Setup Node.js
|
|
18
|
+
uses: actions/setup-node@v2
|
|
19
|
+
with:
|
|
20
|
+
node-version: 16.x
|
|
21
|
+
cache: 'npm'
|
|
22
|
+
|
|
23
|
+
- name: Run eslint
|
|
24
|
+
run: |
|
|
25
|
+
npm ci
|
|
26
|
+
npm run lint
|
|
27
|
+
|
|
28
|
+
build:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
|
|
31
|
+
strategy:
|
|
32
|
+
matrix:
|
|
33
|
+
node-version: [10.x, 12.x, 14.x, 16.x]
|
|
34
|
+
|
|
35
|
+
steps:
|
|
36
|
+
- name: Checkout
|
|
37
|
+
uses: actions/checkout@v2
|
|
38
|
+
|
|
39
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
40
|
+
uses: actions/setup-node@v2
|
|
41
|
+
with:
|
|
42
|
+
node-version: ${{ matrix.node-version }}
|
|
43
|
+
cache: 'npm'
|
|
44
|
+
|
|
45
|
+
- name: Run tests
|
|
46
|
+
run: |
|
|
47
|
+
npm ci
|
|
48
|
+
npm run cover
|
|
49
|
+
|
|
50
|
+
- name: Publish coverage to Coveralls
|
|
51
|
+
uses: coverallsapp/github-action@master
|
|
52
|
+
with:
|
|
53
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Clay Gregory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
|
|
2
|
+
# Serverless Prune Plugin
|
|
3
|
+
|
|
4
|
+
Following deployment, the Serverless Framework does not purge previous versions of functions from AWS, so the number of deployed versions can grow out of hand rather quickly. This plugin allows pruning of all but the most recent version(s) of managed functions from AWS. This plugin is compatible with Serverless 1.x and higher.
|
|
5
|
+
|
|
6
|
+
[](http://www.serverless.com)
|
|
7
|
+
[](https://coveralls.io/github/claygregory/serverless-prune-plugin?branch=master)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install with **npm**:
|
|
12
|
+
```sh
|
|
13
|
+
npm install --save-dev serverless-prune-plugin
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
And then add the plugin to your `serverless.yml` file:
|
|
17
|
+
```yaml
|
|
18
|
+
plugins:
|
|
19
|
+
- serverless-prune-plugin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Alternatively, install with the Serverless **plugin command** (Serverless Framework 1.22 or higher):
|
|
23
|
+
```sh
|
|
24
|
+
sls plugin install -n serverless-prune-plugin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
In the project root, run:
|
|
30
|
+
```sh
|
|
31
|
+
sls prune -n <number of version to keep>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This will delete all but the `n`-most recent versions of each function deployed. Versions referenced by an alias are automatically preserved.
|
|
35
|
+
|
|
36
|
+
### Single Function
|
|
37
|
+
|
|
38
|
+
A single function can be targeted for cleanup:
|
|
39
|
+
```sh
|
|
40
|
+
sls prune -n <number of version to keep> -f helloWorld
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Region/Stage
|
|
44
|
+
|
|
45
|
+
The previous usage examples prune the default stage in the default region. Use `--stage` and `--region` to specify:
|
|
46
|
+
```sh
|
|
47
|
+
sls prune -n <number of version to keep> --stage production --region eu-central-1
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Automatic Pruning
|
|
51
|
+
|
|
52
|
+
This plugin can also be configured to run automatically, following a deployment. Configuration of automatic pruning is within the `custom` property of `serverless.yml`. For example:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
custom:
|
|
56
|
+
prune:
|
|
57
|
+
automatic: true
|
|
58
|
+
number: 3
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
To run automatically, the `automatic` property of `prune` must be set to `true` and the `number` of versions to keep must be specified.
|
|
62
|
+
It is possible to set `number` to `0`. In this case, the plugin will delete all the function versions (except $LATEST); this is useful when disabling function versioning for an already-deployed stack.
|
|
63
|
+
|
|
64
|
+
### Layers
|
|
65
|
+
|
|
66
|
+
This plugin can also prune Lambda Layers in the same manner that it prunes functions. You can specify a Lambda Layer, or add the flag, `includeLayers`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
custom:
|
|
70
|
+
prune:
|
|
71
|
+
automatic: true
|
|
72
|
+
includeLayers: true
|
|
73
|
+
number: 3
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Dry Run
|
|
77
|
+
|
|
78
|
+
A dry-run will preview the deletion candidates, without actually performing the pruning operations:
|
|
79
|
+
```sh
|
|
80
|
+
sls prune -n <number of version to keep> --dryRun
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Additional Help
|
|
84
|
+
|
|
85
|
+
See:
|
|
86
|
+
```sh
|
|
87
|
+
sls prune --help
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Permissions Required
|
|
91
|
+
|
|
92
|
+
To run this plugin, the user will need to be allowed the following permissions in AWS:
|
|
93
|
+
- `lambda:listAliases`
|
|
94
|
+
- `lambda:listVersionsByFunction`
|
|
95
|
+
- `lambda:deleteFunction`
|
|
96
|
+
- `lambda:listLayerVersions`
|
|
97
|
+
- `lambda:deleteLayerVersion`
|
|
98
|
+
|
|
99
|
+
## Common Questions
|
|
100
|
+
|
|
101
|
+
**How do I set up different pruning configurations per region/stage?**
|
|
102
|
+
|
|
103
|
+
Several suggestions are available in [this thread](https://github.com/claygregory/serverless-prune-plugin/issues/21#issuecomment-622651886).
|
|
104
|
+
|
|
105
|
+
**Can I just disable versioning entirely?**
|
|
106
|
+
|
|
107
|
+
Absolutely. While Serverless Framework has it enabled by default, [versioning can be disabled](https://www.serverless.com/framework/docs/providers/aws/guide/functions/#versioning-deployed-functions).
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
Copyright (c) 2017 [Clay Gregory](https://claygregory.com). See the included [LICENSE](LICENSE.md) for rights and limitations under the terms of the MIT license.
|
package/index.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LambdaClient } = require('@aws-sdk/client-lambda');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Run an async mapper over `items` with a bounded concurrency limit.
|
|
7
|
+
* Preserves input order in the resolved array. Native Promise only —
|
|
8
|
+
* keeps AWS Lambda API call rate within safe limits while still pruning
|
|
9
|
+
* many versions in parallel instead of one-at-a-time.
|
|
10
|
+
*/
|
|
11
|
+
async function pMap(items, mapper, concurrency = 5) {
|
|
12
|
+
const results = new Array(items.length);
|
|
13
|
+
let cursor = 0;
|
|
14
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
15
|
+
const workers = new Array(workerCount).fill(0).map(async () => {
|
|
16
|
+
while (true) {
|
|
17
|
+
const index = cursor;
|
|
18
|
+
cursor += 1;
|
|
19
|
+
if (index >= items.length) return;
|
|
20
|
+
results[index] = await mapper(items[index], index);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
await Promise.all(workers);
|
|
24
|
+
return results;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Accept a "deployment skipped" flag regardless of whether it arrives as a
|
|
29
|
+
* boolean (option typed by the framework) or as a string from the CLI.
|
|
30
|
+
*/
|
|
31
|
+
function isTruthyFlag(value) {
|
|
32
|
+
return value === true || value === 'true' || value === 1 || value === '1';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Invoke a Lambda AWS API method in a framework-aware way.
|
|
37
|
+
* osls v4 removed provider.request() (throws AWS_SDK_V2_SURFACE_REMOVED);
|
|
38
|
+
* there a LambdaClient is built from provider.getAwsSdkV3Config() and the
|
|
39
|
+
* matching <Method>Command is sent. On serverless v3 the original
|
|
40
|
+
* provider.request() surface is kept as-is. LambdaClients are cached per
|
|
41
|
+
* provider via a WeakMap.
|
|
42
|
+
*/
|
|
43
|
+
const lambdaClientPromises = new WeakMap();
|
|
44
|
+
function lambdaRequest(provider, action, params) {
|
|
45
|
+
if (typeof provider.getAwsSdkV3Config !== 'function') {
|
|
46
|
+
return provider.request('Lambda', action, params);
|
|
47
|
+
}
|
|
48
|
+
let clientPromise = lambdaClientPromises.get(provider);
|
|
49
|
+
if (!clientPromise) {
|
|
50
|
+
clientPromise = provider.getAwsSdkV3Config().then((cfg) => new LambdaClient(cfg));
|
|
51
|
+
lambdaClientPromises.set(provider, clientPromise);
|
|
52
|
+
}
|
|
53
|
+
return clientPromise.then((client) => {
|
|
54
|
+
const { [`${action}Command`]: Command } = require('@aws-sdk/client-lambda');
|
|
55
|
+
return client.send(new Command(params));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class Prune {
|
|
60
|
+
constructor(serverless, options, { log, progress } = {}) {
|
|
61
|
+
this.serverless = serverless;
|
|
62
|
+
this.options = options || {};
|
|
63
|
+
this.provider = this.serverless.getProvider('aws');
|
|
64
|
+
this.log = log || serverless.cli.log.bind(serverless.cli);
|
|
65
|
+
this.progress = progress;
|
|
66
|
+
|
|
67
|
+
this.pluginCustom = this.loadCustom(this.serverless.service.custom);
|
|
68
|
+
|
|
69
|
+
this.commands = {
|
|
70
|
+
prune: {
|
|
71
|
+
usage: 'Clean up deployed functions and/or layers by deleting older versions.',
|
|
72
|
+
lifecycleEvents: ['prune'],
|
|
73
|
+
options: {
|
|
74
|
+
number: {
|
|
75
|
+
usage: 'Number of previous versions to keep',
|
|
76
|
+
shortcut: 'n',
|
|
77
|
+
required: true,
|
|
78
|
+
type: 'string'
|
|
79
|
+
},
|
|
80
|
+
stage: {
|
|
81
|
+
usage: 'Stage of the service',
|
|
82
|
+
shortcut: 's',
|
|
83
|
+
type: 'string'
|
|
84
|
+
},
|
|
85
|
+
region: {
|
|
86
|
+
usage: 'Region of the service',
|
|
87
|
+
shortcut: 'r',
|
|
88
|
+
type: 'string'
|
|
89
|
+
},
|
|
90
|
+
function: {
|
|
91
|
+
usage: 'Function name. Limits cleanup to the specified function',
|
|
92
|
+
shortcut: 'f',
|
|
93
|
+
required: false,
|
|
94
|
+
type: 'string'
|
|
95
|
+
},
|
|
96
|
+
layer: {
|
|
97
|
+
usage: 'Layer name. Limits cleanup to the specified Lambda layer',
|
|
98
|
+
shortcut: 'l',
|
|
99
|
+
required: false,
|
|
100
|
+
type: 'string'
|
|
101
|
+
},
|
|
102
|
+
includeLayers: {
|
|
103
|
+
usage: 'Boolean flag. Includes the pruning of Lambda layers.',
|
|
104
|
+
shortcut: 'i',
|
|
105
|
+
required: false,
|
|
106
|
+
type: 'boolean'
|
|
107
|
+
},
|
|
108
|
+
dryRun: {
|
|
109
|
+
usage: 'Simulate pruning without executing delete actions. Deletion candidates are logged when used in conjunction with --verbose',
|
|
110
|
+
shortcut: 'd',
|
|
111
|
+
required: false,
|
|
112
|
+
type: 'boolean'
|
|
113
|
+
},
|
|
114
|
+
verbose: {
|
|
115
|
+
usage: 'Enable detailed output during plugin execution',
|
|
116
|
+
required: false,
|
|
117
|
+
type: 'boolean'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.hooks = {
|
|
124
|
+
'prune:prune': this.cliPrune.bind(this),
|
|
125
|
+
'after:deploy:deploy': this.postDeploy.bind(this)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getNumber() {
|
|
130
|
+
return this.options.number || this.pluginCustom.number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
loadCustom(custom) {
|
|
134
|
+
const pluginCustom = {};
|
|
135
|
+
if (custom && custom.prune) {
|
|
136
|
+
|
|
137
|
+
if (custom.prune.number != null) {
|
|
138
|
+
const number = parseInt(custom.prune.number);
|
|
139
|
+
if (!isNaN(number)) pluginCustom.number = number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof custom.prune.automatic === 'boolean') {
|
|
143
|
+
pluginCustom.automatic = custom.prune.automatic;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof custom.prune.includeLayers === 'boolean') {
|
|
147
|
+
pluginCustom.includeLayers = custom.prune.includeLayers;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return pluginCustom;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
cliPrune() {
|
|
155
|
+
if (this.options.dryRun) {
|
|
156
|
+
this.logNotice('Dry-run enabled, no pruning actions will be performed.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if(this.options.includeLayers) {
|
|
160
|
+
return Promise.all([
|
|
161
|
+
this.pruneFunctions(),
|
|
162
|
+
this.pruneLayers()
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.options.layer && !this.options.function) {
|
|
167
|
+
return this.pruneLayers();
|
|
168
|
+
} else {
|
|
169
|
+
return this.pruneFunctions();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
postDeploy() {
|
|
174
|
+
this.pluginCustom = this.loadCustom(this.serverless.service.custom);
|
|
175
|
+
|
|
176
|
+
if (isTruthyFlag(this.options.noDeploy)) {
|
|
177
|
+
// Deployment was skipped — do not prune.
|
|
178
|
+
this.logNotice('Deployment skipped (noDeploy). Skipping prune.');
|
|
179
|
+
return Promise.resolve();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.pluginCustom.automatic &&
|
|
183
|
+
this.pluginCustom.number !== undefined && this.pluginCustom.number >= 0) {
|
|
184
|
+
|
|
185
|
+
if(this.pluginCustom.includeLayers) {
|
|
186
|
+
return Promise.all([
|
|
187
|
+
this.pruneFunctions(),
|
|
188
|
+
this.pruneLayers()
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return this.pruneFunctions();
|
|
193
|
+
} else {
|
|
194
|
+
return Promise.resolve();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async pruneLayers() {
|
|
199
|
+
const selectedLayers = this.options.layer ? [this.options.layer] : this.serverless.service.getAllLayers();
|
|
200
|
+
const layerNames = selectedLayers.map(key => this.serverless.service.getLayer(key).name || key);
|
|
201
|
+
|
|
202
|
+
this.createProgress(
|
|
203
|
+
'prune-plugin-prune-layers',
|
|
204
|
+
'Pruning layer versions'
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
for (const layerName of layerNames) {
|
|
208
|
+
const versions = await this.listVersionsForLayer(layerName);
|
|
209
|
+
if (!versions.length) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const deletionCandidates = this.selectPruneVersionsForLayer(versions);
|
|
214
|
+
if (deletionCandidates.length > 0) {
|
|
215
|
+
this.updateProgress('prune-plugin-prune-layers', `Pruning layer versions (${layerName})`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (this.options.dryRun) {
|
|
219
|
+
this.printPruningCandidates(layerName, deletionCandidates);
|
|
220
|
+
} else {
|
|
221
|
+
await this.deleteVersionsForLayer(layerName, deletionCandidates);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.clearProgress('prune-plugin-prune-layers');
|
|
226
|
+
this.logSuccess('Pruning of layers complete');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async pruneFunctions() {
|
|
230
|
+
const selectedFunctions = this.options.function ? [this.options.function] : this.serverless.service.getAllFunctions();
|
|
231
|
+
const functionNames = selectedFunctions.map(key => this.serverless.service.getFunction(key).name);
|
|
232
|
+
|
|
233
|
+
this.createProgress(
|
|
234
|
+
'prune-plugin-prune-functions',
|
|
235
|
+
'Pruning function versions'
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
for (const functionName of functionNames) {
|
|
239
|
+
const [versions, aliases] = await Promise.all([
|
|
240
|
+
this.listVersionForFunction(functionName),
|
|
241
|
+
this.listAliasesForFunction(functionName)
|
|
242
|
+
]);
|
|
243
|
+
if (!versions.length) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const deletionCandidates = this.selectPruneVersionsForFunction(versions, aliases);
|
|
248
|
+
if (deletionCandidates.length > 0) {
|
|
249
|
+
this.updateProgress('prune-plugin-prune-functions', `Pruning function versions (${functionName})`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (this.options.dryRun) {
|
|
253
|
+
this.printPruningCandidates(functionName, deletionCandidates);
|
|
254
|
+
} else {
|
|
255
|
+
await this.deleteVersionsForFunction(functionName, deletionCandidates);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.clearProgress('prune-plugin-prune-functions');
|
|
260
|
+
this.logSuccess('Pruning of functions complete');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
deleteVersionsForLayer(layerName, versions) {
|
|
264
|
+
return pMap(versions, async (version) => {
|
|
265
|
+
this.logInfo(`Deleting layer version ${layerName}:${version}.`);
|
|
266
|
+
const params = {
|
|
267
|
+
LayerName: layerName,
|
|
268
|
+
VersionNumber: version
|
|
269
|
+
};
|
|
270
|
+
await lambdaRequest(this.provider, 'deleteLayerVersion', params);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
deleteVersionsForFunction(functionName, versions) {
|
|
275
|
+
return pMap(versions, async (version) => {
|
|
276
|
+
this.logInfo(`Deleting function version ${functionName}:${version}.`);
|
|
277
|
+
const params = {
|
|
278
|
+
FunctionName: functionName,
|
|
279
|
+
Qualifier: version
|
|
280
|
+
};
|
|
281
|
+
try {
|
|
282
|
+
await lambdaRequest(this.provider, 'deleteFunction', params);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
//ignore if trying to delete replicated lambda edge function.
|
|
285
|
+
//Works for serverless v3 (provider.request: e.providerError) and
|
|
286
|
+
//osls v4 / AWS SDK v3 (e.$metadata + e.message).
|
|
287
|
+
const httpStatus = (e.providerError && e.providerError.statusCode) || (e.$metadata && e.$metadata.httpStatusCode);
|
|
288
|
+
const message = (e.providerError && e.providerError.message) || e.message;
|
|
289
|
+
if (httpStatus === 400
|
|
290
|
+
&& message
|
|
291
|
+
&& message.startsWith('Lambda was unable to delete')
|
|
292
|
+
&& message.indexOf('because it is a replicated function.') > -1) {
|
|
293
|
+
this.logWarning(`Unable to delete replicated Lambda@Edge function version ${functionName}:${version}.`);
|
|
294
|
+
} else {
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
listAliasesForFunction(functionName) {
|
|
302
|
+
const params = {
|
|
303
|
+
FunctionName: functionName
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return this.makeLambdaRequest('listAliases', params, r => r.Aliases)
|
|
307
|
+
.catch(e => {
|
|
308
|
+
//ignore if function not deployed
|
|
309
|
+
if (e.providerError && e.providerError.statusCode === 404) return [];
|
|
310
|
+
else throw e;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
listVersionForFunction(functionName) {
|
|
315
|
+
const params = {
|
|
316
|
+
FunctionName: functionName
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return this.makeLambdaRequest('listVersionsByFunction', params, r => r.Versions)
|
|
320
|
+
.catch(e => {
|
|
321
|
+
//ignore if function not deployed
|
|
322
|
+
if (e.providerError && e.providerError.statusCode === 404) return [];
|
|
323
|
+
else throw e;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
listVersionsForLayer(layerName) {
|
|
328
|
+
const params = {
|
|
329
|
+
LayerName: layerName
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return this.makeLambdaRequest('listLayerVersions', params, r => r.LayerVersions)
|
|
333
|
+
.catch(e => {
|
|
334
|
+
// ignore if layer not deployed
|
|
335
|
+
if (e.providerError && e.providerError.statusCode === 404) return [];
|
|
336
|
+
else throw e;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
makeLambdaRequest(action, params, responseMapping) {
|
|
342
|
+
const results = [];
|
|
343
|
+
const responseHandler = response => {
|
|
344
|
+
Array.prototype.push.apply(results, responseMapping(response));
|
|
345
|
+
|
|
346
|
+
if (response.NextMarker) {
|
|
347
|
+
return lambdaRequest(this.provider,action, Object.assign({}, params, { Marker: response.NextMarker }))
|
|
348
|
+
.then(responseHandler);
|
|
349
|
+
} else {
|
|
350
|
+
return Promise.resolve(results);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return lambdaRequest(this.provider,action, params)
|
|
355
|
+
.then(responseHandler);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
selectPruneVersionsForFunction(versions, aliases) {
|
|
359
|
+
const aliasedVersion = aliases.map(a => a.FunctionVersion);
|
|
360
|
+
|
|
361
|
+
return versions
|
|
362
|
+
.map(f => f.Version)
|
|
363
|
+
.filter(v => v !== '$LATEST') //skip $LATEST
|
|
364
|
+
.filter(v => aliasedVersion.indexOf(v) === -1) //skip aliased versions
|
|
365
|
+
.sort((a, b) => parseInt(a) === parseInt(b) ? 0 : parseInt(a) > parseInt(b) ? -1 : 1)
|
|
366
|
+
.slice(this.getNumber());
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
selectPruneVersionsForLayer(versions) {
|
|
370
|
+
return versions
|
|
371
|
+
.map(f => f.Version)
|
|
372
|
+
.sort((a, b) => parseInt(a) === parseInt(b) ? 0 : parseInt(a) > parseInt(b) ? -1 : 1)
|
|
373
|
+
.slice(this.getNumber());
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
printPruningCandidates(name, deletionCandidates) {
|
|
377
|
+
deletionCandidates.forEach(version => this.logInfo(`${name}:${version} selected for deletion.`));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// -- Compatibility with both Framework 2.x and 3.x logging ---
|
|
381
|
+
|
|
382
|
+
logInfo(message) {
|
|
383
|
+
if (this.log.info) this.log.info(message);
|
|
384
|
+
else this.log(`Prune: ${message}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
logNotice(message) {
|
|
388
|
+
if (this.log.notice) this.log.notice(message);
|
|
389
|
+
else this.log(`Prune: ${message}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
logWarning(message) {
|
|
393
|
+
if (this.log.warning) this.log.warning(message);
|
|
394
|
+
else this.log(`Prune: ${message}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
logSuccess(message) {
|
|
398
|
+
if (this.log.success) this.log.success(message);
|
|
399
|
+
else this.log(`Prune: ${message}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
createProgress(name, message) {
|
|
403
|
+
if (!this.progress) {
|
|
404
|
+
this.log(`Prune: ${message}...`);
|
|
405
|
+
} else {
|
|
406
|
+
this.progress.create({
|
|
407
|
+
message,
|
|
408
|
+
name
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
updateProgress(name, message) {
|
|
414
|
+
if (!this.progress) {
|
|
415
|
+
this.log(`Prune: ${message}`);
|
|
416
|
+
} else {
|
|
417
|
+
this.progress.get(name).update(message);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
clearProgress(name) {
|
|
422
|
+
if (this.progress) {
|
|
423
|
+
this.progress.get(name).remove();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = Prune;
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kennethwkz/serverless-prune-plugin",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Serverless plugin to delete old versions of deployed functions from AWS",
|
|
5
|
+
"author": "Clay Gregory <clay@claygregory.com> (https://claygregory.com/)",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"serverless",
|
|
11
|
+
"serverless-plugin",
|
|
12
|
+
"aws",
|
|
13
|
+
"aws-lambda",
|
|
14
|
+
"lambda"
|
|
15
|
+
],
|
|
16
|
+
"repository": "KennethWKZ/serverless-prune-plugin",
|
|
17
|
+
"homepage": "https://github.com/KennethWKZ/serverless-prune-plugin",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"main": "index.js",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint": "eslint ./index.js",
|
|
22
|
+
"test": "mocha",
|
|
23
|
+
"cover": "istanbul cover _mocha",
|
|
24
|
+
"coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@aws-sdk/client-lambda": "^3.1079.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"coveralls": "^3.1.1",
|
|
31
|
+
"eslint": "^8.2.0",
|
|
32
|
+
"istanbul": "^0.4.5",
|
|
33
|
+
"mocha": "^9.1.3",
|
|
34
|
+
"mocha-lcov-reporter": "^1.3.0",
|
|
35
|
+
"sinon": "^12.0.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"serverless": "1.x || 2.x || 3.x || 4.x"
|
|
39
|
+
}
|
|
40
|
+
}
|