Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add datasource for AWS Lambda Layers #19081

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1e07577
add dummy datasource description for draft pull request only
kayman-mk Nov 22, 2022
fa638fe
base implementation
kayman-mk Nov 24, 2022
82652f3
add the code
kayman-mk Nov 24, 2022
7a9a5d0
Merge branch 'main' into kayma/add-aws-versioned-arn-datasource
kayman-mk Nov 29, 2022
9edb2e0
retrieve layer list from AWS
kayman-mk Nov 29, 2022
a6168f2
add tests
kayman-mk Nov 29, 2022
f25dad7
Merge branch 'main' into kayma/add-aws-versioned-arn-datasource
kayman-mk Nov 30, 2022
82f8849
Update lib/modules/datasource/aws-versioned-arn/readme.md
kayman-mk Jan 7, 2023
e51647a
reset package manager
kayman-mk Jan 7, 2023
5978e15
Merge remote-tracking branch 'origin/main' into kayma/add-aws-version…
kayman-mk Jan 7, 2023
3ca698d
add aws-sdk lambda
kayman-mk Jan 7, 2023
7b5ed71
add datasource to API
kayman-mk Jan 7, 2023
1162613
add docs
kayman-mk Jan 7, 2023
c2ffdc5
files lost by accident
kayman-mk Jan 8, 2023
d2b2d07
Merge branch 'main' into kayma/add-aws-versioned-arn-datasource
kayman-mk Jan 8, 2023
87d1fa0
files lost by accident
kayman-mk Jan 8, 2023
9230bb9
Merge branch 'main' into kayma/add-aws-versioned-arn-datasource
kayman-mk Jan 9, 2023
4999cc2
Merge branch 'kayma/add-aws-versioned-arn-datasource' of github.com:H…
kayman-mk Jan 14, 2023
4f24402
rename files
kayman-mk Jan 14, 2023
1209738
add tests
kayman-mk Jan 14, 2023
e2cfb26
add tests
kayman-mk Jan 14, 2023
5e3dad2
add tests
kayman-mk Jan 15, 2023
6c4a7e0
Merge branch 'main' into kayma/add-aws-versioned-arn-datasource
kayman-mk Jan 15, 2023
ad2bbbc
fix tests and config
kayman-mk Jan 15, 2023
1dda08d
remove sorting
kayman-mk Jan 15, 2023
2f02403
Merge branch 'main' into kayma/add-aws-versioned-arn-datasource
kayman-mk Jan 18, 2023
10fbbcd
Merge branch 'main' into kayma/add-aws-versioned-arn-datasource
kayman-mk Jan 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/modules/datasource/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AdoptiumJavaDatasource } from './adoptium-java';
import { ArtifactoryDatasource } from './artifactory';
import { AwsLambdaLayerDataSource } from './aws-lambda-layer';
import { AwsMachineImageDataSource } from './aws-machine-image';
import { AwsRdsDataSource } from './aws-rds';
import { AzurePipelinesTasksDatasource } from './azure-pipelines-tasks';
Expand Down Expand Up @@ -59,6 +60,7 @@ api.set(AdoptiumJavaDatasource.id, new AdoptiumJavaDatasource());
api.set(ArtifactoryDatasource.id, new ArtifactoryDatasource());
api.set(AwsMachineImageDataSource.id, new AwsMachineImageDataSource());
api.set(AwsRdsDataSource.id, new AwsRdsDataSource());
api.set(AwsLambdaLayerDataSource.id, new AwsLambdaLayerDataSource());
api.set(AzurePipelinesTasksDatasource.id, new AzurePipelinesTasksDatasource());
api.set(BitBucketTagsDatasource.id, new BitBucketTagsDatasource());
api.set(CdnJsDatasource.id, new CdnJsDatasource());
Expand Down
178 changes: 178 additions & 0 deletions lib/modules/datasource/aws-lambda-layer/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
LambdaClient,
LayerVersionsListItem,
ListLayerVersionsCommand,
ListLayerVersionsCommandOutput,
} from '@aws-sdk/client-lambda';
import { mockClient } from 'aws-sdk-client-mock';
import { getPkgReleases } from '..';
import { AwsLambdaLayerDataSource, AwsLambdaLayerFilter } from './index';

const datasource = AwsLambdaLayerDataSource.id;

/**
* Testdata for mock implementation of LambdaClient
* layer1 to layer3 from oldest to newest
*/
const layer1: LayerVersionsListItem = {
Version: 1,
CreatedDate: '2021-01-10T00:00:00.000Z',
LayerVersionArn: 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:3',
};

