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: implement aws-eks-addon datasource #29613

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
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,4 +1,5 @@
import { ArtifactoryDatasource } from './artifactory';
import { AwsEKSAddonDataSource } from './aws-eks-addon';
import { AwsMachineImageDataSource } from './aws-machine-image';
import { AwsRdsDataSource } from './aws-rds';
import { AzureBicepResourceDatasource } from './azure-bicep-resource';
Expand Down Expand Up @@ -66,6 +67,7 @@ const api = new Map<string, DatasourceApi>();
export default api;

api.set(ArtifactoryDatasource.id, new ArtifactoryDatasource());
api.set(AwsEKSAddonDataSource.id, new AwsEKSAddonDataSource());
api.set(AwsMachineImageDataSource.id, new AwsMachineImageDataSource());
api.set(AwsRdsDataSource.id, new AwsRdsDataSource());
api.set(AzureBicepResourceDatasource.id, new AzureBicepResourceDatasource());
Expand Down
153 changes: 153 additions & 0 deletions lib/modules/datasource/aws-eks-addon/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
type AddonInfo,
type AddonVersionInfo,
DescribeAddonVersionsCommand,
DescribeAddonVersionsResponse,
EKSClient,
} from '@aws-sdk/client-eks';
import { mockClient } from 'aws-sdk-client-mock';
import { getPkgReleases } from '..';
import { AwsEKSAddonDataSource } from '.';

const datasource = AwsEKSAddonDataSource.id;
const eksMock = mockClient(EKSClient);

const addonVersion1: AddonVersionInfo = {
jzhn marked this conversation as resolved.
Show resolved Hide resolved
addonVersion: 'v1.18.1-eksbuild.1',
architecture: ['amd64', 'arm64'],
compatibilities: [
{
clusterVersion: '1.30',
platformVersions: ['*'],
defaultVersion: false,
},
],
requiresConfiguration: false,
};

const addonVersion2: AddonVersionInfo = {
addonVersion: 'v1.18.2-eksbuild.1',
architecture: ['amd64', 'arm64'],
compatibilities: [
{
clusterVersion: '1.30',
platformVersions: ['*'],
defaultVersion: false,
},
],
requiresConfiguration: false,
};

// a bad addonVersion that's missing the basic fields.
const addonVersionBad: AddonVersionInfo = {};

/**
* Testdata for mock implementation of EKSClient
*/
const vpcCniAddonInfo: AddonInfo = {
addonName: 'vpc-cni',
type: 'networking',
addonVersions: [addonVersion1, addonVersion2, addonVersionBad],
publisher: 'eks',
owner: 'aws',
};

const mockAddon: DescribeAddonVersionsResponse = {
addons: [vpcCniAddonInfo],
};

const mockEmpty: DescribeAddonVersionsResponse = {
addons: [],
};

const mockNull: DescribeAddonVersionsResponse = {};

const mockNullAddonVersions: DescribeAddonVersionsResponse = {
addons: [
{
addonName: 'non-existing-addon',
type: 'networking',
publisher: 'eks',
owner: 'aws',
// missing addonVersions
},
],
};

function mockDescribeAddonVersionsCommand(
result: DescribeAddonVersionsResponse,
): void {
eksMock.reset();
eksMock.on(DescribeAddonVersionsCommand).resolves(result);
}

function mockDescribeAddonVersionsCommandWithRegion(
result: DescribeAddonVersionsResponse,
): void {
eksMock.reset();
eksMock
.on(DescribeAddonVersionsCommand)
.callsFake(async (input, getClient) => {
const client = getClient();
const region = await client.config.region();
return {
...result,
// put the client region as nextToken
// so that when we assert on the snapshot, we also verify that region from packageName is
// passed to aws client.
nextToken: region,
};
});
}

describe('modules/datasource/aws-eks-addon/index', () => {
describe('getPkgReleases()', () => {
it.each([
jzhn marked this conversation as resolved.
Show resolved Hide resolved
['null', mockNull],
['empty', mockEmpty],
['nullAddonVersions', mockNullAddonVersions],
])('returned %s addons to be null', async (_, mocked) => {
mockDescribeAddonVersionsCommand(mocked);
const res = await getPkgReleases({
datasource,
packageName:
'{"kubernetesVersion":"1.30","addonName":"non-existing-addon"}',
});
expect(res).toBeNull();
expect(eksMock.calls()).toHaveLength(1);
expect(eksMock.call(0).args[0].input).toEqual({
kubernetesVersion: '1.30',
addonName: 'non-existing-addon',
maxResults: 1,
});
});

it('with matched addon to return all versions of the addon', async () => {
mockDescribeAddonVersionsCommandWithRegion(mockAddon);
const res = await getPkgReleases({
datasource,
packageName:
'{"kubernetesVersion":"1.30","addonName":"vpc-cni","region":"mars-east-1"}',
});
expect(res).toEqual({
releases: [
{
version: addonVersion1.addonVersion,
},
{
version: addonVersion2.addonVersion,
},
],
});
expect(eksMock.call(0).args[0].input).toEqual({
kubernetesVersion: '1.30',
addonName: 'vpc-cni',
maxResults: 1,
});
expect(await eksMock.call(0).returnValue).toEqual({
addons: [vpcCniAddonInfo],
nextToken: 'mars-east-1',
});
});
});
});
64 changes: 64 additions & 0 deletions lib/modules/datasource/aws-eks-addon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
DescribeAddonVersionsCommand,
DescribeAddonVersionsCommandInput,
EKSClient,
} from '@aws-sdk/client-eks';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { cache } from '../../../util/cache/package/decorator';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import type { EKSAddonsFilter } from './types';

