From d3dba2cd147f31d0335ee7b7bc7ea4a1452cf11b Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer <secustor@users.noreply.github.com> Date: Tue, 11 Oct 2022 18:00:52 +0200 Subject: [PATCH] feat(manager/helmv3): add postUpdateOption to update subchart archives (#18162) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- lib/config/options/index.ts | 1 + lib/modules/manager/helmv3/artifacts.spec.ts | 244 ++++++++++++++++++- lib/modules/manager/helmv3/artifacts.ts | 83 +++++-- lib/modules/manager/helmv3/readme.md | 11 + lib/modules/manager/helmv3/utils.ts | 5 + 5 files changed, 326 insertions(+), 18 deletions(-) diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 70691a6a6b..4056537b36 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1908,6 +1908,7 @@ const options: RenovateOptions[] = [ default: [], allowedValues: [ 'bundlerConservative', + 'helmUpdateSubChartArchives', 'gomodMassage', 'gomodUpdateImportPaths', 'gomodTidy', diff --git a/lib/modules/manager/helmv3/artifacts.spec.ts b/lib/modules/manager/helmv3/artifacts.spec.ts index dbffad7a06..f3c2239316 100644 --- a/lib/modules/manager/helmv3/artifacts.spec.ts +++ b/lib/modules/manager/helmv3/artifacts.spec.ts @@ -1,10 +1,11 @@ import { join } from 'upath'; import { envMock, mockExecAll } from '../../../../test/exec-util'; import { Fixtures } from '../../../../test/fixtures'; -import { env, fs, mocked } from '../../../../test/util'; +import { env, fs, git, mocked } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import * as docker from '../../../util/exec/docker'; +import type { StatusResult } from '../../../util/git/types'; import * as hostRules from '../../../util/host-rules'; import * as _datasource from '../../datasource'; import type { UpdateArtifactsConfig } from '../types'; @@ -14,6 +15,7 @@ jest.mock('../../datasource'); jest.mock('../../../util/exec/env'); jest.mock('../../../util/http'); jest.mock('../../../util/fs'); +jest.mock('../../../util/git'); const datasource = mocked(_datasource); @@ -211,6 +213,246 @@ describe('modules/manager/helmv3/artifacts', () => { ]); }); + it('add sub chart artifacts to file list if Chart.lock exists', async () => { + fs.readLocalFile.mockResolvedValueOnce(ociLockFile1 as never); + fs.getSiblingFileName.mockReturnValueOnce('Chart.lock'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce(ociLockFile2 as never); + fs.privateCacheDir.mockReturnValue( + '/tmp/renovate/cache/__renovate-private-cache' + ); + fs.getParentDir.mockReturnValue(''); + + // sub chart artifacts + fs.getSiblingFileName.mockReturnValueOnce('charts'); + git.getRepoStatus.mockResolvedValueOnce({ + not_added: ['charts/example-1.9.2.tgz'], + deleted: ['charts/example-1.6.2.tgz'], + } as StatusResult); + const updatedDeps = [{ depName: 'dep1' }]; + const test = await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: chartFile, + config: { + postUpdateOptions: ['helmUpdateSubChartArchives'], + ...config, + }, + }); + expect(test).toEqual([ + { + file: { + type: 'addition', + path: 'Chart.lock', + contents: ociLockFile2, + }, + }, + { + file: { + type: 'addition', + path: 'charts/example-1.9.2.tgz', + contents: undefined, + }, + }, + { + file: { + type: 'deletion', + path: 'charts/example-1.6.2.tgz', + }, + }, + ]); + expect(execSnapshots).toMatchObject([ + { + cmd: 'helm repo add repo-test --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories https://gitlab.com/api/v4/projects/xxxxxxx/packages/helm/stable', + }, + { + cmd: "helm dependency update --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories ''", + }, + ]); + }); + + it('add sub chart artifacts to file list if Chart.lock is missing', async () => { + fs.readLocalFile.mockResolvedValueOnce(null); + fs.getSiblingFileName.mockReturnValueOnce('Chart.lock'); + const execSnapshots = mockExecAll(); + fs.privateCacheDir.mockReturnValue( + '/tmp/renovate/cache/__renovate-private-cache' + ); + fs.getParentDir.mockReturnValue(''); + + // sub chart artifacts + fs.getSiblingFileName.mockReturnValueOnce('charts'); + git.getRepoStatus.mockResolvedValueOnce({ + not_added: ['charts/example-1.9.2.tgz'], + deleted: ['charts/example-1.6.2.tgz'], + } as StatusResult); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: chartFile, + config: { + postUpdateOptions: ['helmUpdateSubChartArchives'], + ...config, + }, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'charts/example-1.9.2.tgz', + contents: undefined, + }, + }, + { + file: { + type: 'deletion', + path: 'charts/example-1.6.2.tgz', + }, + }, + ]); + expect(execSnapshots).toMatchObject([ + { + cmd: 'helm repo add repo-test --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories https://gitlab.com/api/v4/projects/xxxxxxx/packages/helm/stable', + }, + { + cmd: "helm dependency update --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories ''", + }, + ]); + }); + + it('add sub chart artifacts without old archives', async () => { + fs.readLocalFile.mockResolvedValueOnce(null); + fs.getSiblingFileName.mockReturnValueOnce('Chart.lock'); + const execSnapshots = mockExecAll(); + fs.privateCacheDir.mockReturnValue( + '/tmp/renovate/cache/__renovate-private-cache' + ); + fs.getParentDir.mockReturnValue(''); + + // sub chart artifacts + fs.getSiblingFileName.mockReturnValueOnce('charts'); + git.getRepoStatus.mockResolvedValueOnce({ + not_added: ['charts/example-1.9.2.tgz'], + } as StatusResult); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: chartFile, + config: { + postUpdateOptions: ['helmUpdateSubChartArchives'], + ...config, + }, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'charts/example-1.9.2.tgz', + contents: undefined, + }, + }, + ]); + expect(execSnapshots).toMatchObject([ + { + cmd: 'helm repo add repo-test --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories https://gitlab.com/api/v4/projects/xxxxxxx/packages/helm/stable', + }, + { + cmd: "helm dependency update --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories ''", + }, + ]); + }); + + it('add sub chart artifacts and ignore files outside of the chart folder', async () => { + fs.readLocalFile.mockResolvedValueOnce(null); + fs.getSiblingFileName.mockReturnValueOnce('Chart.lock'); + const execSnapshots = mockExecAll(); + fs.privateCacheDir.mockReturnValue( + '/tmp/renovate/cache/__renovate-private-cache' + ); + fs.getParentDir.mockReturnValue(''); + + // sub chart artifacts + fs.getSiblingFileName.mockReturnValueOnce('charts'); + git.getRepoStatus.mockResolvedValueOnce({ + not_added: ['charts/example-1.9.2.tgz', 'exampleFile'], + deleted: ['charts/example-1.6.2.tgz', 'aFolder/otherFile'], + } as StatusResult); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: chartFile, + config: { + postUpdateOptions: ['helmUpdateSubChartArchives'], + ...config, + }, + }) + ).toEqual([ + { + file: { + type: 'addition', + path: 'charts/example-1.9.2.tgz', + contents: undefined, + }, + }, + { + file: { + type: 'deletion', + path: 'charts/example-1.6.2.tgz', + }, + }, + ]); + expect(execSnapshots).toMatchObject([ + { + cmd: 'helm repo add repo-test --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories https://gitlab.com/api/v4/projects/xxxxxxx/packages/helm/stable', + }, + { + cmd: "helm dependency update --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories ''", + }, + ]); + }); + + it('skip artifacts which are not lock files or in the chart folder', async () => { + fs.readLocalFile.mockResolvedValueOnce(null); + fs.getSiblingFileName.mockReturnValueOnce('Chart.lock'); + const execSnapshots = mockExecAll(); + fs.privateCacheDir.mockReturnValue( + '/tmp/renovate/cache/__renovate-private-cache' + ); + fs.getParentDir.mockReturnValue(''); + + // sub chart artifacts + fs.getSiblingFileName.mockReturnValueOnce('charts'); + git.getRepoStatus.mockResolvedValueOnce({ + modified: ['example/example.tgz'], + } as StatusResult); + const updatedDeps = [{ depName: 'dep1' }]; + expect( + await helmv3.updateArtifacts({ + packageFileName: 'Chart.yaml', + updatedDeps, + newPackageFileContent: chartFile, + config: { + postUpdateOptions: ['helmUpdateSubChartArchives'], + ...config, + }, + }) + ).toBeNull(); + expect(execSnapshots).toMatchObject([ + { + cmd: 'helm repo add repo-test --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories https://gitlab.com/api/v4/projects/xxxxxxx/packages/helm/stable', + }, + { + cmd: "helm dependency update --registry-config /tmp/renovate/cache/__renovate-private-cache/registry.json --repository-config /tmp/renovate/cache/__renovate-private-cache/repositories.yaml --repository-cache /tmp/renovate/cache/__renovate-private-cache/repositories ''", + }, + ]); + }); + it('sets repositories from registryAliases', async () => { fs.privateCacheDir.mockReturnValue( '/tmp/renovate/cache/__renovate-private-cache' diff --git a/lib/modules/manager/helmv3/artifacts.ts b/lib/modules/manager/helmv3/artifacts.ts index 480dfb5512..c486ad423b 100644 --- a/lib/modules/manager/helmv3/artifacts.ts +++ b/lib/modules/manager/helmv3/artifacts.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import yaml from 'js-yaml'; import { quote } from 'shlex'; import upath from 'upath'; @@ -12,6 +13,7 @@ import { readLocalFile, writeLocalFile, } from '../../../util/fs'; +import { getRepoStatus } from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; import { DockerDatasource } from '../../datasource/docker'; import { HelmDatasource } from '../../datasource/helm'; @@ -20,6 +22,7 @@ import type { ChartDefinition, Repository, RepositoryRule } from './types'; import { aliasRecordToRepositories, getRepositories, + isFileInDir, isOCIRegistry, } from './utils'; @@ -110,6 +113,9 @@ export async function updateArtifacts({ logger.debug(`helmv3.updateArtifacts(${packageFileName})`); const isLockFileMaintenance = config.updateType === 'lockFileMaintenance'; + const isUpdateOptionAddChartArchives = config.postUpdateOptions?.includes( + 'helmUpdateSubChartArchives' + ); if ( !isLockFileMaintenance && @@ -121,14 +127,16 @@ export async function updateArtifacts({ const lockFileName = getSiblingFileName(packageFileName, 'Chart.lock'); const existingLockFileContent = await readLocalFile(lockFileName, 'utf8'); - if (!existingLockFileContent) { + if (!existingLockFileContent && !isUpdateOptionAddChartArchives) { logger.debug('No Chart.lock found'); return null; } try { // get repositories and registries defined in the package file const packages = yaml.load(newPackageFileContent) as ChartDefinition; //TODO #9610 - const locks = yaml.load(existingLockFileContent) as ChartDefinition; //TODO #9610 + const locks = existingLockFileContent + ? (yaml.load(existingLockFileContent) as ChartDefinition) + : { dependencies: [] }; //TODO #9610 const chartDefinitions: ChartDefinition[] = []; // prioritize registryAlias naming for Helm repositories @@ -142,7 +150,7 @@ export async function updateArtifacts({ const repositories = getRepositories(chartDefinitions); await writeLocalFile(packageFileName, newPackageFileContent); - logger.debug('Updating ' + lockFileName); + logger.debug('Updating Helm artifacts'); const helmToolConstraint: ToolConstraint = { toolName: 'helm', constraint: config.constraints?.helm, @@ -158,21 +166,62 @@ export async function updateArtifacts({ toolConstraints: [helmToolConstraint], }; await helmCommands(execOptions, packageFileName, repositories); - logger.debug('Returning updated Chart.lock'); - const newHelmLockContent = await readLocalFile(lockFileName, 'utf8'); - if (existingLockFileContent === newHelmLockContent) { - logger.debug('Chart.lock is unchanged'); - return null; + logger.debug('Returning updated Helm artifacts'); + + const fileChanges: UpdateArtifactsResult[] = []; + + if (is.truthy(existingLockFileContent)) { + const newHelmLockContent = await readLocalFile(lockFileName, 'utf8'); + const isLockFileChanged = existingLockFileContent !== newHelmLockContent; + if (isLockFileChanged) { + fileChanges.push({ + file: { + type: 'addition', + path: lockFileName, + contents: newHelmLockContent, + }, + }); + } else { + logger.debug('Chart.lock is unchanged'); + } } - return [ - { - file: { - type: 'addition', - path: lockFileName, - contents: newHelmLockContent, - }, - }, - ]; + + // add modified helm chart archives to artifacts + if (is.truthy(isUpdateOptionAddChartArchives)) { + const chartsPath = getSiblingFileName(packageFileName, 'charts'); + const status = await getRepoStatus(); + const chartsAddition = status.not_added ?? []; + const chartsDeletion = status.deleted ?? []; + + for (const file of chartsAddition) { + // only add artifacts in the chart sub path + if (!isFileInDir(chartsPath, file)) { + continue; + } + fileChanges.push({ + file: { + type: 'addition', + path: file, + contents: await readLocalFile(file), + }, + }); + } + + for (const file of chartsDeletion) { + // only add artifacts in the chart sub path + if (!isFileInDir(chartsPath, file)) { + continue; + } + fileChanges.push({ + file: { + type: 'deletion', + path: file, + }, + }); + } + } + + return fileChanges.length > 0 ? fileChanges : null; } catch (err) { // istanbul ignore if if (err.message === TEMPORARY_ERROR) { diff --git a/lib/modules/manager/helmv3/readme.md b/lib/modules/manager/helmv3/readme.md index 259998d82a..5e94f2c713 100644 --- a/lib/modules/manager/helmv3/readme.md +++ b/lib/modules/manager/helmv3/readme.md @@ -64,3 +64,14 @@ For this you use a custom `hostRules` array. ], } ``` + +### Subchart archives + +To get updates for subchart archives put `helmUpdateSubChartArchives` in your `postUpdateOptions` configuration. +Renovate now updates archives in the `/chart` folder. + +```json +{ + "postUpdateOptions": ["helmUpdateSubChartArchives"] +} +``` diff --git a/lib/modules/manager/helmv3/utils.ts b/lib/modules/manager/helmv3/utils.ts index e002d6c92b..be803f6730 100644 --- a/lib/modules/manager/helmv3/utils.ts +++ b/lib/modules/manager/helmv3/utils.ts @@ -1,3 +1,4 @@ +import upath from 'upath'; import { logger } from '../../../logger'; import { DockerDatasource } from '../../datasource/docker'; import type { PackageDependency } from '../types'; @@ -95,3 +96,7 @@ export function aliasRecordToRepositories( }; }); } + +export function isFileInDir(dir: string, file: string): boolean { + return upath.dirname(file) === dir; +} -- GitLab