From ee9c45aa5147ed4ea3480708404cc83c8c745989 Mon Sep 17 00:00:00 2001 From: Gabriel-Ladzaretti <97394622+Gabriel-Ladzaretti@users.noreply.github.com> Date: Wed, 21 Jun 2023 00:01:25 +0300 Subject: [PATCH] feat(config): optionally remove self-hosted config file once read (#22857) Co-authored-by: Rhys Arkins <rhys@arkins.net> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- docs/usage/self-hosted-experimental.md | 7 + lib/workers/global/config/parse/file.spec.ts | 127 ++++++++++++++++--- lib/workers/global/config/parse/file.ts | 28 ++++ 3 files changed, 145 insertions(+), 17 deletions(-) diff --git a/docs/usage/self-hosted-experimental.md b/docs/usage/self-hosted-experimental.md index 5945938bef..47fd66b1cf 100644 --- a/docs/usage/self-hosted-experimental.md +++ b/docs/usage/self-hosted-experimental.md @@ -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. +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. diff --git a/lib/workers/global/config/parse/file.spec.ts b/lib/workers/global/config/parse/file.spec.ts index 6dd841bd6e..e0aaf92406 100644 --- a/lib/workers/global/config/parse/file.spec.ts +++ b/lib/workers/global/config/parse/file.spec.ts @@ -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 () => { @@ -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, @@ -113,22 +113,115 @@ 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 + }); + fsPathExistsSpy.mockResolvedValueOnce(true 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.deleteNonDefaultConfig({ RENOVATE_CONFIG_FILE: configFile }); + + expect(fsRemoveSpy).toHaveBeenCalledTimes(0); + } + ); + + it('skip when config file does not exist', async () => { + fsPathExistsSpy.mockResolvedValueOnce(false as never); + + await file.deleteNonDefaultConfig({ + RENOVATE_CONFIG_FILE: 'path', + RENOVATE_X_DELETE_CONFIG_FILE: 'true', + }); + + 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.deleteNonDefaultConfig({ + 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.deleteNonDefaultConfig({ + 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.deleteNonDefaultConfig({ + 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' + ); + }); }); }); diff --git a/lib/workers/global/config/parse/file.ts b/lib/workers/global/config/parse/file.ts index 871fe10f45..ba255ecd10 100644 --- a/lib/workers/global/config/parse/file.ts +++ b/lib/workers/global/config/parse/file.ts @@ -71,6 +71,9 @@ export async function getConfig(env: NodeJS.ProcessEnv): Promise<AllConfig> { logger.debug('No config file found on disk - skipping'); } } + + await deleteNonDefaultConfig(env); // Attempt deletion only if RENOVATE_CONFIG_FILE is specified + const { isMigrated, migratedConfig } = migrateConfig(config); if (isMigrated) { logger.warn( @@ -81,3 +84,28 @@ export async function getConfig(env: NodeJS.ProcessEnv): Promise<AllConfig> { } return config; } + +export async function deleteNonDefaultConfig( + env: NodeJS.ProcessEnv +): Promise<void> { + const configFile = env.RENOVATE_CONFIG_FILE; + + if (is.undefined(configFile) || is.emptyStringOrWhitespace(configFile)) { + return; + } + + if (env.RENOVATE_X_DELETE_CONFIG_FILE !== 'true') { + return; + } + + if (!(await fs.pathExists(configFile))) { + return; + } + + try { + await fs.remove(configFile); + logger.trace({ path: configFile }, 'config file successfully deleted'); + } catch (err) { + logger.warn({ err }, 'error deleting config file'); + } +} -- GitLab