feat(manager/helmfile): support helm registry login when updating helmfile.lock (#22494)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
This commit is contained in:
mugi 2023-06-19 14:35:54 +09:00 committed by GitHub
parent 4e8591eda6
commit b52d7255a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 271 additions and 20 deletions

View file

@ -152,6 +152,157 @@ describe('modules/manager/helmfile/artifacts', () => {
]);
});
it('returns updated helmfile.lock if repositories were defined in ../helmfile-defaults.yaml.', async () => {
const helmfileYamlWithoutRepositories = codeBlock`
bases:
- ../helmfile-defaults.yaml
releases:
- name: backstage
chart: backstage/backstage
version: 0.12.0
`;
const lockFileWithoutRepositories = codeBlock`
version: 0.151.0
dependencies:
- name: backstage
repository: https://backstage.github.io/charts
version: 0.11.0
digest: sha256:e284706b71f37b757a536703da4cb148d67901afbf1ab431f7d60a9852ca6eef
generated: "2023-03-08T21:32:06.122276997+01:00"
`;
const lockFileTwoWithoutRepositories = codeBlock`
version: 0.151.0
dependencies:
- name: backstage
repository: https://backstage.github.io/charts
version: 0.12.0
digest: sha256:9d83889176d005effb86041d30c20361625561cbfb439cbd16d7243225bac17c
generated: "2023-03-08T21:30:48.273709455+01:00"
`;
git.getFile.mockResolvedValueOnce(lockFileWithoutRepositories as never);
fs.getSiblingFileName.mockReturnValueOnce('helmfile.lock');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(
lockFileTwoWithoutRepositories as never
);
fs.privateCacheDir.mockReturnValue(
'/tmp/renovate/cache/__renovate-private-cache'
);
fs.getParentDir.mockReturnValue('');
const updatedDeps = [{ depName: 'dep1' }, { depName: 'dep2' }];
expect(
await helmfile.updateArtifacts({
packageFileName: 'helmfile.yaml',
updatedDeps,
newPackageFileContent: helmfileYamlWithoutRepositories,
config,
})
).toEqual([
{
file: {
type: 'addition',
path: 'helmfile.lock',
contents: lockFileTwoWithoutRepositories,
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: 'helmfile deps -f helmfile.yaml' },
]);
});
it('log into private OCI registries, returns updated helmfile.lock', async () => {
const helmfileYamlOCIPrivateRepo = codeBlock`
repositories:
- name: private-charts
url: ghcr.io/charts
oci: true
releases:
- name: chart
chart: private-charts/chart
version: 0.12.0
`;
const lockFileOCIPrivateRepo = codeBlock`
version: 0.151.0
dependencies:
- name: chart
repository: oci://ghcr.io/private-charts
version: 0.11.0
digest: sha256:e284706b71f37b757a536703da4cb148d67901afbf1ab431f7d60a9852ca6eef
generated: "2023-03-08T21:32:06.122276997+01:00"
`;
const lockFileOCIPrivateRepoTwo = codeBlock`
version: 0.151.0
dependencies:
- name: chart
repository: oci://ghcr.io/private-charts
version: 0.12.0
digest: sha256:9d83889176d005effb86041d30c20361625561cbfb439cbd16d7243225bac17c
generated: "2023-03-08T21:30:48.273709455+01:00"
`;
hostRules.add({
username: 'test',
password: 'password',
hostType: 'docker',
matchHost: 'ghcr.io',
});
git.getFile.mockResolvedValueOnce(lockFileOCIPrivateRepo as never);
fs.getSiblingFileName.mockReturnValueOnce('helmfile.lock');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(lockFileOCIPrivateRepoTwo as never);
fs.privateCacheDir.mockReturnValue(
'/tmp/renovate/cache/__renovate-private-cache'
);
fs.getParentDir.mockReturnValue('');
const updatedDeps = [{ depName: 'dep1' }, { depName: 'dep2' }];
expect(
await helmfile.updateArtifacts({
packageFileName: 'helmfile.yaml',
updatedDeps,
newPackageFileContent: helmfileYamlOCIPrivateRepo,
config,
})
).toEqual([
{
file: {
type: 'addition',
path: 'helmfile.lock',
contents: lockFileOCIPrivateRepoTwo,
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'helm registry login --username test --password password ghcr.io',
options: {
env: {
HELM_REGISTRY_CONFIG:
'/tmp/renovate/cache/__renovate-private-cache/registry.json',
HELM_REPOSITORY_CONFIG:
'/tmp/renovate/cache/__renovate-private-cache/repositories.yaml',
HELM_REPOSITORY_CACHE:
'/tmp/renovate/cache/__renovate-private-cache/repositories',
},
},
},
{
cmd: 'helmfile deps -f helmfile.yaml',
options: {
env: {
HELM_REGISTRY_CONFIG:
'/tmp/renovate/cache/__renovate-private-cache/registry.json',
HELM_REPOSITORY_CONFIG:
'/tmp/renovate/cache/__renovate-private-cache/repositories.yaml',
HELM_REPOSITORY_CACHE:
'/tmp/renovate/cache/__renovate-private-cache/repositories',
},
},
},
]);
});
it.each([
{
binarySource: 'docker',
@ -163,6 +314,10 @@ describe('modules/manager/helmfile/artifacts', () => {
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
'-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
'-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' +
'-e HELM_EXPERIMENTAL_OCI ' +
'-e HELM_REGISTRY_CONFIG ' +
'-e HELM_REPOSITORY_CONFIG ' +
'-e HELM_REPOSITORY_CACHE ' +
'-e BUILDPACK_CACHE_DIR ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "/tmp/github/some/repo" ' +

View file

@ -2,6 +2,7 @@ import is from '@sindresorhus/is';
import { quote } from 'shlex';
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { exec } from '../../../util/exec';
import type { ToolConstraint } from '../../../util/exec/types';
import {
@ -10,7 +11,10 @@ import {
writeLocalFile,
} from '../../../util/fs';
import { getFile } from '../../../util/git';
import { regEx } from '../../../util/regex';
import { generateHelmEnvs } from '../helmv3/common';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import { generateRegistryLoginCmd, isOCIRegistry, parseDoc } from './utils';
export async function updateArtifacts({
packageFileName,
@ -59,9 +63,27 @@ export async function updateArtifacts({
constraint: config.constraints?.kustomize,
});
}
await exec(`helmfile deps -f ${quote(packageFileName)}`, {
const cmd: string[] = [];
const doc = parseDoc(newPackageFileContent);
for (const value of coerceArray(doc.repositories).filter(isOCIRegistry)) {
const loginCmd = generateRegistryLoginCmd(
value.name,
`https://${value.url}`,
// this extracts the hostname from url like format ghcr.ip/helm-charts
value.url.replace(regEx(/\/.*/), '')
);
if (loginCmd) {
cmd.push(loginCmd);
}
}
cmd.push(`helmfile deps -f ${quote(packageFileName)}`);
await exec(cmd, {
docker: {},
extraEnv: {},
extraEnv: generateHelmEnvs(),
toolConstraints,
});

View file

@ -13,3 +13,33 @@ The `helmfile` manager defines this default registryAlias:
If your Helm charts make use of repository aliases then you will need to configure an `registryAliases` object in your config to tell Renovate where to look for them. Be aware that alias values must be properly formatted URIs.
If you need to change the versioning format, read the [versioning](https://docs.renovatebot.com/modules/versioning/) documentation to learn more.
### Private repositories and registries
To use private sources of Helm charts, you must set the password and username you use to authenticate to the private source.
For this you use a custom `hostRules` array.
#### OCI registries
```json5
{
hostRules: [
{
// global login
matchHost: 'ghcr.io',
hostType: 'docker',
username: '<some-username>',
password: '<some-password>',
},
{
// login with encrypted password
matchHost: 'https://ghci.io',
hostType: 'docker',
username: '<some-username>',
encrypted: {
password: 'some-encrypted-password',
},
},
],
}
```

View file

@ -0,0 +1,21 @@
import { z } from 'zod';
export const RepositorySchema = z.object({
name: z.string(),
url: z.string(),
oci: z.boolean().optional(),
});
export const ReleaseSchema = z.object({
name: z.string(),
chart: z.string(),
version: z.string(),
strategicMergePatches: z.unknown().optional(),
jsonPatches: z.unknown().optional(),
transformers: z.unknown().optional(),
});
export const DocSchema = z.object({
releases: z.array(ReleaseSchema).optional(),
repositories: z.array(RepositorySchema).optional(),
});

View file

@ -1,19 +1,9 @@
export interface Release {
name: string;
chart: string;
version: string;
strategicMergePatches?: unknown;
jsonPatches?: unknown;
transformers?: unknown;
}
import type { z } from 'zod';
interface Repository {
name: string;
url: string;
oci?: boolean;
}
import type { DocSchema, ReleaseSchema, RepositorySchema } from './schema';
export interface Doc {
releases?: Release[];
repositories?: Repository[];
}
export type Release = z.infer<typeof ReleaseSchema>;
export type Repository = z.infer<typeof RepositorySchema>;
export type Doc = z.infer<typeof DocSchema>;

View file

@ -1,7 +1,14 @@
import yaml from 'js-yaml';
import upath from 'upath';
import { getParentDir, localPathExists } from '../../../util/fs';
import type { Release } from './types';
import * as hostRules from '../../../util/host-rules';
import { DockerDatasource } from '../../datasource/docker';
import { generateLoginCmd } from '../helmv3/common';
import type { RepositoryRule } from '../helmv3/types';
import { DocSchema } from './schema';
import type { Doc, Release, Repository } from './types';
/** Returns true if a helmfile release contains kustomize specific keys **/
export function kustomizationsKeysUsed(release: Release): boolean {
@ -23,3 +30,29 @@ export async function localChartHasKustomizationsYaml(
upath.join(helmfileYamlParentDir, release.chart, 'kustomization.yaml')
);
}
export function parseDoc(packageFileContent: string): Doc {
const doc = yaml.load(packageFileContent);
return DocSchema.parse(doc);
}
export function isOCIRegistry(repository: Repository): boolean {
return repository.oci === true;
}
export function generateRegistryLoginCmd(
repositoryName: string,
repositoryBaseURL: string,
repositoryHost: string
): string | null {
const repositoryRule: RepositoryRule = {
name: repositoryName,
repository: repositoryHost,
hostRule: hostRules.find({
url: repositoryBaseURL,
hostType: DockerDatasource.id,
}),
};
return generateLoginCmd(repositoryRule, 'helm registry login');
}