mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-14 08:36:26 +00:00
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:
parent
4e8591eda6
commit
b52d7255a6
6 changed files with 271 additions and 20 deletions
|
@ -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([
|
it.each([
|
||||||
{
|
{
|
||||||
binarySource: 'docker',
|
binarySource: 'docker',
|
||||||
|
@ -163,6 +314,10 @@ describe('modules/manager/helmfile/artifacts', () => {
|
||||||
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
|
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
|
||||||
'-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
|
'-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
|
||||||
'-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' +
|
'-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 BUILDPACK_CACHE_DIR ' +
|
||||||
'-e CONTAINERBASE_CACHE_DIR ' +
|
'-e CONTAINERBASE_CACHE_DIR ' +
|
||||||
'-w "/tmp/github/some/repo" ' +
|
'-w "/tmp/github/some/repo" ' +
|
||||||
|
|
|
@ -2,6 +2,7 @@ import is from '@sindresorhus/is';
|
||||||
import { quote } from 'shlex';
|
import { quote } from 'shlex';
|
||||||
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
|
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
|
||||||
import { logger } from '../../../logger';
|
import { logger } from '../../../logger';
|
||||||
|
import { coerceArray } from '../../../util/array';
|
||||||
import { exec } from '../../../util/exec';
|
import { exec } from '../../../util/exec';
|
||||||
import type { ToolConstraint } from '../../../util/exec/types';
|
import type { ToolConstraint } from '../../../util/exec/types';
|
||||||
import {
|
import {
|
||||||
|
@ -10,7 +11,10 @@ import {
|
||||||
writeLocalFile,
|
writeLocalFile,
|
||||||
} from '../../../util/fs';
|
} from '../../../util/fs';
|
||||||
import { getFile } from '../../../util/git';
|
import { getFile } from '../../../util/git';
|
||||||
|
import { regEx } from '../../../util/regex';
|
||||||
|
import { generateHelmEnvs } from '../helmv3/common';
|
||||||
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
|
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
|
||||||
|
import { generateRegistryLoginCmd, isOCIRegistry, parseDoc } from './utils';
|
||||||
|
|
||||||
export async function updateArtifacts({
|
export async function updateArtifacts({
|
||||||
packageFileName,
|
packageFileName,
|
||||||
|
@ -59,9 +63,27 @@ export async function updateArtifacts({
|
||||||
constraint: config.constraints?.kustomize,
|
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: {},
|
docker: {},
|
||||||
extraEnv: {},
|
extraEnv: generateHelmEnvs(),
|
||||||
toolConstraints,
|
toolConstraints,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 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.
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
21
lib/modules/manager/helmfile/schema.ts
Normal file
21
lib/modules/manager/helmfile/schema.ts
Normal 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(),
|
||||||
|
});
|
|
@ -1,19 +1,9 @@
|
||||||
export interface Release {
|
import type { z } from 'zod';
|
||||||
name: string;
|
|
||||||
chart: string;
|
|
||||||
version: string;
|
|
||||||
strategicMergePatches?: unknown;
|
|
||||||
jsonPatches?: unknown;
|
|
||||||
transformers?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Repository {
|
import type { DocSchema, ReleaseSchema, RepositorySchema } from './schema';
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
oci?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Doc {
|
export type Release = z.infer<typeof ReleaseSchema>;
|
||||||
releases?: Release[];
|
|
||||||
repositories?: Repository[];
|
export type Repository = z.infer<typeof RepositorySchema>;
|
||||||
}
|
|
||||||
|
export type Doc = z.infer<typeof DocSchema>;
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
import yaml from 'js-yaml';
|
||||||
import upath from 'upath';
|
import upath from 'upath';
|
||||||
|
|
||||||
import { getParentDir, localPathExists } from '../../../util/fs';
|
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 **/
|
/** Returns true if a helmfile release contains kustomize specific keys **/
|
||||||
export function kustomizationsKeysUsed(release: Release): boolean {
|
export function kustomizationsKeysUsed(release: Release): boolean {
|
||||||
|
@ -23,3 +30,29 @@ export async function localChartHasKustomizationsYaml(
|
||||||
upath.join(helmfileYamlParentDir, release.chart, 'kustomization.yaml')
|
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');
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue