diff --git a/lib/manager/helmv3/__snapshots__/extract.spec.ts.snap b/lib/manager/helmv3/__snapshots__/extract.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..624293839e15afb4a863197b0c696431188c4095 --- /dev/null +++ b/lib/manager/helmv3/__snapshots__/extract.spec.ts.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lib/manager/helm-requirements/extract extractPackageFile() parses simple Chart.yaml correctly 1`] = ` +Object { + "datasource": "helm", + "deps": Array [ + Object { + "currentValue": "0.9.0", + "depName": "redis", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com/", + ], + }, + Object { + "currentValue": "0.8.1", + "depName": "postgresql", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com/", + ], + }, + ], +} +`; + +exports[`lib/manager/helm-requirements/extract extractPackageFile() resolves aliased registry urls 1`] = ` +Object { + "datasource": "helm", + "deps": Array [ + Object { + "currentValue": "0.9.0", + "depName": "redis", + "registryUrls": Array [ + "https://my-registry.gcr.io/", + ], + }, + ], +} +`; + +exports[`lib/manager/helm-requirements/extract extractPackageFile() skips invalid registry urls 1`] = ` +Object { + "datasource": "helm", + "deps": Array [ + Object { + "currentValue": "0.9.0", + "depName": "redis", + "registryUrls": Array [ + "@placeholder", + ], + "skipReason": "placeholder-url", + }, + Object { + "currentValue": "0.8.1", + "depName": "postgresql", + "registryUrls": Array [ + "nope", + ], + "skipReason": "invalid-url", + }, + Object { + "currentValue": "0.8.1", + "depName": "broken", + "skipReason": "no-repository", + }, + ], +} +`; + +exports[`lib/manager/helm-requirements/extract extractPackageFile() skips local dependencies 1`] = ` +Object { + "datasource": "helm", + "deps": Array [ + Object { + "currentValue": "0.9.0", + "depName": "redis", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com/", + ], + }, + Object { + "currentValue": "0.8.1", + "depName": "postgresql", + "registryUrls": Array [ + "file:///some/local/path/", + ], + "skipReason": "local-dependency", + }, + ], +} +`; diff --git a/lib/manager/helmv3/extract.spec.ts b/lib/manager/helmv3/extract.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a1185a4f74a66b8839374cfebce2bf1c4c770fd --- /dev/null +++ b/lib/manager/helmv3/extract.spec.ts @@ -0,0 +1,229 @@ +import { fs } from '../../../test/util'; +import { extractPackageFile } from './extract'; + +jest.mock('../../util/fs'); + +describe('lib/manager/helm-requirements/extract', () => { + describe('extractPackageFile()', () => { + beforeEach(() => { + jest.resetAllMocks(); + fs.readLocalFile = jest.fn(); + }); + it('skips invalid registry urls', () => { + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + dependencies: + - name: redis + version: 0.9.0 + repository: '@placeholder' + - name: postgresql + version: 0.8.1 + repository: nope + - name: broken + version: 0.8.1 + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).not.toBeNull(); + expect(result).toMatchSnapshot(); + expect(result.deps.every((dep) => dep.skipReason)).toEqual(true); + }); + it('parses simple Chart.yaml correctly', () => { + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + dependencies: + - name: redis + version: 0.9.0 + repository: https://kubernetes-charts.storage.googleapis.com/ + enabled: true + - name: postgresql + version: 0.8.1 + repository: https://kubernetes-charts.storage.googleapis.com/ + condition: postgresql.enabled + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).not.toBeNull(); + expect(result).toMatchSnapshot(); + }); + it('resolves aliased registry urls', () => { + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + dependencies: + - name: redis + version: 0.9.0 + repository: '@placeholder' + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + placeholder: 'https://my-registry.gcr.io/', + }, + }); + expect(result).not.toBeNull(); + expect(result).toMatchSnapshot(); + expect(result.deps.every((dep) => dep.skipReason)).toEqual(false); + }); + it("doesn't fail if Chart.yaml is invalid", () => { + const content = ` + Invalid Chart.yaml content. + arr: + [ + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).toBeNull(); + }); + it('skips local dependencies', () => { + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + dependencies: + - name: redis + version: 0.9.0 + repository: https://kubernetes-charts.storage.googleapis.com/ + - name: postgresql + version: 0.8.1 + repository: file:///some/local/path/ + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).not.toBeNull(); + expect(result).toMatchSnapshot(); + }); + it('returns null if no dependencies key', () => { + fs.readLocalFile.mockResolvedValueOnce(` + `); + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + hello: world + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).toBeNull(); + }); + it('returns null if dependencies are an empty list', () => { + fs.readLocalFile.mockResolvedValueOnce(` + `); + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + dependencies: [] + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).toBeNull(); + }); + it('returns null if dependencies key is invalid', () => { + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + dependencies: + Invalid dependencies content. + [ + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).toBeNull(); + }); + it('returns null if Chart.yaml is empty', () => { + const content = ''; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).toBeNull(); + }); + it('returns null if Chart.yaml uses an unsupported apiVersion', () => { + const content = ` + apiVersion: v1 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).toBeNull(); + }); + it('returns null if name and version are missing for all dependencies', () => { + const content = ` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + dependencies: + - repository: "test" + - repository: "test" + alias: "test" + `; + const fileName = 'Chart.yaml'; + const result = extractPackageFile(content, fileName, { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + }); + expect(result).toBeNull(); + }); + }); +}); diff --git a/lib/manager/helmv3/extract.ts b/lib/manager/helmv3/extract.ts new file mode 100644 index 0000000000000000000000000000000000000000..01ae9f4bdd6d51f28fa065e6c9149b2f040f9e6e --- /dev/null +++ b/lib/manager/helmv3/extract.ts @@ -0,0 +1,88 @@ +import is from '@sindresorhus/is'; +import yaml from 'js-yaml'; +import * as datasourceHelm from '../../datasource/helm'; +import { logger } from '../../logger'; +import { SkipReason } from '../../types'; +import { ExtractConfig, PackageDependency, PackageFile } from '../common'; + +export function extractPackageFile( + content: string, + fileName: string, + config: ExtractConfig +): PackageFile | null { + let chart: { + apiVersion: string; + name: string; + version: string; + dependencies: Array<{ name: string; version: string; repository: string }>; + }; + try { + chart = yaml.safeLoad(content, { json: true }); + if (!(chart?.apiVersion && chart.name && chart.version)) { + logger.debug( + { fileName }, + 'Failed to find required fields in Chart.yaml' + ); + return null; + } + if (chart.apiVersion !== 'v2') { + logger.debug( + { fileName }, + 'Unsupported Chart apiVersion. Only v2 is supported.' + ); + return null; + } + } catch (err) { + logger.debug({ fileName }, 'Failed to parse helm Chart.yaml'); + return null; + } + let deps: PackageDependency[] = []; + if (!is.nonEmptyArray(chart?.dependencies)) { + logger.debug({ fileName }, 'Chart has no dependencies'); + return null; + } + const validDependencies = chart.dependencies.filter( + (dep) => is.nonEmptyString(dep.name) && is.nonEmptyString(dep.version) + ); + if (!is.nonEmptyArray(validDependencies)) { + logger.debug('Name and/or version missing for all dependencies'); + return null; + } + deps = validDependencies.map((dep) => { + const res: PackageDependency = { + depName: dep.name, + currentValue: dep.version, + }; + if (dep.repository) { + res.registryUrls = [dep.repository]; + if (dep.repository.startsWith('@')) { + const repoWithAtRemoved = dep.repository.slice(1); + const alias = config.aliases[repoWithAtRemoved]; + if (alias) { + res.registryUrls = [alias]; + return res; + } + + res.skipReason = SkipReason.PlaceholderUrl; + } else { + try { + const url = new URL(dep.repository); + if (url.protocol === 'file:') { + res.skipReason = SkipReason.LocalDependency; + } + } catch (err) { + logger.debug({ err }, 'Error parsing url'); + res.skipReason = SkipReason.InvalidUrl; + } + } + } else { + res.skipReason = SkipReason.NoRepository; + } + return res; + }); + const res = { + deps, + datasource: datasourceHelm.id, + }; + return res; +} diff --git a/lib/manager/helmv3/index.ts b/lib/manager/helmv3/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0660e2e055846b79d4fcc18952d1eddbc1bd17ae --- /dev/null +++ b/lib/manager/helmv3/index.ts @@ -0,0 +1,9 @@ +export { extractPackageFile } from './extract'; + +export const defaultConfig = { + aliases: { + stable: 'https://kubernetes-charts.storage.googleapis.com/', + }, + commitMessageTopic: 'helm chart {{depName}}', + fileMatch: ['(^|/)Chart.yaml$'], +}; diff --git a/lib/manager/helmv3/readme.md b/lib/manager/helmv3/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..7c0cd6e68b343da06659fe82cd37db2734616ee7 --- /dev/null +++ b/lib/manager/helmv3/readme.md @@ -0,0 +1,3 @@ +Renovate supports updating Helm Chart references within `requirements.yaml` (Helm v2) and `Chart.yaml` (Helm v3) files. + +If your Helm charts make use of Aliases then you will need to configure an `aliases` object in your config to tell Renovate where to look for them.