diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 294dd221287cb059b58d83bbe7862f36001354b4..d9f61c1ed5e8e5cb2a3aed3c5d185f1562b7531a 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -107,10 +107,21 @@ e.g. Renovate often needs to use third-party binaries in its PRs, e.g. `npm` to update `package-lock.json` or `go` to update `go.sum`. By default, Renovate will use a child process to run such tools, so they need to be pre-installed before running Renovate and available in the path. -As an alternative, Renovate can use "sidecar" containers for third-party tools. +Renovate can instead use "sidecar" containers for third-party tools when `binarySource=docker`. If configured, Renovate will use `docker run` to create containers such as Node.js or Python to run tools within as-needed. For this to work, `docker` needs to be installed and the Docker socket available to Renovate. +Additionally, when Renovate is run inside a container built using [`containerbase/buildpack`](https://github.com/containerbase/buildpack), such as the official Renovate images on Docker Hub, then `binarySource=install` can be used. +This mode means that Renovate will dynamically install the version of tools available, if supported. + +Supported tools for dynamic install are: + +- `composer` +- `jb` +- `npm` + +Unsupported tools will fall back to `binarySource=global`. + ## cacheDir By default Renovate uses a temporary directory like `/tmp/renovate/cache` to store cache data. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 3f12e873009b0d2dfc49a67e1badb1510d143e12..84b34978c2f00ef70c942ff9151834b76c1a21cd 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -262,10 +262,10 @@ const options: RenovateOptions[] = [ { name: 'binarySource', description: - 'Controls whether third-party tools like npm or Gradle are called directly, or via Docker sidecar containers.', + 'Controls whether third-party tools like npm or Gradle are called directly, via Docker sidecar containers, or dynamic install.', globalOnly: true, type: 'string', - allowedValues: ['global', 'docker'], + allowedValues: ['global', 'docker', 'install'], default: 'global', }, { diff --git a/lib/config/types.ts b/lib/config/types.ts index c349a7066c016ae20ee295861b1c3d6d667b1fb9..cd4e2e32aef47d5c1a2a3bebdde75939811f81d7 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -96,7 +96,7 @@ export interface RepoGlobalConfig { allowPostUpgradeCommandTemplating?: boolean; allowScripts?: boolean; allowedPostUpgradeCommands?: string[]; - binarySource?: 'docker' | 'global'; + binarySource?: 'docker' | 'global' | 'install'; customEnvVariables?: Record<string, string>; dockerChildPrefix?: string; dockerImagePrefix?: string; diff --git a/lib/util/exec/buildpack.spec.ts b/lib/util/exec/buildpack.spec.ts index 6cc85cde5bfcb232bdcd331d6064ba68cce380d5..479b0cbed4d32bd5b5dc8e8fedfd3e2da2e81d4f 100644 --- a/lib/util/exec/buildpack.spec.ts +++ b/lib/util/exec/buildpack.spec.ts @@ -1,6 +1,11 @@ import { mocked } from '../../../test/util'; +import { GlobalConfig } from '../../config/global'; import * as _datasource from '../../datasource'; -import { generateInstallCommands, resolveConstraint } from './buildpack'; +import { + generateInstallCommands, + isDynamicInstall, + resolveConstraint, +} from './buildpack'; import type { ToolConstraint } from './types'; jest.mock('../../../lib/datasource'); @@ -8,6 +13,34 @@ jest.mock('../../../lib/datasource'); const datasource = mocked(_datasource); describe('util/exec/buildpack', () => { + describe('isDynamicInstall()', () => { + beforeEach(() => { + GlobalConfig.reset(); + delete process.env.BUILDPACK; + }); + it('returns false if binarySource is not install', () => { + expect(isDynamicInstall()).toBeFalse(); + }); + it('returns false if not buildpack', () => { + GlobalConfig.set({ binarySource: 'install' }); + expect(isDynamicInstall()).toBeFalse(); + }); + it('returns false if any unsupported tools', () => { + GlobalConfig.set({ binarySource: 'install' }); + process.env.BUILDPACK = 'true'; + const toolConstraints: ToolConstraint[] = [ + { toolName: 'node' }, + { toolName: 'npm' }, + ]; + expect(isDynamicInstall(toolConstraints)).toBeFalse(); + }); + it('returns false if supported tools', () => { + GlobalConfig.set({ binarySource: 'install' }); + process.env.BUILDPACK = 'true'; + const toolConstraints: ToolConstraint[] = [{ toolName: 'npm' }]; + expect(isDynamicInstall(toolConstraints)).toBeTrue(); + }); + }); describe('resolveConstraint()', () => { beforeEach(() => { datasource.getPkgReleases.mockResolvedValueOnce({ diff --git a/lib/util/exec/buildpack.ts b/lib/util/exec/buildpack.ts index 75bae696d82c64690b0e09004889c463ffa38705..a056cd04c12af38862b520cbea21aaa7bf6a3228 100644 --- a/lib/util/exec/buildpack.ts +++ b/lib/util/exec/buildpack.ts @@ -1,4 +1,5 @@ import { quote } from 'shlex'; +import { GlobalConfig } from '../../config/global'; import { getPkgReleases } from '../../datasource'; import { logger } from '../../logger'; import * as allVersioning from '../../versioning'; @@ -26,6 +27,30 @@ const allToolConfig: Record<string, ToolConfig> = { }, }; +export function supportsDynamicInstall(toolName: string): boolean { + return !!allToolConfig[toolName]; +} + +export function isBuildpack(): boolean { + return !!process.env.BUILDPACK; +} + +export function isDynamicInstall(toolConstraints?: ToolConstraint[]): boolean { + const { binarySource } = GlobalConfig.get(); + if (binarySource !== 'install') { + return false; + } + if (!isBuildpack()) { + logger.warn( + 'binarySource=install is only compatible with images derived from containerbase/buildpack' + ); + return false; + } + return !!toolConstraints?.every((toolConstraint) => + supportsDynamicInstall(toolConstraint.toolName) + ); +} + export async function resolveConstraint( toolConstraint: ToolConstraint ): Promise<string> { diff --git a/lib/util/exec/index.spec.ts b/lib/util/exec/index.spec.ts index 556ec654477136da61fedf48b612bfe709ed208e..6083ebac878960e7c911c0a292516f6eeb65ca57 100644 --- a/lib/util/exec/index.spec.ts +++ b/lib/util/exec/index.spec.ts @@ -13,6 +13,7 @@ import { exec } from '.'; const cpExec: jest.Mock<typeof _cpExec> = _cpExec as any; jest.mock('child_process'); +jest.mock('../../../lib/datasource'); interface TestInput { processEnv: Record<string, string>; @@ -750,6 +751,16 @@ describe('util/exec/index', () => { // FIXME: explicit assert condition expect(actualCmd).toMatchSnapshot(); }); + it('Supports binarySource=install', async () => { + process.env = processEnv; + cpExec.mockImplementation(() => { + throw new Error('some error occurred'); + }); + GlobalConfig.set({ binarySource: 'install' }); + process.env.BUILDPACK = 'true'; + const promise = exec('foobar', { toolConstraints: [{ toolName: 'npm' }] }); + await expect(promise).rejects.toThrow('No tool releases found.'); + }); it('only calls removeDockerContainer in catch block is useDocker is set', async () => { cpExec.mockImplementation(() => { diff --git a/lib/util/exec/index.ts b/lib/util/exec/index.ts index a04b9b710a0b633dfbb0cf98f343cdaa4b67df24..bc7b2078fa9f0db90619b67f5ee0167200257dda 100644 --- a/lib/util/exec/index.ts +++ b/lib/util/exec/index.ts @@ -2,7 +2,7 @@ import { dirname, join } from 'upath'; import { GlobalConfig } from '../../config/global'; import { TEMPORARY_ERROR } from '../../constants/error-messages'; import { logger } from '../../logger'; -import { generateInstallCommands } from './buildpack'; +import { generateInstallCommands, isDynamicInstall } from './buildpack'; import { rawExec } from './common'; import { generateDockerCommand, removeDockerContainer } from './docker'; import { getChildProcessEnv } from './env'; @@ -120,6 +120,12 @@ async function prepareRawExec( dockerOptions ); rawCommands = [dockerCommand]; + } else if (isDynamicInstall(opts.toolConstraints)) { + logger.debug('Using buildpack dynamic installs'); + rawCommands = [ + ...(await generateInstallCommands(opts.toolConstraints)), + ...rawCommands, + ]; } return { rawCommands, rawOptions }; @@ -141,7 +147,7 @@ export async function exec( if (useDocker) { await removeDockerContainer(docker.image, dockerChildPrefix); } - logger.debug({ command: rawCommands }, 'Executing command'); + logger.debug({ command: rawCmd }, 'Executing command'); logger.trace({ commandOptions: rawOptions }, 'Command options'); try { res = await rawExec(rawCmd, rawOptions);