Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): optionally remove self-hosted config file once read #22857

Merged
merged 19 commits into from Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/usage/self-hosted-experimental.md
Expand Up @@ -71,6 +71,13 @@ Source: [AWS S3 documentation - Interface BucketEndpointInputConfig](https://doc

If set, Renovate will terminate the whole process group of a terminated child process spawned by Renovate.

## `RENOVATE_X_DELETE_CONFIG_FILE`

If `true` Renovate tries to delete the self-hosted config file after reading it.
rarkins marked this conversation as resolved.
Show resolved Hide resolved
You can set the config file Renovate should read with the `RENOVATE_CONFIG_FILE` environment variable.

The process that runs Renovate must have the correct permissions to delete the config file.

## `RENOVATE_X_MATCH_PACKAGE_NAMES_MORE`

If set, you'll get the following behavior.
Expand Down
124 changes: 107 additions & 17 deletions lib/workers/global/config/parse/file.spec.ts
Expand Up @@ -7,6 +7,10 @@ import customConfig from './__fixtures__/config';
import * as file from './file';

describe('workers/global/config/parse/file', () => {
const processExitSpy = jest.spyOn(process, 'exit');
const fsPathExistsSpy = jest.spyOn(fsExtra, 'pathExists');
const fsRemoveSpy = jest.spyOn(fsExtra, 'remove');

let tmp: DirectoryResult;

beforeAll(async () => {
Expand Down Expand Up @@ -71,32 +75,28 @@ describe('workers/global/config/parse/file', () => {
])(
'fatal error and exit if error in parsing %s',
async (fileName, fileContent) => {
const mockProcessExit = jest
.spyOn(process, 'exit')
.mockImplementationOnce(() => undefined as never);
processExitSpy.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(tmp.path, fileName);
fs.writeFileSync(configFile, fileContent, { encoding: 'utf8' });
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(1);
fs.unlinkSync(configFile);
}
);

it('fatal error and exit if custom config file does not exist', async () => {
const mockProcessExit = jest
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);

processExitSpy
.mockImplementationOnce(() => undefined as never)
.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(tmp.path, './file4.js');

await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });

expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('fatal error and exit if config.js contains unresolved env var', async () => {
const mockProcessExit = jest
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
processExitSpy.mockImplementationOnce(() => undefined as never);

const configFile = upath.resolve(
__dirname,
Expand All @@ -113,22 +113,112 @@ describe('workers/global/config/parse/file', () => {
expect(logger.fatal).toHaveBeenCalledWith(
`Error parsing config file due to unresolved variable(s): CI_API_V4_URL is not defined`
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it.each([
['invalid config file type', './file.txt'],
['missing config file type', './file'],
])('fatal error and exit if %s', async (fileType, filePath) => {
const mockProcessExit = jest
.spyOn(process, 'exit')
.mockImplementationOnce(() => undefined as never);
processExitSpy.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(tmp.path, filePath);
fs.writeFileSync(configFile, `{"token": "abc"}`, { encoding: 'utf8' });
await file.getConfig({ RENOVATE_CONFIG_FILE: configFile });
expect(mockProcessExit).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(logger.fatal).toHaveBeenCalledWith('Unsupported file type');
fs.unlinkSync(configFile);
});

it('removes the config file if RENOVATE_CONFIG_FILE & RENOVATE_X_DELETE_CONFIG_FILE are set', async () => {
fsRemoveSpy.mockImplementationOnce(() => {
// no-op
});
processExitSpy.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(tmp.path, './config.json');
fs.writeFileSync(configFile, `{"token": "abc"}`, { encoding: 'utf8' });

await file.getConfig({
RENOVATE_CONFIG_FILE: configFile,
RENOVATE_X_DELETE_CONFIG_FILE: 'true',
});

expect(processExitSpy).not.toHaveBeenCalled();
expect(fsRemoveSpy).toHaveBeenCalledTimes(1);
expect(fsRemoveSpy).toHaveBeenCalledWith(configFile);
fs.unlinkSync(configFile);
});
});

describe('deleteConfigFile()', () => {
it.each([[undefined], [' ']])(
'skip when RENOVATE_CONFIG_FILE is not set ("%s")',
async (configFile) => {
await file.deleteConfigFile({ RENOVATE_CONFIG_FILE: configFile });

expect(fsRemoveSpy).toHaveBeenCalledTimes(0);
}
);

it('skip when config file does not exist', async () => {
fsPathExistsSpy.mockResolvedValueOnce(false as never);
rarkins marked this conversation as resolved.
Show resolved Hide resolved

await file.deleteConfigFile({ RENOVATE_CONFIG_FILE: 'path' });

expect(fsRemoveSpy).toHaveBeenCalledTimes(0);
});

it.each([['false'], [' ']])(
'skip if RENOVATE_X_DELETE_CONFIG_FILE is not set ("%s")',
async (deleteConfig) => {
fsPathExistsSpy.mockResolvedValueOnce(true as never);

await file.deleteConfigFile({
RENOVATE_X_DELETE_CONFIG_FILE: deleteConfig,
RENOVATE_CONFIG_FILE: '/path/to/config.js',
});

expect(fsRemoveSpy).toHaveBeenCalledTimes(0);
}
);

it('removes the specified config file', async () => {
fsRemoveSpy.mockImplementationOnce(() => {
// no-op
});
fsPathExistsSpy.mockResolvedValueOnce(true as never);
const configFile = '/path/to/config.js';

await file.deleteConfigFile({
RENOVATE_CONFIG_FILE: configFile,
RENOVATE_X_DELETE_CONFIG_FILE: 'true',
});

expect(fsRemoveSpy).toHaveBeenCalledTimes(1);
expect(fsRemoveSpy).toHaveBeenCalledWith(configFile);
expect(logger.trace).toHaveBeenCalledWith(
expect.anything(),
'config file successfully deleted'
);
});

it('fails silently when attempting to delete the config file', async () => {
fsRemoveSpy.mockImplementationOnce(() => {
throw new Error();
});
fsPathExistsSpy.mockResolvedValueOnce(true as never);
const configFile = '/path/to/config.js';

await file.deleteConfigFile({
RENOVATE_CONFIG_FILE: configFile,
RENOVATE_X_DELETE_CONFIG_FILE: 'true',
});

expect(fsRemoveSpy).toHaveBeenCalledTimes(1);
expect(fsRemoveSpy).toHaveBeenCalledWith(configFile);
expect(logger.warn).toHaveBeenCalledWith(
expect.anything(),
'error deleting config file'
);
});
});
});
31 changes: 31 additions & 0 deletions lib/workers/global/config/parse/file.ts
Expand Up @@ -71,6 +71,14 @@ export async function getConfig(env: NodeJS.ProcessEnv): Promise<AllConfig> {
logger.debug('No config file found on disk - skipping');
}
}

if (
env.RENOVATE_CONFIG_FILE &&
Gabriel-Ladzaretti marked this conversation as resolved.
Show resolved Hide resolved
env.RENOVATE_X_DELETE_CONFIG_FILE === 'true'
) {
await deleteConfigFile(env);
}

const { isMigrated, migratedConfig } = migrateConfig(config);
if (isMigrated) {
logger.warn(
Expand All @@ -81,3 +89,26 @@ export async function getConfig(env: NodeJS.ProcessEnv): Promise<AllConfig> {
}
return config;
}

export async function deleteConfigFile(env: NodeJS.ProcessEnv): Promise<void> {
const configFile = env.RENOVATE_CONFIG_FILE;

if (is.undefined(configFile) || is.emptyStringOrWhitespace(configFile)) {
return;
}

if (!(await fs.pathExists(configFile))) {
Gabriel-Ladzaretti marked this conversation as resolved.
Show resolved Hide resolved
return;
}

if (env.RENOVATE_X_DELETE_CONFIG_FILE !== 'true') {
return;
}
rarkins marked this conversation as resolved.
Show resolved Hide resolved

try {
await fs.remove(configFile);
logger.trace({ path: configFile }, 'config file successfully deleted');
} catch (err) {
logger.warn({ err }, 'error deleting config file');
}
}