mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 06:56:24 +00:00
feat(host-rules): Support readOnly
request matching (#28562)
Co-authored-by: Rhys Arkins <rhys@arkins.net>
This commit is contained in:
parent
e82e747f29
commit
5c0628bf3b
12 changed files with 106 additions and 9 deletions
|
@ -898,6 +898,27 @@ It will be compiled using Handlebars and the regex `groups` result.
|
||||||
It will be compiled using Handlebars and the regex `groups` result.
|
It will be compiled using Handlebars and the regex `groups` result.
|
||||||
It will default to the value of `depName` if left unconfigured/undefined.
|
It will default to the value of `depName` if left unconfigured/undefined.
|
||||||
|
|
||||||
|
### readOnly
|
||||||
|
|
||||||
|
If the `readOnly` field is being set to `true` inside the host rule, it will match only against the requests that are known to be read operations.
|
||||||
|
Examples are `GET` requests or `HEAD` requests, but also it could be certain types of GraphQL queries.
|
||||||
|
|
||||||
|
This option could be used to avoid rate limits for certain platforms like GitHub or Bitbucket, by offloading the read operations to a different user.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hostRules": [
|
||||||
|
{
|
||||||
|
"matchHost": "api.github.com",
|
||||||
|
"readOnly": true,
|
||||||
|
"token": "********"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If more than one token matches for a read-only request then the `readOnly` token will be given preference.
|
||||||
|
|
||||||
### currentValueTemplate
|
### currentValueTemplate
|
||||||
|
|
||||||
If the `currentValue` for a dependency is not captured with a named group then it can be defined in config using this field.
|
If the `currentValue` for a dependency is not captured with a named group then it can be defined in config using this field.
|
||||||
|
|
|
@ -2476,6 +2476,16 @@ const options: RenovateOptions[] = [
|
||||||
cli: false,
|
cli: false,
|
||||||
env: false,
|
env: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'readOnly',
|
||||||
|
description:
|
||||||
|
'Match against requests that only read data and do not mutate anything.',
|
||||||
|
type: 'boolean',
|
||||||
|
stage: 'repository',
|
||||||
|
parents: ['hostRules'],
|
||||||
|
cli: false,
|
||||||
|
env: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'timeout',
|
name: 'timeout',
|
||||||
description: 'Timeout (in milliseconds) for queries to external endpoints.',
|
description: 'Timeout (in milliseconds) for queries to external endpoints.',
|
||||||
|
|
|
@ -461,6 +461,7 @@ export async function initRepo({
|
||||||
const opts = hostRules.find({
|
const opts = hostRules.find({
|
||||||
hostType: 'github',
|
hostType: 'github',
|
||||||
url: platformConfig.endpoint,
|
url: platformConfig.endpoint,
|
||||||
|
readOnly: true,
|
||||||
});
|
});
|
||||||
config.renovateUsername = renovateUsername;
|
config.renovateUsername = renovateUsername;
|
||||||
[config.repositoryOwner, config.repositoryName] = repository.split('/');
|
[config.repositoryOwner, config.repositoryName] = repository.split('/');
|
||||||
|
@ -499,6 +500,7 @@ export async function initRepo({
|
||||||
name: config.repositoryName,
|
name: config.repositoryName,
|
||||||
user: renovateUsername,
|
user: renovateUsername,
|
||||||
},
|
},
|
||||||
|
readOnly: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res?.errors) {
|
if (res?.errors) {
|
||||||
|
@ -1214,6 +1216,7 @@ async function getIssues(): Promise<Issue[]> {
|
||||||
name: config.repositoryName,
|
name: config.repositoryName,
|
||||||
user: config.renovateUsername,
|
user: config.renovateUsername,
|
||||||
},
|
},
|
||||||
|
readOnly: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1975,6 +1978,7 @@ export async function getVulnerabilityAlerts(): Promise<VulnerabilityAlert[]> {
|
||||||
variables: { owner: config.repositoryOwner, name: config.repositoryName },
|
variables: { owner: config.repositoryOwner, name: config.repositoryName },
|
||||||
paginate: false,
|
paginate: false,
|
||||||
acceptHeader: 'application/vnd.github.vixen-preview+json',
|
acceptHeader: 'application/vnd.github.vixen-preview+json',
|
||||||
|
readOnly: true,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug({ err }, 'Error retrieving vulnerability alerts');
|
logger.debug({ err }, 'Error retrieving vulnerability alerts');
|
||||||
|
|
|
@ -25,9 +25,10 @@ export interface HostRule {
|
||||||
hostType?: string;
|
hostType?: string;
|
||||||
matchHost?: string;
|
matchHost?: string;
|
||||||
resolvedHost?: string;
|
resolvedHost?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CombinedHostRule = Omit<
|
export type CombinedHostRule = Omit<
|
||||||
HostRule,
|
HostRule,
|
||||||
'encrypted' | 'hostType' | 'matchHost' | 'resolvedHost'
|
'encrypted' | 'hostType' | 'matchHost' | 'resolvedHost' | 'readOnly'
|
||||||
>;
|
>;
|
||||||
|
|
|
@ -107,6 +107,7 @@ export class GithubGraphqlDatasourceFetcher<
|
||||||
return {
|
return {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
repository,
|
repository,
|
||||||
|
readOnly: true,
|
||||||
body: { query, variables },
|
body: { query, variables },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,6 +295,25 @@ describe('util/host-rules', () => {
|
||||||
}),
|
}),
|
||||||
).toEqual({ token: 'longest' });
|
).toEqual({ token: 'longest' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches readOnly requests', () => {
|
||||||
|
add({
|
||||||
|
matchHost: 'https://api.github.com/repos/',
|
||||||
|
token: 'aaa',
|
||||||
|
hostType: 'github',
|
||||||
|
});
|
||||||
|
add({
|
||||||
|
matchHost: 'https://api.github.com',
|
||||||
|
token: 'bbb',
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
find({
|
||||||
|
url: 'https://api.github.com/repos/foo/bar/tags',
|
||||||
|
readOnly: true,
|
||||||
|
}),
|
||||||
|
).toEqual({ token: 'bbb' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hosts()', () => {
|
describe('hosts()', () => {
|
||||||
|
|
|
@ -73,6 +73,7 @@ export function add(params: HostRule): void {
|
||||||
export interface HostRuleSearch {
|
export interface HostRuleSearch {
|
||||||
hostType?: string;
|
hostType?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesHost(url: string, matchHost: string): boolean {
|
function matchesHost(url: string, matchHost: string): boolean {
|
||||||
|
@ -107,8 +108,9 @@ function fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number {
|
||||||
return a.matchHost.length - b.matchHost.length;
|
return a.matchHost.length - b.matchHost.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hostRuleRank({ hostType, matchHost }: HostRule): number {
|
function hostRuleRank({ hostType, matchHost, readOnly }: HostRule): number {
|
||||||
if (hostType && matchHost) {
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
if ((hostType || readOnly) && matchHost) {
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +144,7 @@ export function find(search: HostRuleSearch): CombinedHostRule {
|
||||||
for (const rule of sortedRules) {
|
for (const rule of sortedRules) {
|
||||||
let hostTypeMatch = true;
|
let hostTypeMatch = true;
|
||||||
let hostMatch = true;
|
let hostMatch = true;
|
||||||
|
let readOnlyMatch = true;
|
||||||
|
|
||||||
if (rule.hostType) {
|
if (rule.hostType) {
|
||||||
hostTypeMatch = false;
|
hostTypeMatch = false;
|
||||||
|
@ -157,7 +160,15 @@ export function find(search: HostRuleSearch): CombinedHostRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostTypeMatch && hostMatch) {
|
if (!is.undefined(rule.readOnly)) {
|
||||||
|
readOnlyMatch = false;
|
||||||
|
if (search.readOnly === rule.readOnly) {
|
||||||
|
readOnlyMatch = true;
|
||||||
|
hostTypeMatch = true; // When we match `readOnly`, we don't care about `hostType`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostTypeMatch && readOnlyMatch && hostMatch) {
|
||||||
matchedRules.push(clone(rule));
|
matchedRules.push(clone(rule));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,6 +177,7 @@ export function find(search: HostRuleSearch): CombinedHostRule {
|
||||||
delete res.hostType;
|
delete res.hostType;
|
||||||
delete res.resolvedHost;
|
delete res.resolvedHost;
|
||||||
delete res.matchHost;
|
delete res.matchHost;
|
||||||
|
delete res.readOnly;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -276,7 +276,7 @@ export class GithubHttp extends Http<GithubHttpOptions> {
|
||||||
options?: InternalHttpOptions & GithubHttpOptions,
|
options?: InternalHttpOptions & GithubHttpOptions,
|
||||||
okToRetry = true,
|
okToRetry = true,
|
||||||
): Promise<HttpResponse<T>> {
|
): Promise<HttpResponse<T>> {
|
||||||
const opts: GithubHttpOptions = {
|
const opts: InternalHttpOptions & GithubHttpOptions = {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
...options,
|
...options,
|
||||||
throwHttpErrors: true,
|
throwHttpErrors: true,
|
||||||
|
@ -296,8 +296,17 @@ export class GithubHttp extends Http<GithubHttpOptions> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let readOnly = opts.readOnly;
|
||||||
|
const { method = 'get' } = opts;
|
||||||
|
if (
|
||||||
|
readOnly === undefined &&
|
||||||
|
['get', 'head'].includes(method.toLowerCase())
|
||||||
|
) {
|
||||||
|
readOnly = true;
|
||||||
|
}
|
||||||
const { token } = findMatchingRule(authUrl.toString(), {
|
const { token } = findMatchingRule(authUrl.toString(), {
|
||||||
hostType: this.hostType,
|
hostType: this.hostType,
|
||||||
|
readOnly,
|
||||||
});
|
});
|
||||||
opts.token = token;
|
opts.token = token;
|
||||||
}
|
}
|
||||||
|
@ -393,6 +402,7 @@ export class GithubHttp extends Http<GithubHttpOptions> {
|
||||||
baseUrl: baseUrl.replace('/v3/', '/'), // GHE uses unversioned graphql path
|
baseUrl: baseUrl.replace('/v3/', '/'), // GHE uses unversioned graphql path
|
||||||
body,
|
body,
|
||||||
headers: { accept: options?.acceptHeader },
|
headers: { accept: options?.acceptHeader },
|
||||||
|
readOnly: options.readOnly,
|
||||||
};
|
};
|
||||||
if (options.token) {
|
if (options.token) {
|
||||||
opts.token = options.token;
|
opts.token = options.token;
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { matchRegexOrGlobList } from '../string-match';
|
||||||
import { parseUrl } from '../url';
|
import { parseUrl } from '../url';
|
||||||
import { dnsLookup } from './dns';
|
import { dnsLookup } from './dns';
|
||||||
import { keepAliveAgents } from './keep-alive';
|
import { keepAliveAgents } from './keep-alive';
|
||||||
import type { GotOptions } from './types';
|
import type { GotOptions, InternalHttpOptions } from './types';
|
||||||
|
|
||||||
export type HostRulesGotOptions = Pick<
|
export type HostRulesGotOptions = Pick<
|
||||||
GotOptions,
|
GotOptions & InternalHttpOptions,
|
||||||
| 'hostType'
|
| 'hostType'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'noAuth'
|
| 'noAuth'
|
||||||
|
@ -34,14 +34,15 @@ export type HostRulesGotOptions = Pick<
|
||||||
| 'agent'
|
| 'agent'
|
||||||
| 'http2'
|
| 'http2'
|
||||||
| 'https'
|
| 'https'
|
||||||
|
| 'readOnly'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function findMatchingRule<GotOptions extends HostRulesGotOptions>(
|
export function findMatchingRule<GotOptions extends HostRulesGotOptions>(
|
||||||
url: string,
|
url: string,
|
||||||
options: GotOptions,
|
options: GotOptions,
|
||||||
): HostRule {
|
): HostRule {
|
||||||
const { hostType } = options;
|
const { hostType, readOnly } = options;
|
||||||
let res = hostRules.find({ hostType, url });
|
let res = hostRules.find({ hostType, url, readOnly });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
is.nonEmptyString(res.token) ||
|
is.nonEmptyString(res.token) ||
|
||||||
|
|
|
@ -162,6 +162,13 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
|
|
||||||
applyDefaultHeaders(options);
|
applyDefaultHeaders(options);
|
||||||
|
|
||||||
|
if (
|
||||||
|
is.undefined(options.readOnly) &&
|
||||||
|
['head', 'get'].includes(options.method)
|
||||||
|
) {
|
||||||
|
options.readOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
const hostRule = findMatchingRule(url, options);
|
const hostRule = findMatchingRule(url, options);
|
||||||
options = applyHostRule(url, options, hostRule);
|
options = applyHostRule(url, options, hostRule);
|
||||||
if (options.enabled === false) {
|
if (options.enabled === false) {
|
||||||
|
@ -457,6 +464,14 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDefaultHeaders(combinedOptions);
|
applyDefaultHeaders(combinedOptions);
|
||||||
|
|
||||||
|
if (
|
||||||
|
is.undefined(combinedOptions.readOnly) &&
|
||||||
|
['head', 'get'].includes(combinedOptions.method)
|
||||||
|
) {
|
||||||
|
combinedOptions.readOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
const hostRule = findMatchingRule(url, combinedOptions);
|
const hostRule = findMatchingRule(url, combinedOptions);
|
||||||
combinedOptions = applyHostRule(resolvedUrl, combinedOptions, hostRule);
|
combinedOptions = applyHostRule(resolvedUrl, combinedOptions, hostRule);
|
||||||
if (combinedOptions.enabled === false) {
|
if (combinedOptions.enabled === false) {
|
||||||
|
|
|
@ -48,6 +48,7 @@ export interface GraphqlOptions {
|
||||||
cursor?: string | null;
|
cursor?: string | null;
|
||||||
acceptHeader?: string;
|
acceptHeader?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpOptions {
|
export interface HttpOptions {
|
||||||
|
@ -67,6 +68,7 @@ export interface HttpOptions {
|
||||||
token?: string;
|
token?: string;
|
||||||
memCache?: boolean;
|
memCache?: boolean;
|
||||||
cacheProvider?: HttpCacheProvider;
|
cacheProvider?: HttpCacheProvider;
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternalHttpOptions extends HttpOptions {
|
export interface InternalHttpOptions extends HttpOptions {
|
||||||
|
|
|
@ -53,6 +53,7 @@ export class GitHubChangeLogSource extends ChangeLogSource {
|
||||||
const { token } = hostRules.find({
|
const { token } = hostRules.find({
|
||||||
hostType: 'github',
|
hostType: 'github',
|
||||||
url,
|
url,
|
||||||
|
readOnly: true,
|
||||||
});
|
});
|
||||||
// istanbul ignore if
|
// istanbul ignore if
|
||||||
if (host && !token) {
|
if (host && !token) {
|
||||||
|
|
Loading…
Reference in a new issue