const layer2: LayerVersionsListItem = {
Version: 2,
CreatedDate: '2021-02-05T00:00:00.000Z',
LayerVersionArn: 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:2',
};

const layer3: LayerVersionsListItem = {
Version: 3,
CreatedDate: '2021-03-01T00:00:00.000Z',
LayerVersionArn: 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:1',
};

const mock3Layers: ListLayerVersionsCommandOutput = {
LayerVersions: [layer1, layer2, layer3],
$metadata: {},
};

const mock1Layer: ListLayerVersionsCommandOutput = {
LayerVersions: [layer3],
$metadata: {},
};

const mockEmpty: ListLayerVersionsCommandOutput = {
LayerVersions: [],
$metadata: {},
};

const lambdaFilter: AwsLambdaLayerFilter = {
name: '',
architecture: '',
runtime: '',
};

const lambdaClientMock = mockClient(LambdaClient);

function mockListLayerVersionsCommandOutput(
result: ListLayerVersionsCommandOutput
): void {
lambdaClientMock.reset();
lambdaClientMock.on(ListLayerVersionsCommand).resolves(result);
}

describe('modules/datasource/aws-lambda-layer/index', () => {
describe('getSortedLambdaLayerVersions', () => {
it('should return empty array if no layers found', async () => {
mockListLayerVersionsCommandOutput(mockEmpty);
const lamdbaLayerDatasource = new AwsLambdaLayerDataSource();
const res = await lamdbaLayerDatasource.getSortedLambdaLayerVersions(
lambdaFilter
);

expect(res).toEqual([]);
});

it('should return array with one layer if one layer found', async () => {
mockListLayerVersionsCommandOutput(mock1Layer);
const lamdbaLayerDatasource = new AwsLambdaLayerDataSource();
const res = await lamdbaLayerDatasource.getSortedLambdaLayerVersions(
lambdaFilter
);

expect(res).toEqual([layer3]);
});

it('should return array with three layers if three layers found', async () => {
mockListLayerVersionsCommandOutput(mock3Layers);
const lamdbaLayerDatasource = new AwsLambdaLayerDataSource();
const res = await lamdbaLayerDatasource.getSortedLambdaLayerVersions(
lambdaFilter
);

expect(res).toEqual([layer1, layer2, layer3]);
});

it('should have the filters for listLayerVersions set calling the AWS API', async () => {
mockListLayerVersionsCommandOutput(mock3Layers);
const lambdaLayerDatasource = new AwsLambdaLayerDataSource();

await lambdaLayerDatasource.getSortedLambdaLayerVersions({
name: 'arn',
runtime: 'runtime',
architecture: 'architecture',
});

expect(lambdaClientMock.calls()).toHaveLength(1);

expect(lambdaClientMock.calls()[0].args[0].input).toEqual({
CompatibleArchitecture: 'architecture',
CompatibleRuntime: 'runtime',
LayerName: 'arn',
});
});
});

describe('integration', () => {
describe('getPkgReleases', () => {
it('should return null if no releases found', async () => {
mockListLayerVersionsCommandOutput(mockEmpty);

const res = await getPkgReleases({
datasource,
depName:
'{"arn": "arn:aws:lambda:us-east-1:123456789012:layer:my-layer", "runtime": "python37", "architecture": "x86_64"}',
});

expect(res).toBeNull();
});

it('should return one image', async () => {
mockListLayerVersionsCommandOutput(mock1Layer);

const res = await getPkgReleases({
datasource,
depName:
'{"arn": "arn:aws:lambda:us-east-1:123456789012:layer:my-layer", "runtime": "python37", "architecture": "x86_64"}',
});

expect(res).toStrictEqual({
releases: [
{
isDeprecated: false,
version: layer1.Version,
},
],
});
});

it('should return 3 images', async () => {
mockListLayerVersionsCommandOutput(mock3Layers);

const res = await getPkgReleases({
datasource,
depName:
'{"arn": "arn:aws:lambda:us-east-1:123456789012:layer:my-layer", "runtime": "python37", "architecture": "x86_64"}',
});

expect(res).toStrictEqual({
releases: [
{
isDeprecated: false,
version: layer1.Version,
},
{
isDeprecated: false,
version: layer2.Version,
},
{
isDeprecated: false,
version: layer3.Version,
},
],
});
});
});
});
});
74 changes: 74 additions & 0 deletions lib/modules/datasource/aws-lambda-layer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
LambdaClient,
LayerVersionsListItem,
ListLayerVersionsCommand,
} from '@aws-sdk/client-lambda';
import { cache } from '../../../util/cache/package/decorator';
import { Lazy } from '../../../util/lazy';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';

