-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(datasource): add aws-eks datasource
Signed-off-by: ivan katliarchuk <[email protected]>
- Loading branch information
1 parent
8d31e5c
commit 53f21e2
Showing
9 changed files
with
919 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { | ||
DescribeClusterVersionsCommand, | ||
type DescribeClusterVersionsCommandOutput, | ||
EKSClient, | ||
} from '@aws-sdk/client-eks'; | ||
import { mockClient } from 'aws-sdk-client-mock'; | ||
import { logger } from '../../../../test/util'; | ||
import { getPkgReleases } from '../index'; | ||
import { AwsEKSDataSource } from '.'; | ||
|
||
const datasource = AwsEKSDataSource.id; | ||
const eksMock = mockClient(EKSClient); | ||
|
||
describe('modules/datasource/aws-eks/index', () => { | ||
beforeEach(() => { | ||
eksMock.reset(); | ||
}); | ||
|
||
describe('getReleases()', () => { | ||
it('should return releases when the response is valid', async () => { | ||
const mockResponse: DescribeClusterVersionsCommandOutput = { | ||
$metadata: {}, | ||
clusterVersions : [ | ||
{ | ||
clusterVersion: '1.21', | ||
releaseDate: new Date(new Date().setMonth(new Date().getMonth() - 24)), | ||
endOfStandardSupportDate: new Date(new Date().setMonth(new Date().getMonth() + 10)), | ||
}, | ||
], | ||
}; | ||
|
||
eksMock.on(DescribeClusterVersionsCommand).resolves(mockResponse); | ||
|
||
const result = await getPkgReleases({ datasource, packageName: '{}' }); | ||
|
||
expect(result?.releases).toHaveLength(1) | ||
expect(result).toEqual({ | ||
releases: [ | ||
{ | ||
version: '1.21', | ||
}, | ||
], | ||
}); | ||
|
||
expect(eksMock.calls()).toHaveLength(1); | ||
expect(eksMock.call(0).args[0].input).toEqual({}); | ||
}); | ||
|
||
it('should return null and log an error when the filter is invalid', async () => { | ||
const invalidFilter = '{ invalid json }'; | ||
const actual = await getPkgReleases({ datasource, packageName: invalidFilter }); | ||
expect(actual).toBeNull(); | ||
expect(logger.logger.error).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should return default cluster only', async () => { | ||
const mockResponse: DescribeClusterVersionsCommandOutput = { | ||
$metadata: {}, | ||
clusterVersions: [ | ||
{ | ||
clusterVersion: '1.31', | ||
defaultVersion: true, | ||
status: 'standard-support', | ||
}, | ||
], | ||
}; | ||
eksMock.on(DescribeClusterVersionsCommand).resolves(mockResponse); | ||
|
||
const actual = await getPkgReleases( | ||
{ datasource, packageName: '{"default":"true", "region":"eu-west-1"}' } | ||
); | ||
|
||
expect(eksMock.calls()).toHaveLength(1); | ||
expect(eksMock.call(0).args[0].input).toEqual({"defaultOnly": true}); | ||
|
||
expect(actual).toEqual({ | ||
releases: [ | ||
{ | ||
version: '1.31', | ||
}, | ||
], | ||
}); | ||
}); | ||
|
||
it('should return default and non-default cluster when default:false', async () => { | ||
const mockResponse: DescribeClusterVersionsCommandOutput = { | ||
$metadata: {}, | ||
clusterVersions: [ | ||
{ | ||
clusterVersion: '1.31', | ||
defaultVersion: true, | ||
}, | ||
{ | ||
clusterVersion: '1.30', | ||
defaultVersion: false, | ||
}, | ||
{ | ||
clusterVersion: '1.29', | ||
defaultVersion: false, | ||
}, | ||
], | ||
}; | ||
eksMock.on(DescribeClusterVersionsCommand).resolves(mockResponse); | ||
|
||
const actual = await getPkgReleases( | ||
{ datasource, packageName: '{"default":"false", "region":"eu-west-1", "profile":"admin"}' } | ||
); | ||
|
||
expect(eksMock.calls()).toHaveLength(1); | ||
expect(eksMock.call(0).args[0].input).toEqual({"defaultOnly": false}); | ||
|
||
expect(actual).toEqual({ | ||
releases: [ | ||
{ version: '1.29' }, | ||
{ version: '1.30' }, | ||
{ version: '1.31' }, | ||
], | ||
}); | ||
}); | ||
|
||
it('should return empty response', async () => { | ||
const mockResponse: DescribeClusterVersionsCommandOutput = { | ||
$metadata: {}, | ||
clusterVersions: [], | ||
}; | ||
eksMock.on(DescribeClusterVersionsCommand).resolves(mockResponse); | ||
|
||
const actual = await getPkgReleases( | ||
{ datasource, packageName: '{"profile":"not-exist-profile"}' } | ||
); | ||
|
||
expect(eksMock.calls()).toHaveLength(1); | ||
expect(eksMock.call(0).args[0].input).toEqual({}); | ||
expect(actual).toBeNull(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { | ||
type ClusterVersionInformation, | ||
DescribeClusterVersionsCommand, | ||
type DescribeClusterVersionsCommandInput, | ||
type DescribeClusterVersionsCommandOutput, | ||
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 { Datasource } from '../datasource'; | ||
import type { GetReleasesConfig, ReleaseResult } from '../types'; | ||
import { EksFilter } from './schema'; | ||
|
||
export class AwsEKSDataSource extends Datasource { | ||
static readonly id = 'aws-eks'; | ||
|
||
override readonly caching = true; | ||
private readonly clients: Record<string, EKSClient> = {}; | ||
|
||
override readonly releaseTimestampSupport = true; | ||
override readonly releaseTimestampNote = | ||
'The release timestamp is determined from the `endOfStandardSupportDate` field in the results.'; | ||
|
||
override readonly defaultConfig: Record<string, unknown> | undefined = { | ||
commitMessageTopic: '{{{datasource}}}', | ||
commitMessageExtra: '{{{currentVersion}}} to {{{newVersion}}}', | ||
}; | ||
|
||
constructor() { | ||
super(AwsEKSDataSource.id); | ||
} | ||
|
||
@cache({ | ||
namespace: `datasource-${AwsEKSDataSource.id}`, | ||
key: ({ packageName }: GetReleasesConfig) => `getReleases:${packageName}`, | ||
}) | ||
async getReleases({ | ||
packageName: serializedFilter, | ||
}: GetReleasesConfig): Promise<ReleaseResult | null> { | ||
const res = EksFilter.safeParse(serializedFilter); | ||
if (!res.success) { | ||
logger.error( | ||
{ err: res.error, serializedFilter }, | ||
'Error parsing eks config.', | ||
); | ||
return null; | ||
} | ||
|
||
const input: DescribeClusterVersionsCommandInput = { | ||
defaultOnly: res.data.default ?? undefined, | ||
}; | ||
const cmd = new DescribeClusterVersionsCommand(input) | ||
const response: DescribeClusterVersionsCommandOutput = await this.getClient(res.data).send(cmd) | ||
const results: ClusterVersionInformation[] = response.clusterVersions ?? []; | ||
return { | ||
releases: results | ||
.filter((el): el is ClusterVersionInformation & { clusterVersion: string } => Boolean(el.clusterVersion)) | ||
.map((el) => ({ | ||
version: el.clusterVersion, | ||
})), | ||
}; | ||
} | ||
|
||
private getClient({ region, profile }: EksFilter): 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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
The EKS `datasource` is designed to query one or more [AWS EKS](https://docs.aws.amazon.com/eks/latest/userguide/platform-versions.html) 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: | ||
|
||
- Specifies the AWS region where your resources are located. This is crucial for routing requests to the correct endpoint. | ||
- Set the region via the `AWS_REGION` environment variable | ||
- Pass the `region` option to Renovate | ||
- Read credentials from environment variables (e.g., `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). | ||
- Load credentials from the shared credentials file (~/.aws/credentials). | ||
- Use IAM roles for EC2 instances or Lambda functions. | ||
- A chain of credential providers that the SDK attempts in order. | ||
|
||
The minimal IAM privileges required for this datasource are: | ||
|
||
```json | ||
{ | ||
"Effect": "Allow", | ||
"Action": ["eks:DescribeClusterVersions"], | ||
"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 datasource, you need to help Renovate by configuring the custom manager to identify the AWS EKS configuration you want updated. | ||
|
||
Configuration Options | ||
|
||
```yaml | ||
# discover all available eks versions. | ||
renovate: eksFilter={} | ||
|
||
# discover default eks versions | ||
renovate: eksFilter={"default":true} | ||
|
||
# discover all available eks versions in us-east-1 region using environmental AWS credentials. Region is a recommended option. | ||
renovate: eksFilter={"region":"eu-west-1"} | ||
|
||
# discover all available eks versions in us-east-1 region using AWS credentials from `renovate-east` profile. | ||
renovate: eksFilter={"region":"us-east-1","profile":"renovate-east"} | ||
``` | ||
```json | ||
{ | ||
"packageRules": [ | ||
{ | ||
"matchDatasources": ["aws-eks"], | ||
"prBodyColumns": [ | ||
"Package", | ||
"Update", | ||
"Change", | ||
"Sources", | ||
"Changelog" | ||
], | ||
"prBodyDefinitions": { | ||
"Sources": "[▶️](https://github.com/aws/eks-distro/)", | ||
"Changelog": "[▶️](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-{{{newVersion}}}.md)", | ||
} | ||
} | ||
], | ||
"customManagers": [ | ||
{ | ||
"customType": "regex", | ||
"fileMatch": [".*\\.tf"], | ||
"matchStrings": [ | ||
".*# renovate: eksFilter=(?<packageName>.*?)\n.*?[a-zA-Z0-9-_:]*[ ]*?[:|=][ ]*?[\"|']?(?<currentValue>[a-zA-Z0-9-_.]+)[\"|']?.*" | ||
], | ||
"datasourceTemplate": "aws-eks", | ||
"versioningTemplate": "loose" // aws-eks versioning is not yet supported | ||
} | ||
] | ||
} | ||
``` | ||
|
||
Check failure on line 79 in lib/modules/datasource/aws-eks/readme.md GitHub Actions / lint-docsInvalid JSON in fenced code block
|
||
The configuration above matches every terraform file, and recognizes these lines: | ||
|
||
```hcl | ||
variable "eks_version" { | ||
type = string | ||
description = "EKS vpc-cni add-on version" | ||
# region provided | ||
# renovate: eksFilter={"region":"eu-west-1"} | ||
default = "1.24" | ||
} | ||
``` | ||
|
||
```yml | ||
clusters: | ||
- name: main | ||
# only default version | ||
# renovate: eksFilter={"default":true} | ||
version: 1.24 | ||
- name: tier-1 | ||
# region and where or not only default versions | ||
# renovate: eksFilter={"default":"false", "region":"eu-west-1"} | ||
version: 1.28 | ||
``` | ||
**Cluster Upgrade** | ||
- [AWS EKS cluster upgrade best practices](https://docs.aws.amazon.com/eks/latest/best-practices/cluster-upgrades.html) | ||
At the moment there is no `aws-eks` versioning. The recommended approach is to upgrade to next minor version | ||
|
||
When performing an in-place cluster upgrade, it is important to note that only one minor version upgrade can be executed at a time (e.g., from 1.24 to 1.25). This means that if you need to update multiple versions, a series of sequential upgrades will be required. | ||
|
||
Correct | ||
|
||
```diff | ||
- version: 1.24 | ||
+ version: 1.25 | ||
``` | ||
|
||
Will not work | ||
|
||
```diff | ||
- version: 1.24 | ||
+ version: 1.27 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { EksFilter } from './schema'; | ||
|
||
describe('modules/datasource/aws-eks/schema', () => { | ||
describe('EksFilter', () => { | ||
it.each` | ||
input | expected | ||
${{ default: 'false' }} | ${true} | ||
${{ default: 'true' }} | ${true} | ||
${{ default: false }} | ${true} | ||
${{ default: true }} | ${true} | ||
${{}} | ${true} | ||
${{ default: 'false', region: 'eu-west-1' }} | ${true} | ||
${{ region: 'us-gov-west-1', profile: 'gov-profile' }} | ${true} | ||
${{ region: 'us-gov-west-not-exist' }} | ${false} | ||
${{ default: 'abrakadabra' }} | ${true} | ||
`('EksFilter.safeParse("$input") === $expected', ({ input, expected }) => { | ||
const actual = EksFilter.safeParse(JSON.stringify(input)); | ||
expect(actual.success).toBe(expected); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.