diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 84564e3040ac23..5113beeb7eabb3 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -429,7 +429,7 @@ Currently the purpose of `hostRules` is to configure credentials for host authen The lookup keys for a hostRule are: `hostType`, `domainName`, `hostName`, and `baseUrl`. All are optional, but you can only have one of the last three per rule. -Supported credential fields are `token`, `username`, `password`, `timeout` and `insecureRegistry`. +Supported credential fields are `token`, `username`, `password`, `timeout`, `enabled` and `insecureRegistry`. Example for configuring `docker` auth: @@ -445,6 +445,29 @@ Example for configuring `docker` auth: } ``` +To disable requests to a particular host, you can configure a rule like: + +```json +{ + "hostRules": [ + { + "hostName": "registry.npmjs.org", + "enabled": false + } + ] +} +``` + +A preset alternative to the above is: + +```json +{ + "extends": [":disableHost(registry.npmjs.org)"] +} +``` + +Note: Disabling a host is only 100% effective if added to self-hosted config. Renovate currently still checks its _cache_ for results first before making connection attempts, so if a public host is blocked in your repository config (e.g. `renovate.json`) then it's possible you may get cached _results_ from that host if another repository using the same bot has successfully queried for the same dependency recently. + ### abortIgnoreStatusCodes This field can be used to configure status codes that Renovate ignores and passes through when `abortOnError` is set to `true`. For example to also skip 404 responses then configure the following: diff --git a/lib/config/presets/internal/default.ts b/lib/config/presets/internal/default.ts index 3c9aacb79bea1c..44594e625a74a5 100644 --- a/lib/config/presets/internal/default.ts +++ b/lib/config/presets/internal/default.ts @@ -15,6 +15,24 @@ export const presets: Record = { enabled: false, }, }, + disableDomain: { + description: 'Disable requests to a particular domain', + hostRules: [ + { + domainName: '{{arg0}}', + enabled: false, + }, + ], + }, + disableHost: { + description: 'Disable requests to a particular hostName', + hostRules: [ + { + hostName: '{{arg0}}', + enabled: false, + }, + ], + }, ignoreModulesAndTests: { description: 'Ignore `node_modules`, `bower_components`, `vendor` and various test/tests directories', diff --git a/lib/constants/error-messages.ts b/lib/constants/error-messages.ts index 3064bab6d2204a..929d296c161eca 100644 --- a/lib/constants/error-messages.ts +++ b/lib/constants/error-messages.ts @@ -37,6 +37,7 @@ export const MANAGER_NO_PACKAGE_FILES = 'no-package-files'; // Host error export const EXTERNAL_HOST_ERROR = 'external-host-error'; export const IGNORABLE_HOST_ERROR = 'ignorable-host-error'; +export const HOST_DISABLED = 'host-disabled'; // Worker Error export const WORKER_FILE_UPDATE_FAILED = 'update-failure'; diff --git a/lib/datasource/index.spec.ts b/lib/datasource/index.spec.ts index cce29d3bfb255b..b477d9f4d8d346 100644 --- a/lib/datasource/index.spec.ts +++ b/lib/datasource/index.spec.ts @@ -1,5 +1,8 @@ import { mocked } from '../../test/util'; -import { EXTERNAL_HOST_ERROR } from '../constants/error-messages'; +import { + EXTERNAL_HOST_ERROR, + HOST_DISABLED, +} from '../constants/error-messages'; import { ExternalHostError } from '../types/errors/external-host-error'; import { loadModules } from '../util/modules'; import * as datasourceDocker from './docker'; @@ -131,6 +134,18 @@ describe('datasource/index', () => { }); expect(res).not.toBeNull(); }); + it('returns null for HOST_DISABLED', async () => { + packagistDatasource.getReleases.mockImplementationOnce(() => { + throw new ExternalHostError(new Error(HOST_DISABLED)); + }); + expect( + await datasource.getPkgReleases({ + datasource: datasourcePackagist.id, + depName: 'something', + registryUrls: ['https://reg1.com'], + }) + ).toBeNull(); + }); it('hunts registries and aborts on ExternalHostError', async () => { packagistDatasource.getReleases.mockImplementationOnce(() => { throw new ExternalHostError(new Error()); diff --git a/lib/datasource/index.ts b/lib/datasource/index.ts index 243a083aaa0d35..415557fc1e600a 100644 --- a/lib/datasource/index.ts +++ b/lib/datasource/index.ts @@ -1,5 +1,6 @@ import is from '@sindresorhus/is'; import equal from 'fast-deep-equal'; +import { HOST_DISABLED } from '../constants/error-messages'; import { logger } from '../logger'; import { ExternalHostError } from '../types/errors/external-host-error'; import * as memCache from '../util/cache/memory'; @@ -196,6 +197,9 @@ async function fetchReleases( }); } } catch (err) { + if (err.message === HOST_DISABLED || err.err?.message === HOST_DISABLED) { + return null; + } if (err instanceof ExternalHostError) { throw err; } diff --git a/lib/types/host-rules.ts b/lib/types/host-rules.ts index 403b8dad9730c1..58ed1867d8ac3c 100644 --- a/lib/types/host-rules.ts +++ b/lib/types/host-rules.ts @@ -15,4 +15,5 @@ export interface HostRule { encrypted?: HostRule; abortOnError?: boolean; abortIgnoreStatusCodes?: number[]; + enabled?: boolean; } diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts index 6d1f7229893a1d..81cce5994f0453 100644 --- a/lib/util/http/host-rules.ts +++ b/lib/util/http/host-rules.ts @@ -10,7 +10,7 @@ export function applyHostRules(url: string, inOptions: any): any { hostType: options.hostType, url, }) || {}; - const { username, password, token } = foundRules; + const { username, password, token, enabled } = foundRules; if (options.headers?.authorization || options.auth || options.token) { logger.trace('Authorization already set for host: ' + options.hostname); } else if (password) { @@ -19,6 +19,8 @@ export function applyHostRules(url: string, inOptions: any): any { } else if (token) { logger.trace('Applying Bearer authentication for host ' + options.hostname); options.token = token; + } else if (enabled === false) { + options.enabled = false; } // Apply optional params ['abortOnError', 'abortIgnoreStatusCodes', 'timeout'].forEach((param) => { diff --git a/lib/util/http/index.spec.ts b/lib/util/http/index.spec.ts index f97e240dc77c21..93cd5cde991ba2 100644 --- a/lib/util/http/index.spec.ts +++ b/lib/util/http/index.spec.ts @@ -1,6 +1,9 @@ import nock from 'nock'; import { getName } from '../../../test/util'; -import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages'; +import { + EXTERNAL_HOST_ERROR, + HOST_DISABLED, +} from '../../constants/error-messages'; import * as hostRules from '../host-rules'; import { Http } from '.'; @@ -34,6 +37,12 @@ describe(getName(__filename), () => { ); expect(nock.isDone()).toBe(true); }); + it('disables hosts', async () => { + hostRules.add({ hostName: 'renovate.com', enabled: false }); + await expect(http.get('http://renovate.com/test')).rejects.toThrow( + HOST_DISABLED + ); + }); it('ignores 404 error and does not throw ExternalHostError', async () => { nock(baseUrl).get('/test').reply(404); hostRules.add({ abortOnError: true, abortIgnoreStatusCodes: [404] }); diff --git a/lib/util/http/index.ts b/lib/util/http/index.ts index 78a57d43e088e3..7977f71fef1da1 100644 --- a/lib/util/http/index.ts +++ b/lib/util/http/index.ts @@ -1,6 +1,7 @@ import crypto from 'crypto'; import URL from 'url'; import got from 'got'; +import { HOST_DISABLED } from '../../constants/error-messages'; import { ExternalHostError } from '../../types/errors/external-host-error'; import * as memCache from '../cache/memory'; import { clone } from '../clone'; @@ -89,6 +90,9 @@ export class Http { }; options = applyHostRules(url, options); + if (options.enabled === false) { + throw new Error(HOST_DISABLED); + } options = applyAuthorization(options); // Cache GET requests unless useCache=false