Files
renovate/lib/modules/manager/helmfile/extract.spec.ts
Sebastian Poxhofer be13c9f5f7 feat(manager): share OCI Helm chart resolution across managers (#45760)
* feat(manager): share OCI Helm chart resolution across managers

Add getOciChartDep() to helmv3/oci.ts and use it in argocd, fleet, flux,
helmfile, helmsman, helmv3, kustomize, sveltos, terraform and vendir.

- set pinDigests: false for OCI charts in all of them
- apply registryAliases to OCI charts in argocd, helmfile and helmsman
- keep the registry port in terraform helm_release repositories
- skip fleet OCI charts without a version instead of using 'undefined'

* docs(manager/helmv3): correct the OCI chart pinDigests rationale

* refactor(manager/helmfile): drop redundant OCI repository url guard

The repository schema already requires url to be a string, so the falsy
branch could not be reached by parsed input and left the patch partially
covered.
2026-09-09 13:51:30 +00:00

669 lines
19 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { Fixtures } from '~test/fixtures.ts';
import { fs, logger } from '~test/util.ts';
import { GlobalConfig } from '../../../config/global.ts';
import { FILE_ACCESS_VIOLATION_ERROR } from '../../../constants/error-messages.ts';
import { extractPackageFile } from './index.ts';
vi.mock('../../../util/fs/index.ts');
const localDir = '/tmp/github/some/repo';
describe('modules/manager/helmfile/extract', () => {
describe('extractPackageFile()', () => {
beforeEach(() => {
GlobalConfig.set({ localDir });
});
it('skip null YAML document', async () => {
const content = codeBlock`
~
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toBeNull();
});
it('returns null if no releases', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toBeNull();
});
it('do not crash on invalid helmfile.yaml', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases: [
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toBeNull();
});
it('skip if repository details are not specified', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: experimental/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result?.deps).toMatchObject([
{ depName: 'example', skipReason: 'unknown-registry' },
]);
});
it('skip templetized release with invalid characters', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: stable/!!!!--!
- name: example-internal
version: 1.0.0
chart: stable/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result?.deps).toMatchObject([
{ depName: '!!!!--!', skipReason: 'unsupported-chart-type' },
{ currentValue: '1.0.0', depName: 'example' },
]);
});
it('skip local charts', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: ./charts/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toEqual({
datasource: 'helm',
deps: [
{
depName: 'example',
skipReason: 'local-chart',
},
],
});
});
it('skip chart with unknown repository', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result?.deps).toMatchObject([
{ depName: 'example', skipReason: 'unknown-registry' },
]);
});
it('skip chart with special character in the name', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: kiwigrid/example/example
- name: example2
version: 1.0.0
chart: kiwigrid/example?example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result?.deps).toMatchObject([
{ depName: 'example/example', skipReason: 'unsupported-chart-type' },
{ depName: 'example?example', skipReason: 'unsupported-chart-type' },
]);
});
it('skip chart that does not have specified version', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
chart: stable/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toEqual({
datasource: 'helm',
deps: [
{
depName: 'example',
skipReason: 'invalid-version',
},
],
});
});
it('parses multidoc yaml', async () => {
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(
Fixtures.get('multidoc.yaml'),
fileName,
{
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
},
);
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{ depName: 'manifests', skipReason: 'local-chart' },
{
depName: 'rabbitmq',
currentValue: '7.4.3',
registryUrls: ['https://charts.bitnami.com/bitnami'],
},
{
depName: 'kube-prometheus-stack',
currentValue: '13.7',
registryUrls: [
'https://prometheus-community.github.io/helm-charts',
],
},
{ depName: 'external-dns', skipReason: 'invalid-version' },
{
depName: 'raw',
currentValue: '0.1.0',
registryUrls: ['https://charts.helm.sh/incubator/'],
},
],
managerData: { needKustomize: true },
});
});
it('parses a chart with a go templating', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
{{- if neq .Values.example.version "" }}
version: {{ .Values.example.version }}
{{- else }}
version: 1.0.0
{{- end }}
chart: stable/example
- name: example-internal
version: 1.0.0
chart: stable/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
currentValue: '1.0.0',
depName: 'example',
},
{
currentValue: '1.0.0',
depName: 'example',
},
],
});
});
it('parses a chart with empty strings for template values', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: {{ .Values.example.version }}
chart: stable/example
- name: example-external
version: 1.0.0
chart: {{ .Values.example.repository }}
- name: example-internal
version: 1.0.0
chart: stable/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
skipReason: 'invalid-version',
},
{
currentValue: '1.0.0',
depName: 'example',
},
],
});
});
it('parses a chart with an oci repository and non-oci one', async () => {
const content = `
repositories:
- name: oci-repo
url: ghcr.io/example/oci-repo
oci: true
- name: jenkins
url: https://charts.jenkins.io
releases:
- name: example
version: 0.1.0
chart: oci-repo/example
- name: jenkins
chart: jenkins/jenkins
version: 3.3.0
- name: oci-url
version: 0.4.2
chart: oci://ghcr.io/example/oci-repo/url-example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toStrictEqual({
datasource: 'helm',
deps: [
{
currentValue: '0.1.0',
depName: 'example',
datasource: 'docker',
packageName: 'ghcr.io/example/oci-repo/example',
pinDigests: false,
},
{
currentValue: '3.3.0',
depName: 'jenkins',
registryUrls: ['https://charts.jenkins.io'],
},
{
currentValue: '0.4.2',
depName: 'ghcr.io/example/oci-repo/url-example',
datasource: 'docker',
packageName: 'ghcr.io/example/oci-repo/url-example',
pinDigests: false,
},
],
});
});
it('resolves registryAliases for OCI charts', async () => {
const content = codeBlock`
repositories:
- name: oci-repo
url: ghcr.io/example/oci-repo
oci: true
releases:
- name: example
version: 0.1.0
chart: oci-repo/example
- name: oci-url
version: 0.4.2
chart: oci://ghcr.io/example/oci-repo/url-example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
'ghcr.io': 'ghcr.proxy.test',
},
});
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
currentValue: '0.1.0',
depName: 'example',
datasource: 'docker',
packageName: 'ghcr.proxy.test/example/oci-repo/example',
pinDigests: false,
},
{
currentValue: '0.4.2',
depName: 'ghcr.io/example/oci-repo/url-example',
datasource: 'docker',
packageName: 'ghcr.proxy.test/example/oci-repo/url-example',
pinDigests: false,
},
],
});
});
it('allows OCI chart names containing forward slashes', async () => {
const content = `
repositories:
- name: oci-repo
url: ghcr.io/example/oci-repo
oci: true
releases:
- name: nested-example
version: 1.2.3
chart: oci-repo/nested/path/chart
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {});
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
currentValue: '1.2.3',
depName: 'nested/path/chart',
datasource: 'docker',
packageName: 'ghcr.io/example/oci-repo/nested/path/chart',
pinDigests: false,
},
],
});
});
it('parses a chart with an oci repository with ---', async () => {
const content = codeBlock`
repositories:
- name: oci-repo
url: ghcr.io/example/oci-repo
oci: true
---
releases:
- name: example
version: 0.1.0
chart: oci-repo/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
currentValue: '0.1.0',
depName: 'example',
datasource: 'docker',
packageName: 'ghcr.io/example/oci-repo/example',
pinDigests: false,
},
],
});
});
it('parses and replaces templating strings', async () => {
const filename = 'helmfile.yaml';
fs.localPathExists.mockReturnValue(Promise.resolve(true));
const result = await extractPackageFile(
Fixtures.get('go-template.yaml'),
filename,
{
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
},
);
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
depName: '',
skipReason: 'local-chart',
},
{
depName: '',
skipReason: 'local-chart',
},
{
depName: '',
skipReason: 'local-chart',
},
{
depName: 'ingress-nginx',
currentValue: '3.37.0',
registryUrls: [],
skipReason: 'unknown-registry',
},
{
depName: 'memcached',
currentValue: '6.0.0',
registryUrls: ['https://charts.bitnami.com/bitnami'],
},
{
depName: 'example',
currentValue: '1.30.0',
registryUrls: ['https://charts.helm.sh/stable'],
},
{ depName: 'kube-prometheus-stack', skipReason: 'invalid-version' },
{
depName: 'external-dns',
currentValue: '2.0.0',
registryUrls: ['https://charts.helm.sh/stable'],
},
],
managerData: { needKustomize: true },
});
});
it('detects kustomize and respects relative paths', async () => {
fs.localPathExists.mockImplementationOnce((path) => {
if (!path.startsWith(GlobalConfig.get('localDir'))) {
throw new Error(FILE_ACCESS_VIOLATION_ERROR);
}
return Promise.resolve(true);
});
const parentDir = `${localDir}/project`;
fs.getParentDir.mockReturnValue(parentDir);
const result = await extractPackageFile(
Fixtures.get('uses-kustomization.yaml'),
`${parentDir}/helmfile.yaml`, // In subdir
{
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
},
);
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
depName: 'my-chart',
skipReason: 'local-chart',
},
{
depName: 'memcached',
currentValue: '6.0.0',
registryUrls: ['https://charts.bitnami.com/bitnami'],
},
],
managerData: { needKustomize: true },
});
});
it('makes sure url joiner works correctly', async () => {
const content = codeBlock`
releases:
- name: argocd
version: 0.4.2
chart: oci://gitlab.example.com:5000/group/subgroup
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
registryAliases: {
stable: 'https://charts.helm.sh/stable',
},
});
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
currentValue: '0.4.2',
datasource: 'docker',
depName: 'gitlab.example.com:5000/group/subgroup',
packageName: 'gitlab.example.com:5000/group/subgroup',
pinDigests: false,
},
],
});
});
it('skips helm-git repos', async () => {
const content = codeBlock`
repositories:
# Official codefresh helm chart
- name: gitops-external-cluster
url: git+https://github.com/codefresh-io/csdp-official@add-cluster/helm
releases:
- name: gitops-external-cluster
namespace: gitops-runtime
chart: gitops-external-cluster/csdp-add-cluster
version: 0.4.0
`;
const result = await extractPackageFile(content, 'helmfile.yaml', {});
expect(logger.logger.debug).toHaveBeenCalledWith(
{
repo: {
name: 'gitops-external-cluster',
url: 'git+https://github.com/codefresh-io/csdp-official@add-cluster/helm',
},
packageFile: 'helmfile.yaml',
},
'Skipping unsupported helm-git repository.',
);
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
currentValue: '0.4.0',
depName: 'csdp-add-cluster',
skipReason: 'unknown-registry',
},
],
});
});
it('parses templates key alongside releases', async () => {
const content = codeBlock`
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
templates:
common:
name: common
version: 1.0.0
chart: kiwigrid/common
shared:
name: shared
version: 2.0.0
chart: kiwigrid/shared
releases:
- name: my-release
version: 3.0.0
chart: kiwigrid/my-chart
`;
const result = await extractPackageFile(content, 'helmfile.yaml', {});
expect(result).toMatchObject({
datasource: 'helm',
deps: [
{
currentValue: '3.0.0',
depName: 'my-chart',
},
{
currentValue: '1.0.0',
depName: 'common',
},
{
currentValue: '2.0.0',
depName: 'shared',
},
],
});
});
});
});