import { isNonEmptyString, isNullOrUndefined } from '@sindresorhus/is'; import { quote } from 'shlex'; import upath from 'upath'; import { TEMPORARY_ERROR } from '../../../constants/error-messages.ts'; import { logger } from '../../../logger/index.ts'; import { coerceArray } from '../../../util/array.ts'; import { exec } from '../../../util/exec/index.ts'; import type { ExecOptions, ToolConstraint } from '../../../util/exec/types.ts'; import { deleteLocalFile, getSiblingFileName, localPathExists, readLocalFile, } from '../../../util/fs/index.ts'; import { getRepoStatus } from '../../../util/git/index.ts'; import { DockerDatasource } from '../../datasource/docker/index.ts'; import { HelmDatasource } from '../../datasource/helm/index.ts'; import type { UpdateArtifact, UpdateArtifactsResult } from '../types.ts'; import { generateHelmEnvs } from './common.ts'; import { parseKustomize } from './extract.ts'; async function localExistingChartPath( chartHome: string, dependencyName: string, version?: string, ): Promise { const folderName = `${dependencyName}-${version}`; const path = upath.join(chartHome, folderName); const pathExists = await localPathExists(path); return pathExists ? path : null; } function helmRepositoryArgs( repository: string, depName: string, datasource?: string, ): string { switch (datasource) { case HelmDatasource.id: return `--repo ${quote(repository)} ${quote(depName)}`; case DockerDatasource.id: return quote(`oci://${repository}`); /* v8 ignore next: should never happen */ default: throw new Error(`Unknown datasource: ${datasource}`); } } async function inflateHelmChart( flagEnabled: boolean, execOptions: ExecOptions, chartHome: string, depName: string, repository: string, currentVersion: string, newVersion?: string, datasource?: string, ): Promise { const currentChartExistingPath = await localExistingChartPath( chartHome, depName, currentVersion, ); if (!flagEnabled && isNullOrUndefined(currentChartExistingPath)) { logger.debug( `Not inflating Helm chart for ${depName} as kustomizeInflateHelmCharts is not enabled and the current version isn't inflated`, ); return; } if ( isNonEmptyString(currentChartExistingPath) && isNonEmptyString(newVersion) ) { logger.debug(`Deleting previous helm chart: ${currentChartExistingPath}`); await deleteLocalFile(currentChartExistingPath); } const versionToPull = newVersion ?? currentVersion; const versionToPullExistingPath = await localExistingChartPath( chartHome, depName, versionToPull, ); if (isNonEmptyString(versionToPullExistingPath)) { logger.debug( `Helm chart ${depName} version ${versionToPull} already exists at ${versionToPullExistingPath}`, ); return; } const folderName = `${depName}-${versionToPull}`; const untarDir = upath.join(chartHome, folderName); logger.debug( `Pulling helm chart ${depName} version ${versionToPull} to ${untarDir}`, ); const cmd = `helm pull --untar --untardir ${quote(untarDir)} ` + `--version ${quote(versionToPull)} ${helmRepositoryArgs(repository, depName, datasource)}`; await exec(cmd, execOptions); } export async function updateArtifacts({ packageFileName, updatedDeps, newPackageFileContent, config, }: UpdateArtifact): Promise { logger.debug(`kustomize.updateArtifacts(${packageFileName})`); const project = parseKustomize(newPackageFileContent); const isUpdateOptionInflateChartArchives = config.postUpdateOptions?.includes('kustomizeInflateHelmCharts') === true; if (isNullOrUndefined(project)) { return [ { artifactError: { stderr: 'Failed to parse new package file content', }, }, ]; } const chartHome = getSiblingFileName( packageFileName, project.helmGlobals?.chartHome ?? 'charts', ); try { const helmToolConstraint: ToolConstraint = { toolName: 'helm', constraint: config.constraints?.helm, }; const execOptions: ExecOptions = { docker: {}, extraEnv: generateHelmEnvs(config), toolConstraints: [helmToolConstraint], }; for (const dependency of updatedDeps) { if (!dependency.currentVersion) { continue; } if (dependency.newVersion === dependency.currentVersion) { continue; } if (!isNonEmptyString(dependency.depName)) { continue; } if (dependency.depType !== 'HelmChart') { continue; } let repository = null; switch (dependency.datasource) { case HelmDatasource.id: repository = dependency.registryUrls?.[0]; break; case DockerDatasource.id: repository = dependency.packageName; break; } if (isNullOrUndefined(repository)) { continue; } await inflateHelmChart( isUpdateOptionInflateChartArchives, execOptions, chartHome, dependency.depName, repository, dependency.currentVersion, dependency.newVersion, dependency.datasource, ); } const status = await getRepoStatus(); const chartsAddition = coerceArray(status?.not_added); const chartsDeletion = coerceArray(status?.deleted); const fileChanges: UpdateArtifactsResult[] = []; for (const file of chartsAddition) { // only add artifacts in the chartHome path if (!file.startsWith(chartHome)) { continue; } fileChanges.push({ file: { type: 'addition', path: file, contents: await readLocalFile(file), }, }); } for (const file of chartsDeletion) { // only add artifacts in the chartHome path if (!file.startsWith(chartHome)) { continue; } fileChanges.push({ file: { type: 'deletion', path: file, }, }); } return fileChanges.length > 0 ? fileChanges : null; } catch (err) { if (err.message === TEMPORARY_ERROR) { throw err; } logger.debug({ err }, 'Failed to inflate helm chart'); return [ { artifactError: { stderr: err.message, }, }, ]; } }