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 11 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
128 changes: 128 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,128 @@
import {
type AddonInfo,
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);

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<{ des: string; req: DescribeAddonVersionsResponse }>`
des | req
${'null'} | ${{}}
${'empty'} | ${{ addons: [] }}
${'emptyVersion'} | ${{ addons: [{}] }}
`('returned $des addons to be null', async ({ req }) => {
mockDescribeAddonVersionsCommand(req);
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 () => {
const vpcCniAddonInfo: AddonInfo = {
addonName: 'vpc-cni',
type: 'networking',
addonVersions: [
{
addonVersion: 'v1.18.1-eksbuild.1',
architecture: ['amd64', 'arm64'],
compatibilities: [
{
clusterVersion: '1.30',
platformVersions: ['*'],
defaultVersion: false,
},
],
requiresConfiguration: false,
},
{
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.
{},
],
publisher: 'eks',
owner: 'aws',
};

mockDescribeAddonVersionsCommandWithRegion({
addons: [vpcCniAddonInfo],
});
const res = await getPkgReleases({
datasource,
packageName:
'{"kubernetesVersion":"1.30","addonName":"vpc-cni","region":"mars-east-1"}',
});
expect(res).toEqual({
releases: [
{
version: 'v1.18.1-eksbuild.1',
},
{
version: 'v1.18.2-eksbuild.1',
},
],
});
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',
});
});
});
});
72 changes: 72 additions & 0 deletions lib/modules/datasource/aws-eks-addon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 { EKSAddonsFilter, EKSAddonsFilterSchema } from './schema';
rarkins marked this conversation as resolved.
Show resolved Hide resolved

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

override readonly caching = true;
private readonly eksClients: Record<string, EKSClient> = {};

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 = EKSAddonsFilterSchema.parse(
JSON.parse(serializedFilter),
);
rarkins 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)
.map((versionInfo) => ({
version: versionInfo?.addonVersion ?? '',
}))
.filter((release) => release.version),
};
}

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 {
// we have two dimensions here, building a cache key for simplicity.
const cacheKey = `${region ?? 'default'}#${profile ?? 'default'}`;
if (!(cacheKey in this.eksClients)) {
this.eksClients[cacheKey] = new EKSClient({
region,
credentials: fromNodeProviderChain({ profile }),
});
}
return this.eksClients[cacheKey];
}
}
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"
}
```
10 changes: 10 additions & 0 deletions lib/modules/datasource/aws-eks-addon/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod';

export const EKSAddonsFilterSchema = z.object({
rarkins marked this conversation as resolved.
Show resolved Hide resolved
kubernetesVersion: z.string().min(1),
addonName: z.string().min(1),
region: z.string().optional(),
profile: z.string().optional(),
});

export type EKSAddonsFilter = z.infer<typeof EKSAddonsFilterSchema>;
rarkins marked this conversation as resolved.
Show resolved Hide resolved
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