Skip to content

Commit

Permalink
feat: binarySource=install (#12961)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins committed Dec 10, 2021
1 parent e8645bf commit a9d3348
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 7 deletions.
13 changes: 12 additions & 1 deletion docs/usage/self-hosted-configuration.md
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions lib/config/options/index.ts
Expand Up @@ -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',
},
{
Expand Down
2 changes: 1 addition & 1 deletion lib/config/types.ts
Expand Up @@ -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;
Expand Down
35 changes: 34 additions & 1 deletion lib/util/exec/buildpack.spec.ts
@@ -1,13 +1,46 @@
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');

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({
Expand Down
25 changes: 25 additions & 0 deletions 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';
Expand Down Expand Up @@ -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> {
Expand Down
11 changes: 11 additions & 0 deletions lib/util/exec/index.spec.ts
Expand Up @@ -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>;
Expand Down Expand Up @@ -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(() => {
Expand Down
10 changes: 8 additions & 2 deletions lib/util/exec/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
Expand All @@ -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);
Expand Down

0 comments on commit a9d3348

Please sign in to comment.