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(datasource): add eks addon datasource #33272

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fbf6e8b
feat(23410): eks addon refresh
ivankatliarchuk Dec 24, 2024
0364b73
Merge branch 'main' into feat-23410
ivankatliarchuk Dec 24, 2024
d314263
feat(23410): update package json
ivankatliarchuk Dec 24, 2024
7264215
update pnpm-lock.yaml
ivankatliarchuk Dec 24, 2024
33414f4
feat(23410): update as per linter
ivankatliarchuk Dec 25, 2024
e784702
feat(23410): update dependencies
ivankatliarchuk Dec 25, 2024
d0775cc
feat(23410): fix linters
ivankatliarchuk Dec 25, 2024
b071797
feat(23410): fix linters
ivankatliarchuk Dec 25, 2024
dffe55e
feat(23410): added support for versioning
ivankatliarchuk Dec 26, 2024
1a64d1a
feat(23410): added tests for schema. prettier-fix
ivankatliarchuk Dec 26, 2024
d551b46
feat(23410): fix all tests
ivankatliarchuk Dec 26, 2024
8c7a10b
feat(23410): update package.json deps and loc
ivankatliarchuk Dec 26, 2024
6663a43
Merge branch 'main' into feat-23410
ivankatliarchuk Dec 26, 2024
1e59984
Merge branch 'main' into feat-23410
ivankatliarchuk Dec 26, 2024
2c5724a
feat(23410): increase code coverage
ivankatliarchuk Dec 26, 2024
e9a9022
feat(23410): prettier fix
ivankatliarchuk Dec 26, 2024
339b63f
feat(23410): prettier fix
ivankatliarchuk Dec 26, 2024
23c386b
feat(23410): prettier fix
ivankatliarchuk Dec 26, 2024
6824963
feat(23410): cover all lines for aws-eks-addon versioning
ivankatliarchuk Dec 26, 2024
a9b061a
Merge branch 'main' into feat-23410
ivankatliarchuk Dec 26, 2024
36f1ac6
feat(datasource): split versioning
ivankatliarchuk Dec 27, 2024
69046a8
feat(datasource): split versioning
ivankatliarchuk Dec 27, 2024
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 @@ -70,6 +71,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
264 changes: 264 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,264 @@
import {
type AddonInfo,
DescribeAddonVersionsCommand,
type DescribeAddonVersionsResponse,
EKSClient,
} from '@aws-sdk/client-eks';
import { mockClient } from 'aws-sdk-client-mock';
import { getPkgReleases } from '..';
import { logger } from '../../../../test/util';

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,
};
});
}

const addonInfo: AddonInfo = {
addonName: 'vpc-cni',
type: 'networking',
addonVersions: [
{
addonVersion: 'v1.19.0-eksbuild.1',
architecture: ['amd64', 'arm64'],
compatibilities: [
{
clusterVersion: '1.31',
defaultVersion: true,
},
{
clusterVersion: '1.30',
defaultVersion: true,
},
{
clusterVersion: '1.29',
defaultVersion: true,
},
],
},
{
addonVersion: 'v1.18.1-eksbuild.1',
architecture: ['amd64', 'arm64'],
compatibilities: [
{
clusterVersion: '1.30',
defaultVersion: false,
},
],
},
{
addonVersion: 'v1.18.2-eksbuild.1',
architecture: ['amd64', 'arm64'],
compatibilities: [
{
clusterVersion: '1.30',
platformVersions: ['*'],
defaultVersion: true,
},
],
},
],
publisher: 'eks',
owner: 'aws',
};

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 addonName not supplied', async () => {
const res = await getPkgReleases({
datasource,
packageName: '{"kubernetesVersion":"1.30"}',
});
expect(res).toBeNull();
expect(logger.logger.error).toHaveBeenCalledTimes(1);
});

it('with addonName only', async () => {
mockDescribeAddonVersionsCommand({ addons: [addonInfo] });
const res = await getPkgReleases({
datasource,
packageName: '{"addonName":"vpc-cni"}',
});
expect(res?.releases).toHaveLength(3);
expect(res).toEqual({
releases: [
{
version: 'v1.18.1-eksbuild.1',
compatibleWith: ['1.30'],
default: false,
},
{
version: 'v1.18.2-eksbuild.1',
compatibleWith: ['1.30'],
default: true,
},
{
version: 'v1.19.0-eksbuild.1',
compatibleWith: ['1.31', '1.30', '1.29'],
default: true,
},
],
});
expect(eksMock.call(0).args[0].input).toEqual({
addonName: 'vpc-cni',
maxResults: 1,
});
});

