Skip to content

Commit

Permalink
feat(manager/helmv3): add support for ECR credentials (#24432)
Browse files Browse the repository at this point in the history
  • Loading branch information
ezzatron authored Sep 23, 2023
1 parent dc03546 commit a975be8
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 11 deletions.
2 changes: 1 addition & 1 deletion lib/modules/manager/helmfile/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function updateArtifacts({
const doc = parseDoc(newPackageFileContent);

for (const value of coerceArray(doc.repositories).filter(isOCIRegistry)) {
const loginCmd = generateRegistryLoginCmd(
const loginCmd = await generateRegistryLoginCmd(
value.name,
`https://${value.url}`,
// this extracts the hostname from url like format ghcr.ip/helm-charts
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/manager/helmfile/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ export function isOCIRegistry(repository: Repository): boolean {
return repository.oci === true;
}

export function generateRegistryLoginCmd(
export async function generateRegistryLoginCmd(
repositoryName: string,
repositoryBaseURL: string,
repositoryHost: string
): string | null {
): Promise<string | null> {
const repositoryRule: RepositoryRule = {
name: repositoryName,
repository: repositoryHost,
Expand All @@ -59,5 +59,5 @@ export function generateRegistryLoginCmd(
}),
};

return generateLoginCmd(repositoryRule, 'helm registry login');
return await generateLoginCmd(repositoryRule, 'helm registry login');
}
7 changes: 7 additions & 0 deletions lib/modules/manager/helmv3/__fixtures__/ChartECR.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v2
name: app
version: 0.1.0
dependencies:
- name: some-ecr-chart
version: 1.2.3
repository: oci://123456789.dkr.ecr.us-east-1.amazonaws.com
6 changes: 6 additions & 0 deletions lib/modules/manager/helmv3/__fixtures__/oci_1_ecr.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dependencies:
- name: some-ecr-chart
repository: oci://123456789.dkr.ecr.us-east-1.amazonaws.com
version: 1.2.3
digest: sha256:886f204516ea48785fe615d22071d742f7fb0d6519ed3cd274f4ec0978d8b82b
generated: "2022-01-20T17:48:47.610371241+01:00"
6 changes: 6 additions & 0 deletions lib/modules/manager/helmv3/__fixtures__/oci_2_ecr.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dependencies:
- name: some-ecr-chart
repository: oci://123456789.dkr.ecr.us-east-1.amazonaws.com
version: 1.3.4
digest: sha256:886f204516ea48785fe615d22071d742f7fb0d6519ed3cd274f4ec0978d8b82b
generated: "2022-01-20T17:48:47.610371241+01:00"
259 changes: 258 additions & 1 deletion lib/modules/manager/helmv3/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
ECRClient,
GetAuthorizationTokenCommand,
GetAuthorizationTokenCommandOutput,
} from '@aws-sdk/client-ecr';
import { mockClient } from 'aws-sdk-client-mock';
import { mockDeep } from 'jest-mock-extended';
import { join } from 'upath';
import { envMock, mockExecAll } from '../../../../test/exec-util';
Expand All @@ -8,6 +14,7 @@ import type { RepoGlobalConfig } from '../../../config/types';
import * as docker from '../../../util/exec/docker';
import type { StatusResult } from '../../../util/git/types';
import * as hostRules from '../../../util/host-rules';
import { toBase64 } from '../../../util/string';
import * as _datasource from '../../datasource';
import type { UpdateArtifactsConfig } from '../types';
import * as helmv3 from '.';
Expand All @@ -17,7 +24,6 @@ jest.mock('../../../util/exec/env');
jest.mock('../../../util/http');
jest.mock('../../../util/fs');
jest.mock('../../../util/git');

const datasource = mocked(_datasource);

const adminConfig: RepoGlobalConfig = {
Expand All @@ -35,12 +41,29 @@ const ociLockFile1Alias = Fixtures.get('oci_1_alias.lock');
const ociLockFile2Alias = Fixtures.get('oci_2_alias.lock');
const chartFileAlias = Fixtures.get('ChartAlias.yaml');

const ociLockFile1ECR = Fixtures.get('oci_1_ecr.lock');
const ociLockFile2ECR = Fixtures.get('oci_2_ecr.lock');
const chartFileECR = Fixtures.get('ChartECR.yaml');

const ecrMock = mockClient(ECRClient);

function mockEcrAuthResolve(
res: Partial<GetAuthorizationTokenCommandOutput> = {}
) {
ecrMock.on(GetAuthorizationTokenCommand).resolvesOnce(res);
}

function mockEcrAuthReject(msg: string) {
ecrMock.on(GetAuthorizationTokenCommand).rejectsOnce(new Error(msg));
}

describe('modules/manager/helmv3/artifacts', () => {
beforeEach(() => {
env.getChildProcessEnv.mockReturnValue(envMock.basic);
GlobalConfig.set(adminConfig);
docker.resetPrefetchedImages();
hostRules.clear();
ecrMock.reset();
});

afterEach(() => {
Expand Down Expand Up @@ -723,6 +746,240 @@ describe('modules/manager/helmv3/artifacts', () => {
expect(execSnapshots).toMatchSnapshot();
});

it('supports ECR authentication', async () => {
mockEcrAuthResolve({
authorizationData: [
{ authorizationToken: toBase64('token-username:token-password') },
],
});

hostRules.add({
username: 'some-username',
password: 'some-password',
token: 'some-session-token',
hostType: 'docker',
matchHost: '123456789.dkr.ecr.us-east-1.amazonaws.com',
});

fs.getSiblingFileName.mockReturnValueOnce('Chart.lock');
fs.readLocalFile.mockResolvedValueOnce(ociLockFile1ECR as never);
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(ociLockFile2ECR as never);
fs.privateCacheDir.mockReturnValue(
'/tmp/renovate/cache/__renovate-private-cache'
);
fs.getParentDir.mockReturnValue('');

expect(
await helmv3.updateArtifacts({
packageFileName: 'Chart.yaml',
updatedDeps: [],
newPackageFileContent: chartFileECR,
config: {
...config,
updateType: 'lockFileMaintenance',
registryAliases: {},
},
})
).toMatchObject([
{
file: {
type: 'addition',
path: 'Chart.lock',
contents: ociLockFile2ECR,
},
},
]);

const ecr = ecrMock.call(0).thisValue as ECRClient;
expect(await ecr.config.region()).toBe('us-east-1');
expect(await ecr.config.credentials()).toEqual({
accessKeyId: 'some-username',
secretAccessKey: 'some-password',
sessionToken: 'some-session-token',
});

expect(execSnapshots).toMatchObject([
{
cmd: 'helm registry login --username token-username --password token-password 123456789.dkr.ecr.us-east-1.amazonaws.com',
},
{
cmd: "helm dependency update ''",
},
]);
});

it("does not use ECR authentication when the host rule's username is AWS", async () => {
mockEcrAuthResolve({
authorizationData: [
{ authorizationToken: toBase64('token-username:token-password') },
],
});

hostRules.add({
username: 'AWS',
password: 'some-password',
token: 'some-session-token',
hostType: 'docker',
matchHost: '123456789.dkr.ecr.us-east-1.amazonaws.com',
});

fs.getSiblingFileName.mockReturnValueOnce('Chart.lock');
fs.readLocalFile.mockResolvedValueOnce(ociLockFile1ECR as never);
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(ociLockFile2ECR as never);
fs.privateCacheDir.mockReturnValue(
'/tmp/renovate/cache/__renovate-private-cache'
);
fs.getParentDir.mockReturnValue('');

expect(
await helmv3.updateArtifacts({
packageFileName: 'Chart.yaml',
updatedDeps: [],
newPackageFileContent: chartFileECR,
config: {
...config,
updateType: 'lockFileMaintenance',
registryAliases: {},
},
})
).toMatchObject([
{
file: {
type: 'addition',
path: 'Chart.lock',
contents: ociLockFile2ECR,
},
},
]);

expect(ecrMock.calls).toHaveLength(0);

expect(execSnapshots).toMatchObject([
{
cmd: 'helm registry login --username AWS --password some-password 123456789.dkr.ecr.us-east-1.amazonaws.com',
},
{
cmd: "helm dependency update ''",
},
]);
});

it('continues without auth if the ECR token is invalid', async () => {
mockEcrAuthResolve({
authorizationData: [{ authorizationToken: ':' }],
});

hostRules.add({
username: 'some-username',
password: 'some-password',
token: 'some-session-token',
hostType: 'docker',
matchHost: '123456789.dkr.ecr.us-east-1.amazonaws.com',
});

fs.getSiblingFileName.mockReturnValueOnce('Chart.lock');
fs.readLocalFile.mockResolvedValueOnce(ociLockFile1ECR as never);
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(ociLockFile2ECR as never);
fs.privateCacheDir.mockReturnValue(
'/tmp/renovate/cache/__renovate-private-cache'
);
fs.getParentDir.mockReturnValue('');

expect(
await helmv3.updateArtifacts({
packageFileName: 'Chart.yaml',
updatedDeps: [],
newPackageFileContent: chartFileECR,
config: {
...config,
updateType: 'lockFileMaintenance',
registryAliases: {},
},
})
).toMatchObject([
{
file: {
type: 'addition',
path: 'Chart.lock',
contents: ociLockFile2ECR,
},
},
]);

const ecr = ecrMock.call(0).thisValue as ECRClient;
expect(await ecr.config.region()).toBe('us-east-1');
expect(await ecr.config.credentials()).toEqual({
accessKeyId: 'some-username',
secretAccessKey: 'some-password',
sessionToken: 'some-session-token',
});

expect(execSnapshots).toMatchObject([
{
cmd: "helm dependency update ''",
},
]);
});

it('continues without auth if ECR authentication fails', async () => {
mockEcrAuthReject('some error');

hostRules.add({
username: 'some-username',
password: 'some-password',
token: 'some-session-token',
hostType: 'docker',
matchHost: '123456789.dkr.ecr.us-east-1.amazonaws.com',
});

fs.getSiblingFileName.mockReturnValueOnce('Chart.lock');
fs.readLocalFile.mockResolvedValueOnce(ociLockFile1ECR as never);
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(ociLockFile2ECR as never);
fs.privateCacheDir.mockReturnValue(
'/tmp/renovate/cache/__renovate-private-cache'
);
fs.getParentDir.mockReturnValue('');

expect(
await helmv3.updateArtifacts({
packageFileName: 'Chart.yaml',
updatedDeps: [],
newPackageFileContent: chartFileECR,
config: {
...config,
updateType: 'lockFileMaintenance',
registryAliases: {},
},
})
).toMatchObject([
{
file: {
type: 'addition',
path: 'Chart.lock',
contents: ociLockFile2ECR,
},
},
]);

const ecr = ecrMock.call(0).thisValue as ECRClient;
expect(await ecr.config.region()).toBe('us-east-1');
expect(await ecr.config.credentials()).toEqual({
accessKeyId: 'some-username',
secretAccessKey: 'some-password',
sessionToken: 'some-session-token',
});

expect(execSnapshots).toMatchObject([
{
cmd: "helm dependency update ''",
},
]);
});

it('alias name is picked, when repository is as alias and dependency defined', async () => {
hostRules.add({
username: 'basicUser',
Expand Down
5 changes: 3 additions & 2 deletions lib/modules/manager/helmv3/artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import is from '@sindresorhus/is';
import yaml from 'js-yaml';
import pMap from 'p-map';
import { quote } from 'shlex';
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { logger } from '../../../logger';
Expand Down Expand Up @@ -46,8 +47,8 @@ async function helmCommands(
});

// if credentials for the registry have been found, log into it
registries.forEach((value) => {
const loginCmd = generateLoginCmd(value, 'helm registry login');
await pMap(registries, async (value) => {
const loginCmd = await generateLoginCmd(value, 'helm registry login');
if (loginCmd) {
cmd.push(loginCmd);
}
Expand Down
Loading

0 comments on commit a975be8

Please sign in to comment.