diff --git a/lib/manager/helmv3/__snapshots__/artifacts.spec.ts.snap b/lib/manager/helmv3/__snapshots__/artifacts.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..da0ed762c322695c73821027ba202436be8f4989 --- /dev/null +++ b/lib/manager/helmv3/__snapshots__/artifacts.spec.ts.snap @@ -0,0 +1,116 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`.updateArtifacts() catches errors 1`] = ` +Array [ + Object { + "artifactError": Object { + "lockFile": "Chart.lock", + "stderr": "not found", + }, + }, +] +`; + +exports[`.updateArtifacts() returns null if unchanged 1`] = ` +Array [ + Object { + "cmd": "helm dependency update ''", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`.updateArtifacts() returns updated Chart.lock 1`] = ` +Array [ + Object { + "cmd": "helm dependency update ''", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`.updateArtifacts() returns updated Chart.lock for lockfile maintenance 1`] = ` +Array [ + Object { + "cmd": "helm dependency update ''", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`.updateArtifacts() returns updated Chart.lock with docker 1`] = ` +Array [ + Object { + "cmd": "docker pull renovate/helm", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker ps --filter name=renovate_helm -aq", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker run --rm --name=renovate_helm --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/helm bash -l -c \\"helm dependency update ''\\"", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; diff --git a/lib/manager/helmv3/artifacts.spec.ts b/lib/manager/helmv3/artifacts.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2723973b7c3992e1c67d33ae086074678d6f4a6 --- /dev/null +++ b/lib/manager/helmv3/artifacts.spec.ts @@ -0,0 +1,136 @@ +import { exec as _exec } from 'child_process'; +import _fs from 'fs-extra'; +import { join } from 'upath'; +import { envMock, mockExecAll } from '../../../test/execUtil'; +import { git, mocked } from '../../../test/util'; +import { setExecConfig } from '../../util/exec'; +import { BinarySource } from '../../util/exec/common'; +import * as docker from '../../util/exec/docker'; +import * as _env from '../../util/exec/env'; +import * as helmv3 from './artifacts'; + +jest.mock('fs-extra'); +jest.mock('child_process'); +jest.mock('../../util/exec/env'); +jest.mock('../../util/git'); +jest.mock('../../util/http'); + +const fs: jest.Mocked<typeof _fs> = _fs as any; +const exec: jest.Mock<typeof _exec> = _exec as any; +const env = mocked(_env); + +const config = { + // `join` fixes Windows CI + localDir: join('/tmp/github/some/repo'), + dockerUser: 'foobar', +}; + +describe('.updateArtifacts()', () => { + beforeEach(async () => { + jest.resetAllMocks(); + jest.resetModules(); + + env.getChildProcessEnv.mockReturnValue(envMock.basic); + await setExecConfig(config); + docker.resetPrefetchedImages(); + }); + it('returns null if no Chart.lock found', async () => { + const updatedDeps = ['dep1']; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: '', + config, + }) + ).toBeNull(); + }); + it('returns null if updatedDeps is empty', async () => { + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps: [], + newPackageFileContent: '', + config, + }) + ).toBeNull(); + }); + it('returns null if unchanged', async () => { + fs.readFile.mockResolvedValueOnce('Current Chart.lock' as any); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockResolvedValueOnce('Current Chart.lock' as any); + const updatedDeps = ['dep1']; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: '', + config, + }) + ).toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('returns updated Chart.lock', async () => { + git.getFile.mockResolvedValueOnce('Old Chart.lock'); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockResolvedValueOnce('New Chart.lock' as any); + const updatedDeps = ['dep1']; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + + it('returns updated Chart.lock for lockfile maintenance', async () => { + git.getFile.mockResolvedValueOnce('Old Chart.lock'); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockResolvedValueOnce('New Chart.lock' as any); + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps: [], + newPackageFileContent: '{}', + config: { ...config, updateType: 'lockFileMaintenance' }, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + + it('returns updated Chart.lock with docker', async () => { + jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce(); + await setExecConfig({ ...config, binarySource: BinarySource.Docker }); + git.getFile.mockResolvedValueOnce('Old Chart.lock'); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockResolvedValueOnce('New Chart.lock' as any); + const updatedDeps = ['dep1']; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('catches errors', async () => { + fs.readFile.mockResolvedValueOnce('Current Chart.lock' as any); + fs.outputFile.mockImplementationOnce(() => { + throw new Error('not found'); + }); + const updatedDeps = ['dep1']; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).toMatchSnapshot(); + }); +}); diff --git a/lib/manager/helmv3/artifacts.ts b/lib/manager/helmv3/artifacts.ts new file mode 100644 index 0000000000000000000000000000000000000000..92968ae2b9d35c301b58a655c5f27c6b8d55477a --- /dev/null +++ b/lib/manager/helmv3/artifacts.ts @@ -0,0 +1,76 @@ +import { quote } from 'shlex'; +import { logger } from '../../logger'; +import { ExecOptions, exec } from '../../util/exec'; +import { + getSiblingFileName, + getSubDirectory, + readLocalFile, + writeLocalFile, +} from '../../util/fs'; +import { UpdateArtifact, UpdateArtifactsResult } from '../common'; + +async function helmUpdate(manifestPath: string): Promise<void> { + const cmd = `helm dependency update ${quote(getSubDirectory(manifestPath))}`; + + const execOptions: ExecOptions = { + docker: { + image: 'renovate/helm', + }, + }; + await exec(cmd, execOptions); +} + +export async function updateArtifacts({ + packageFileName, + updatedDeps, + newPackageFileContent, + config, +}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> { + logger.debug(`helmv3.updateArtifacts(${packageFileName})`); + + const isLockFileMaintenance = config.updateType === 'lockFileMaintenance'; + + if ( + !isLockFileMaintenance && + (updatedDeps === undefined || updatedDeps.length < 1) + ) { + logger.debug('No updated helmv3 deps - returning null'); + return null; + } + + const lockFileName = getSiblingFileName(packageFileName, 'Chart.lock'); + const existingLockFileContent = await readLocalFile(lockFileName); + if (!existingLockFileContent) { + logger.debug('No Chart.lock found'); + return null; + } + try { + await writeLocalFile(packageFileName, newPackageFileContent); + logger.debug('Updating ' + lockFileName); + await helmUpdate(packageFileName); + logger.debug('Returning updated Chart.lock'); + const newHelmLockContent = await readLocalFile(lockFileName); + if (existingLockFileContent === newHelmLockContent) { + logger.debug('Chart.lock is unchanged'); + return null; + } + return [ + { + file: { + name: lockFileName, + contents: newHelmLockContent, + }, + }, + ]; + } catch (err) { + logger.warn({ err }, 'Failed to update Helm lock file'); + return [ + { + artifactError: { + lockFile: lockFileName, + stderr: err.message, + }, + }, + ]; + } +} diff --git a/lib/manager/helmv3/index.ts b/lib/manager/helmv3/index.ts index 0660e2e055846b79d4fcc18952d1eddbc1bd17ae..0152904b54d2f399447b335df06a9c0d0d766d01 100644 --- a/lib/manager/helmv3/index.ts +++ b/lib/manager/helmv3/index.ts @@ -1,5 +1,8 @@ +export { updateArtifacts } from './artifacts'; export { extractPackageFile } from './extract'; +export const supportsLockFileMaintenance = true; + export const defaultConfig = { aliases: { stable: 'https://kubernetes-charts.storage.googleapis.com/',