export class AwsEKSAddonDataSource extends Datasource {
static readonly id = 'aws-eks-addon';

override readonly caching = true;

constructor() {
super(AwsEKSAddonDataSource.id);
}

@cache({
namespace: `datasource-${AwsEKSAddonDataSource.id}`,
key: ({ packageName }: GetReleasesConfig) => `getReleases:${packageName}`,
viceice marked this conversation as resolved.
Show resolved Hide resolved
})
async getReleases({
packageName: serializedFilter,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
const filter: EKSAddonsFilter = JSON.parse(serializedFilter);
jzhn marked this conversation as resolved.
Show resolved Hide resolved
const eksClient = this.getEKSClient(filter);
jzhn marked this conversation as resolved.
Show resolved Hide resolved

const cmd = new DescribeAddonVersionsCommand(
this.getDescribeAddonsRequest(filter),
);
const response = await eksClient.send(cmd);
const addons = response.addons ?? [];
return {
releases: addons
.flatMap((addon) => addon.addonVersions)
.filter((versionInfo) => versionInfo?.addonVersion)
.map((versionInfo) => ({
version: versionInfo!.addonVersion!,
jzhn marked this conversation as resolved.
Show resolved Hide resolved
})),
};
}

private getDescribeAddonsRequest({
kubernetesVersion,
addonName,
}: EKSAddonsFilter): DescribeAddonVersionsCommandInput {
// this API is paginated, but we only ever care about a single addon at a time.
return {
kubernetesVersion,
addonName,
maxResults: 1,
};
}

private getEKSClient({ region, profile }: EKSAddonsFilter): EKSClient {
return new EKSClient({
region,
credentials: fromNodeProviderChain({ profile }),
});
}
}
90 changes: 90 additions & 0 deletions lib/modules/datasource/aws-eks-addon/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
This datasource returns the addon versions available for use on [AWS EKS](https://aws.amazon.com/eks/) via the AWS API.

**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

Alternatively, you can specify different `region` and `profile` for each addon.

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": "AllowDescribeEKSAddonVersions",
"Effect": "Allow",
"Action": ["eks:DescribeAddonVersions"],
"Resource": "*"
}
```

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

**Usage**

Because Renovate has no manager for the AWS EKS Addon datasource, you need to help Renovate by configuring the custom manager to identify the AWS EKS Addons you want updated.

When configuring the custom manager, you have to pass in the Kubernetes version and addon names as a minified JSON object as the `packageName`
For example:

```yaml
# Getting the vpc-cni version for Kubernetes 1.30
{
"kubernetesVersion": "1.30",
"addonName": "vpc-cni"
}

# In order to use it with this datasource, you have to minify it:
{"kubernetesVersion":"1.30","addonName":"vpc-cni"}
```

Although it's unlikely that EKS might support different addon versions across regions, you can optionally specify the `region` and/or `profile` in the minified JSON object to discover the addon versions specific to this region.

```yaml
# discover vpc-cni addon versions on Kubernetes 1.30 in us-east-1 region using environmental AWS credentials.
{"kubernetesVersion":"1.30","addonName":"vpc-cni","region":"us-east-1"}

# discover vpc-cni addon versions on Kubernetes 1.30 in us-east-1 region using AWS credentials from `renovate-east` profile.
{"kubernetesVersion":"1.30","addonName":"vpc-cni","region":"us-east-1","profile":"renovate-east"}
```

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

```json
{
"packageRules": [
{
"matchDatasources": ["aws-eks-addon"],
"ignoreUnstable": false
}
],
"customManagers": [
{
"customType": "regex",
"fileMatch": [".*\\.tf"],
"matchStrings": [
".*# renovate: eksAddonsFilter=(?<packageName>.*?)\n.*?[a-zA-Z0-9-_:]*[ ]*?[:|=][ ]*?[\"|']?(?<currentValue>[a-zA-Z0-9-_.]+)[\"|']?.*"
],
"datasourceTemplate": "aws-eks-addon",
"versioningTemplate": "semver"
}
]
}
```

The configuration above matches every terraform file, and recognizes these lines:

```yaml
variable "vpc_cni_version" {
type = string
description = "EKS vpc-cni add-on version"
# renovate: eksAddonsFilter={"kubernetesVersion":"1.30","addonName":"vpc-cni"}
default = "v1.18.1-eksbuild.3"
}
```
6 changes: 6 additions & 0 deletions lib/modules/datasource/aws-eks-addon/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface EKSAddonsFilter {
kubernetesVersion: string;
addonName: string;
region?: string;
profile?: string;
}
1 change: 1 addition & 0 deletions lib/util/cache/package/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type PackageCacheNamespace =
| 'datasource-artifactory'
| 'datasource-aws-machine-image'
| 'datasource-aws-rds'
| 'datasource-aws-eks-addon'
| 'datasource-azure-bicep-resource'
| 'datasource-azure-pipelines-tasks'
| 'datasource-bazel'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"@aws-sdk/client-codecommit": "3.588.0",
"@aws-sdk/client-ec2": "3.588.0",
"@aws-sdk/client-ecr": "3.588.0",
"@aws-sdk/client-eks": "3.588.0",
"@aws-sdk/client-rds": "3.588.0",
"@aws-sdk/client-s3": "3.588.0",
"@aws-sdk/credential-providers": "3.588.0",
Expand Down
Loading