export interface AwsLambdaLayerFilter {
name: string;
runtime: string;
architecture: string;
}

export class AwsLambdaLayerDataSource extends Datasource {
static readonly id = 'aws-lambda-layer';

override readonly caching = true;

private readonly lambda: Lazy<LambdaClient>;

constructor() {
super(AwsLambdaLayerDataSource.id);
this.lambda = new Lazy(() => new LambdaClient({}));
}

@cache({
namespace: `datasource-${AwsLambdaLayerDataSource.id}`,
key: (serializedLayerFilter: string) =>
`getSortedLambdaLayerVersions:${serializedLayerFilter}`,
})
async getSortedLambdaLayerVersions(
filter: AwsLambdaLayerFilter
): Promise<LayerVersionsListItem[]> {
const cmd = new ListLayerVersionsCommand({
LayerName: filter.name,
CompatibleArchitecture: filter.architecture,
CompatibleRuntime: filter.runtime,
});

const matchingLayerVersions = await this.lambda.getValue().send(cmd);

return matchingLayerVersions.LayerVersions ?? [];
}

@cache({
namespace: `datasource-${AwsLambdaLayerDataSource.id}`,
key: ({ packageName }: GetReleasesConfig) => `getReleases:${packageName}`,
})
async getReleases({
packageName: serializedLambdaLayerFilter,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
const filter: AwsLambdaLayerFilter = JSON.parse(
serializedLambdaLayerFilter
);

const lambdaLayerVersions = await this.getSortedLambdaLayerVersions(filter);

if (lambdaLayerVersions.length === 0) {
return null;
}

return {
releases: lambdaLayerVersions.map((layer) => ({
version: layer.Version?.toString() ?? '0',
releaseTimestamp: layer.CreatedDate,
newDigest: layer.LayerVersionArn,
isDeprecated: false,
})),
};
}
}
62 changes: 62 additions & 0 deletions lib/modules/datasource/aws-lambda-layer/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
This datasource returns releases from a versioned AWS ARN.

It currently supports Lambda Layer ARNs only.

**AWS API configuration**

Since the datasource uses the AWS SDK for JavaScript, you can configure it like other AWS Tools.
You can use common AWS configuration options, for example:

- Set the region via the `AWS_REGION` environment variable or your `~/.aws/config` file
- Provide credentials via the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables or your `~/.aws/credentials` file
- Select the profile to use via `AWS_PROFILE` environment variable

Read the [AWS Developer Guide - Configuring the SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/configuring-the-jssdk.html) for more information on these configuration options.

The minimal IAM privileges required for this datasource are:

```json
{
"Sid": "AllowLambdaLayerVersionLookup",
"Effect": "Allow",
"Action": ["lambda:ListLayerVersion"],
"Resource": "*"
}
```

Read the [AWS Lambda IAM reference](https://docs.aws.amazon.com/service-authorization/latest/reference/list_awslambda.html) for more information.

**Usage**

Because Renovate has no manager for the AWS Lambda Layer datasource, you need to help Renovate by configuring the regex manager to identify the layer dependencies you want updated.

Here's an example of using the regex manager to configure this datasource:

```json
{
"regexManagers": [
{
"fileMatch": ["\\.tf$"],
"matchStrings": [
".*renovate: datasource=(?<datasource>.*) filter=(?<packageName>.*)\\s+.* = \"(?<depName>.*):(?<currentValue>\\d+)\""
],
"versioningTemplate": "loose"
}
]
}
```

The configuration above matches every Terraform file, and recognizes these line:

```hcl
locals {
# renovate: datasource=aws-lambda-layer filter={"name": "my-layer", "architecture": "x86_64", "runtime": "python3.7"}
my_layer_arn = "arn:aws:lambda:us-east-1:123456789012:layer:my-layer:21"
}

resource "aws_lambda_function" "example" {
# ... other configuration ...

layers = [local.my_layer_arn]
}
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"@aws-sdk/client-ec2": "3.226.0",
"@aws-sdk/client-ecr": "3.226.0",
"@aws-sdk/client-iam": "3.226.0",
"@aws-sdk/client-lambda": "3.245.0",
"@aws-sdk/client-rds": "3.226.0",
"@aws-sdk/client-s3": "3.226.0",
"@breejs/later": "4.1.0",
Expand Down
Loading