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

refactor(config): Better prefix handling and enhanced coverage #12083

Merged
merged 5 commits into from
Oct 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 140 additions & 34 deletions lib/workers/global/config/parse/env.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RenovateOptions } from '../../../../config/types';
import { PlatformId } from '../../../../constants';
import { logger } from '../../../../logger';
import * as env from './env';

describe('workers/global/config/parse/env', () => {
Expand Down Expand Up @@ -32,114 +33,191 @@ describe('workers/global/config/parse/env', () => {
const envParam: NodeJS.ProcessEnv = { RENOVATE_TOKEN: 'a' };
expect(env.getConfig(envParam).token).toBe('a');
});
it('supports custom prefixes', () => {
const envParam: NodeJS.ProcessEnv = {
ENV_PREFIX: 'FOOBAR_',
FOOBAR_TOKEN: 'abc',
};
const res = env.getConfig(envParam);
expect(res).toMatchObject({ token: 'abc' });
});
it('supports json', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_LOCK_FILE_MAINTENANCE: '{}',
};
expect(env.getConfig(envParam).lockFileMaintenance).toEqual({});
});
it('supports arrays of objects', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_HOST_RULES: JSON.stringify([{ foo: 'bar' }]),
};
const res = env.getConfig(envParam);
expect(res).toMatchObject({ hostRules: [{ foo: 'bar' }] });
});
it('skips misconfigured arrays', () => {
const envName = 'RENOVATE_HOST_RULES';
const val = JSON.stringify('foobar');
const envParam: NodeJS.ProcessEnv = { [envName]: val };

const res = env.getConfig(envParam);

expect(res).toEqual({ hostRules: [] });
expect(logger.debug).toHaveBeenLastCalledWith(
{ val, envName },
'Could not parse object array'
);
});
it('skips garbage array values', () => {
const envName = 'RENOVATE_HOST_RULES';
const val = '!@#';
const envParam: NodeJS.ProcessEnv = { [envName]: val };

const res = env.getConfig(envParam);

expect(res).toEqual({ hostRules: [] });
expect(logger.debug).toHaveBeenLastCalledWith(
{ val, envName },
'Could not parse environment variable'
);
});
it('supports GitHub token', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_TOKEN: 'github.com token',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
token: 'github.com token',
});
});
it('supports GitHub custom endpoint', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_ENDPOINT: 'a ghe endpoint',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
endpoint: 'a ghe endpoint',
});
});
it('supports GitHub custom endpoint and github.com', () => {
const envParam: NodeJS.ProcessEnv = {
GITHUB_COM_TOKEN: 'a github.com token',
RENOVATE_ENDPOINT: 'a ghe endpoint',
RENOVATE_TOKEN: 'a ghe token',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
endpoint: 'a ghe endpoint',
hostRules: [
{
hostType: 'github',
matchHost: 'github.com',
token: 'a github.com token',
},
],
token: 'a ghe token',
});
});
it('supports GitHub custom endpoint and gitlab.com', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_ENDPOINT: 'a ghe endpoint',
RENOVATE_TOKEN: 'a ghe token',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
endpoint: 'a ghe endpoint',
token: 'a ghe token',
});
});
it('supports GitLab token', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_PLATFORM: PlatformId.Gitlab,
RENOVATE_TOKEN: 'a gitlab.com token',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
platform: 'gitlab',
token: 'a gitlab.com token',
});
});
it('supports GitLab custom endpoint', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_PLATFORM: PlatformId.Gitlab,
RENOVATE_TOKEN: 'a gitlab token',
RENOVATE_ENDPOINT: 'a gitlab endpoint',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
endpoint: 'a gitlab endpoint',
platform: 'gitlab',
token: 'a gitlab token',
});
});
it('supports Azure DevOps', () => {
const envParam: NodeJS.ProcessEnv = {
RENOVATE_PLATFORM: 'azure',
RENOVATE_TOKEN: 'an Azure DevOps token',
RENOVATE_ENDPOINT: 'an Azure DevOps endpoint',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
endpoint: 'an Azure DevOps endpoint',
platform: 'azure',
token: 'an Azure DevOps token',
});
});
it('supports docker username/password', () => {
const envParam: NodeJS.ProcessEnv = {
DOCKER_USERNAME: 'some-username',
DOCKER_PASSWORD: 'some-password',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
hostRules: [
{
hostType: 'docker',
password: 'some-password',
username: 'some-username',
},
],
});
});
it('supports password-only', () => {
const envParam: NodeJS.ProcessEnv = {
NPM_PASSWORD: 'some-password',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
hostRules: [{ hostType: 'npm', password: 'some-password' }],
});
});
it('supports domain and host names with case insensitivity', () => {
const envParam: NodeJS.ProcessEnv = {
GITHUB__TAGS_GITHUB_COM_TOKEN: 'some-token',
pypi_my_CUSTOM_HOST_passWORD: 'some-password',
};
const res = env.getConfig(envParam);
expect(res).toMatchSnapshot();
expect(res.hostRules).toHaveLength(2);
expect(res).toMatchSnapshot({
hostRules: [
{ matchHost: 'github.com', token: 'some-token' },
{ matchHost: 'my.custom.host', password: 'some-password' },
],
});
});
it('regression test for #10937', () => {
const envParam: NodeJS.ProcessEnv = {
GIT__TAGS_GITLAB_EXAMPLE__DOMAIN_NET_USERNAME: 'some-user',
GIT__TAGS_GITLAB_EXAMPLE__DOMAIN_NET_PASSWORD: 'some-password',
};
const res = env.getConfig(envParam);
expect(res.hostRules).toMatchObject([
{
hostType: 'git-tags',
matchHost: 'gitlab.example-domain.net',
password: 'some-password',
username: 'some-user',
},
]);
expect(res).toMatchObject({
hostRules: [
{
hostType: 'git-tags',
matchHost: 'gitlab.example-domain.net',
password: 'some-password',
username: 'some-user',
},
],
});
});
it('supports datasource env token', () => {
const envParam: NodeJS.ProcessEnv = {
PYPI_TOKEN: 'some-token',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
hostRules: [{ hostType: 'pypi', token: 'some-token' }],
});
});
it('rejects incomplete datasource env token', () => {
const envParam: NodeJS.ProcessEnv = {
Expand All @@ -160,8 +238,12 @@ describe('workers/global/config/parse/env', () => {
RENOVATE_USERNAME: 'some-username',
RENOVATE_PASSWORD: 'app-password',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
platform: 'bitbucket',
endpoint: 'a bitbucket endpoint',
username: 'some-username',
password: 'app-password',
});
});
it('supports Bitbucket username/password', () => {
const envParam: NodeJS.ProcessEnv = {
Expand All @@ -170,8 +252,13 @@ describe('workers/global/config/parse/env', () => {
RENOVATE_USERNAME: 'some-username',
RENOVATE_PASSWORD: 'app-password',
};
// FIXME: explicit assert condition
expect(env.getConfig(envParam)).toMatchSnapshot();
expect(env.getConfig(envParam)).toMatchSnapshot({
endpoint: 'a bitbucket endpoint',
hostRules: [],
password: 'app-password',
platform: 'bitbucket',
username: 'some-username',
});
});
it('merges full config from env', () => {
const envParam: NodeJS.ProcessEnv = {
Expand All @@ -182,6 +269,25 @@ describe('workers/global/config/parse/env', () => {
expect(config.enabled).toBe(false);
expect(config.token).toBe('a');
});
describe('malformed RENOVATE_CONFIG', () => {
let processExit: jest.SpyInstance<never, [code?: number]>;

beforeAll(() => {
processExit = jest
.spyOn(process, 'exit')
.mockImplementation((() => {}) as never);
viceice marked this conversation as resolved.
Show resolved Hide resolved
});

afterAll(() => {
processExit.mockRestore();
});

it('crashes', () => {
const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG: '!@#' };
env.getConfig(envParam);
expect(processExit).toHaveBeenCalledWith(1);
});
});
});
describe('.getEnvName(definition)', () => {
it('returns empty', () => {
Expand Down
30 changes: 21 additions & 9 deletions lib/workers/global/config/parse/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ import { getDatasourceList } from '../../../../datasource';
import { logger } from '../../../../logger';
import type { HostRule } from '../../../../types';

// istanbul ignore if
if (process.env.ENV_PREFIX) {
for (const [key, val] of Object.entries(process.env)) {
if (key.startsWith(process.env.ENV_PREFIX)) {
process.env[key.replace(process.env.ENV_PREFIX, 'RENOVATE_')] = val;
function normalizePrefixes(
env: NodeJS.ProcessEnv,
prefix: string | undefined
): NodeJS.ProcessEnv {
const result = { ...env };
if (prefix) {
for (const [key, val] of Object.entries(result)) {
if (key.startsWith(prefix)) {
const newKey = key.replace(prefix, 'RENOVATE_');
result[newKey] = val;
delete result[key];
}
}
}
return result;
}

export function getEnvName(option: Partial<RenovateOptions>): string {
Expand All @@ -27,7 +35,9 @@ export function getEnvName(option: Partial<RenovateOptions>): string {
return `RENOVATE_${nameWithUnderscores.toUpperCase()}`;
}

export function getConfig(env: NodeJS.ProcessEnv): AllConfig {
export function getConfig(inputEnv: NodeJS.ProcessEnv): AllConfig {
const env = normalizePrefixes(inputEnv, inputEnv.ENV_PREFIX);

const options = getOptions();

let config: AllConfig = {};
Expand All @@ -36,7 +46,7 @@ export function getConfig(env: NodeJS.ProcessEnv): AllConfig {
try {
config = JSON.parse(env.RENOVATE_CONFIG);
logger.debug({ config }, 'Detected config in env RENOVATE_CONFIG');
} catch (err) /* istanbul ignore next */ {
} catch (err) {
logger.fatal({ err }, 'Could not parse RENOVATE_CONFIG');
process.exit(1);
}
Expand All @@ -56,7 +66,6 @@ export function getConfig(env: NodeJS.ProcessEnv): AllConfig {
if (option.env !== false) {
const envName = getEnvName(option);
if (env[envName]) {
// istanbul ignore if
if (option.type === 'array' && option.subType === 'object') {
try {
const parsed = JSON.parse(env[envName]);
Expand All @@ -69,7 +78,10 @@ export function getConfig(env: NodeJS.ProcessEnv): AllConfig {
);
}
} catch (err) {
logger.debug({ val: env[envName], envName }, 'Could not parse CLI');
logger.debug(
{ val: env[envName], envName },
'Could not parse environment variable'
);
}
} else {
const coerce = coersions[option.type];
Expand Down