diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 70691a6a6b2ca4d92ef5d664bec42891c4aa425e..4056537b36949ab8856bced3e260c45c4f4576d2 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 dbffad7a061cc80ffe249679075706cc353045bd..f3c223931622f606a4edda94325cb95d0ce698dd 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 480dfb55123ecbd04340ec21875697986766dbca..c486ad423ba1d09b6e3277a100aedb432743c2d2 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 259998d82abe2d40fd1519fcc5994b9c3dbc5e05..5e94f2c71387a5f7ce970fa2dec256158afb7b76 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 e002d6c92bef3ad864e3879dacae1503d283307c..be803f67304ee2f5c4c867fdab56341ed8f45b1e 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; +}