diff --git a/docs/usage/private-modules.md b/docs/usage/private-modules.md index e12ef14ef87e2221513f0b112efab9d54cc4d390..76611a006dc269b588b51e6143bb0c933170733a 100644 --- a/docs/usage/private-modules.md +++ b/docs/usage/private-modules.md @@ -211,3 +211,67 @@ For instructions on this, see the above section on encrypting secrets for the Wh - Replace the existing public key in the HTML with the public key you generated in the step prior - Use the resulting HTML encrypt page to encrypt secrets for your app before adding them to user/repository config - Configure the app to run with `privateKey` set to the private key you generated above + +### hostRules configuration using environment variables + +Self-hosted users can use environment variables to configure the most common types of `hostRules` for authentication. + +The format of the environment variables must be all upper-case and follow: + +- Datasource name (e.g. `NPM`, `PYPI`) +- Underscore (`_`) +- `matchHost` +- Underscore (`_`) +- Field name (`TOKEN`, `USER_NAME`, or `PASSWORD`) + +Hyphens (`-`) in datasource or host name must be replaced with double underscores (`__`). +Periods (`.`) in host names must be replaced with a single underscore (`_`). + +Examples: + +`NPM_REGISTRY_NPMJS_ORG_TOKEN=abc123`: + +```json +{ + "hostRules": [ + { + "hostType": "npm", + "matchHost": "registry.npmjs.org", + "token": "abc123" + } + ] +} +``` + +`GITLAB__TAGS_CODE__HOST_COMPANY_COM_USERNAME=bot GITLAB__TAGS_CODE__HOST_COMPANY_COM_PASSWORD=botpass123`: + +```json +{ + "hostRules": [ + { + "hostType": "gitlab-tags", + "matchHost": "code-host.company.com", + "username": "bot", + "password": "botpass123" + } + ] +} +``` + +It's also possible to skip the host part, and just have datasource + credentials. + +Example: + +`DOCKER_USERNAME=bot DOCKER_PASSWORD=botpass123`: + +```json +{ + "hostRules": [ + { + "hostType": "docker", + "username": "bot", + "password": "botpass123" + } + ] +} +``` diff --git a/lib/config/__snapshots__/env.spec.ts.snap b/lib/config/__snapshots__/env.spec.ts.snap index c33d6580cc22dda0ab1dc0e69ad02f18bf839dd4..98f9c387eb70ce054d522114a8dadf1c10052b6f 100644 --- a/lib/config/__snapshots__/env.spec.ts.snap +++ b/lib/config/__snapshots__/env.spec.ts.snap @@ -82,6 +82,17 @@ Object { } `; +exports[`config/env .getConfig(env) supports datasource env token 1`] = ` +Object { + "hostRules": Array [ + Object { + "hostType": "pypi", + "token": "some-token", + }, + ], +} +`; + exports[`config/env .getConfig(env) supports docker username/password 1`] = ` Object { "hostRules": Array [ @@ -93,3 +104,31 @@ Object { ], } `; + +exports[`config/env .getConfig(env) supports domain and host names with case insensitivity 1`] = ` +Object { + "hostRules": Array [ + Object { + "hostType": "github-tags", + "matchHost": "github.com", + "token": "some-token", + }, + Object { + "hostType": "pypi", + "matchHost": "my.custom.host", + "password": "some-password", + }, + ], +} +`; + +exports[`config/env .getConfig(env) supports password-only 1`] = ` +Object { + "hostRules": Array [ + Object { + "hostType": "npm", + "password": "some-password", + }, + ], +} +`; diff --git a/lib/config/env.spec.ts b/lib/config/env.spec.ts index 0f395003cad4bc8c7a5a18788264f7a92dacca73..acb608f5ab7ef4878b2f03c35bca60c975865402 100644 --- a/lib/config/env.spec.ts +++ b/lib/config/env.spec.ts @@ -99,6 +99,33 @@ describe(getName(), () => { }; expect(env.getConfig(envParam)).toMatchSnapshot(); }); + it('supports password-only', () => { + const envParam: NodeJS.ProcessEnv = { + NPM_PASSWORD: 'some-password', + }; + expect(env.getConfig(envParam)).toMatchSnapshot(); + }); + 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); + }); + it('supports datasource env token', () => { + const envParam: NodeJS.ProcessEnv = { + PYPI_TOKEN: 'some-token', + }; + expect(env.getConfig(envParam)).toMatchSnapshot(); + }); + it('rejects incomplete datasource env token', () => { + const envParam: NodeJS.ProcessEnv = { + PYPI_FOO_TOKEN: 'some-token', + }; + expect(env.getConfig(envParam).hostRules).toHaveLength(0); + }); it('supports Bitbucket token', () => { const envParam: NodeJS.ProcessEnv = { RENOVATE_PLATFORM: PLATFORM_TYPE_BITBUCKET, diff --git a/lib/config/env.ts b/lib/config/env.ts index fd960cbb75e446458838118614c44b48002a2704..9858f3ff8ff359fbdb2967553355ea22b5a9dfda 100644 --- a/lib/config/env.ts +++ b/lib/config/env.ts @@ -1,8 +1,9 @@ import is from '@sindresorhus/is'; import { PLATFORM_TYPE_GITHUB } from '../constants/platforms'; -import * as datasourceDocker from '../datasource/docker'; +import { getDatasourceList } from '../datasource'; import { logger } from '../logger'; +import type { HostRule } from '../types'; import { getOptions } from './definitions'; import type { GlobalConfig, RenovateOptions } from './types'; @@ -86,14 +87,52 @@ export function getConfig(env: NodeJS.ProcessEnv): GlobalConfig { }); } - if (env.DOCKER_USERNAME && env.DOCKER_PASSWORD) { - config.hostRules.push({ - hostType: datasourceDocker.id, - username: env.DOCKER_USERNAME, - password: env.DOCKER_PASSWORD, - }); + const datasources = new Set(getDatasourceList()); + const fields = ['token', 'username', 'password']; + + const hostRules: HostRule[] = []; + + for (const envName of Object.keys(env).sort()) { + // Double underscore __ is used in place of hyphen - + const splitEnv = envName.toLowerCase().replace('__', '-').split('_'); + const hostType = splitEnv.shift(); + if (datasources.has(hostType)) { + const suffix = splitEnv.pop(); + if (fields.includes(suffix)) { + let matchHost: string; + const rule: HostRule = {}; + rule[suffix] = env[envName]; + if (splitEnv.length === 0) { + // host-less rule + } else if (splitEnv.length === 1) { + logger.warn(`Cannot parse ${envName} env`); + continue; // eslint-disable-line no-continue + } else { + matchHost = splitEnv.join('.'); + } + const existingRule = hostRules.find( + (hr) => hr.hostType === hostType && hr.matchHost === matchHost + ); + if (existingRule) { + // Add current field to existing rule + existingRule[suffix] = env[envName]; + } else { + // Create a new rule + const newRule: HostRule = { + hostType, + }; + if (matchHost) { + newRule.matchHost = matchHost; + } + newRule[suffix] = env[envName]; + hostRules.push(newRule); + } + } + } } + config.hostRules = [...config.hostRules, ...hostRules]; + // These env vars are deprecated and deleted to make sure they're not used const unsupportedEnv = [ 'BITBUCKET_TOKEN',