it('with addon and profile', async () => {
mockDescribeAddonVersionsCommand({ addons: [] });
await getPkgReleases({
datasource,
packageName: '{"addonName":"vpc-cni-not-exist", "profile":"paradox"}',
});
expect(eksMock.calls()).toHaveLength(1);
});

it('with addon and region', async () => {
mockDescribeAddonVersionsCommand({ addons: [] });
await getPkgReleases({
datasource,
packageName: '{"addonName":"vpc-cni-not-exist", "region":"usa"}',
});
expect(eksMock.calls()).toHaveLength(1);
});

it('with addonName and default only config', async () => {
mockDescribeAddonVersionsCommand({ addons: [addonInfo] });
const res = await getPkgReleases({
datasource,
packageName: '{"addonName":"vpc-cni", "default":true}',
});
expect(eksMock.call(0).args[0].input).toEqual({
addonName: 'vpc-cni',
maxResults: 1,
});
expect(res?.releases).toHaveLength(2);
expect(res).toEqual({
releases: [
{
version: 'v1.18.2-eksbuild.1',
compatibleWith: ['1.30'],
default: true,
},
{
version: 'v1.19.0-eksbuild.1',
compatibleWith: ['1.31', '1.30', '1.29'],
default: true,
},
],
});
});

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',
compatibleWith: ['1.30'],
default: false,
},
{
version: 'v1.18.2-eksbuild.1',
compatibleWith: ['1.30'],
default: false,
},
],
});
expect(eksMock.call(0).args[0].input).toEqual({
kubernetesVersion: '1.30',
addonName: 'vpc-cni',
maxResults: 1,
});
});
});
});
96 changes: 96 additions & 0 deletions lib/modules/datasource/aws-eks-addon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
type AddonInfo,
type AddonVersionInfo,
type Compatibility,
DescribeAddonVersionsCommand,
type DescribeAddonVersionsCommandInput,
type DescribeAddonVersionsCommandOutput,
EKSClient,
} from '@aws-sdk/client-eks';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { logger } from '../../../logger';
import { cache } from '../../../util/cache/package/decorator';
import * as awsEksAddonVersioning from '../../versioning/aws-eks-addon';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import { EksAddonsFilter } from './schema';

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

override readonly defaultVersioning = awsEksAddonVersioning.id;
override readonly caching = true;
private readonly clients: Record<string, EKSClient> = {};

override readonly defaultConfig: Record<string, unknown> | undefined = {
commitMessageTopic: '{{datasource}}',
commitMessageExtra: '{{currentVersion}} to {{{newVersion}}}',
};

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

@cache({
namespace: `datasource-${AwsEKSAddonDataSource.id}`,
key: ({ packageName }: GetReleasesConfig) => `getReleases:${packageName}`,
})
async getReleases({
packageName: serializedFilter,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
const res = EksAddonsFilter.safeParse(serializedFilter);
if (!res.success) {
logger.error(
{ err: res.error, serializedFilter },
'Error parsing eks-addons config.',
);
return null;
}

const filter = res.data;
const input: DescribeAddonVersionsCommandInput = {
kubernetesVersion: filter?.kubernetesVersion,
addonName: filter?.addonName,
maxResults: 1,
};

const cmd = new DescribeAddonVersionsCommand(input);
const response: DescribeAddonVersionsCommandOutput =
await this.getClient(filter).send(cmd);
const addons: AddonInfo[] = response.addons ?? [];
return {
releases: addons
.flatMap((addon: AddonInfo): AddonVersionInfo[] | undefined => {
return addon.addonVersions;
})
.map((versionInfo: AddonVersionInfo | undefined) => ({
version: versionInfo?.addonVersion ?? '',
default:
versionInfo?.compatibilities?.some(
(comp: Compatibility): boolean | undefined => comp.defaultVersion,
) ?? false,
compatibleWith: versionInfo?.compatibilities?.flatMap(
(comp: Compatibility): string | undefined => comp.clusterVersion,
),
}))
.filter((release) => release.version && release.version !== '')
.filter((release): boolean => {
if (filter.default) {
return release.default && release.default === filter.default;
}
return true;
}),
};
}

private getClient({ region, profile }: EksAddonsFilter): EKSClient {
const cacheKey = `${region ?? 'default'}#${profile ?? 'default'}`;
if (!(cacheKey in this.clients)) {
this.clients[cacheKey] = new EKSClient({
region: region ?? undefined,
credentials: fromNodeProviderChain(profile ? { profile } : undefined),
});
}
return this.clients[cacheKey];
}
}
Loading
Loading