Skip to content

Commit

Permalink
feat(host-rules): Support readOnly request matching (#28562)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <rhys@arkins.net>
  • Loading branch information
zharinov and rarkins committed Apr 23, 2024
1 parent e82e747 commit 5c0628b
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 9 deletions.
21 changes: 21 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

If the `currentValue` for a dependency is not captured with a named group then it can be defined in config using this field.
Expand Down
10 changes: 10 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2476,6 +2476,16 @@ const options: RenovateOptions[] = [
cli: 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',
description: 'Timeout (in milliseconds) for queries to external endpoints.',
Expand Down
4 changes: 4 additions & 0 deletions lib/modules/platform/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ export async function initRepo({
const opts = hostRules.find({
hostType: 'github',
url: platformConfig.endpoint,
readOnly: true,
});
config.renovateUsername = renovateUsername;
[config.repositoryOwner, config.repositoryName] = repository.split('/');
Expand Down Expand Up @@ -499,6 +500,7 @@ export async function initRepo({
name: config.repositoryName,
user: renovateUsername,
},
readOnly: true,
});

if (res?.errors) {
Expand Down Expand Up @@ -1214,6 +1216,7 @@ async function getIssues(): Promise<Issue[]> {
name: config.repositoryName,
user: config.renovateUsername,
},
readOnly: true,
},
);

Expand Down Expand Up @@ -1975,6 +1978,7 @@ export async function getVulnerabilityAlerts(): Promise<VulnerabilityAlert[]> {
variables: { owner: config.repositoryOwner, name: config.repositoryName },
paginate: false,
acceptHeader: 'application/vnd.github.vixen-preview+json',
readOnly: true,
});
} catch (err) {
logger.debug({ err }, 'Error retrieving vulnerability alerts');
Expand Down
3 changes: 2 additions & 1 deletion lib/types/host-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export interface HostRule {
hostType?: string;
matchHost?: string;
resolvedHost?: string;
readOnly?: boolean;
}

export type CombinedHostRule = Omit<
HostRule,
'encrypted' | 'hostType' | 'matchHost' | 'resolvedHost'
'encrypted' | 'hostType' | 'matchHost' | 'resolvedHost' | 'readOnly'
>;
1 change: 1 addition & 0 deletions lib/util/github/graphql/datasource-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export class GithubGraphqlDatasourceFetcher<
return {
baseUrl,
repository,
readOnly: true,
body: { query, variables },
};
}
Expand Down
19 changes: 19 additions & 0 deletions lib/util/host-rules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,25 @@ describe('util/host-rules', () => {
}),
).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()', () => {
Expand Down
18 changes: 15 additions & 3 deletions lib/util/host-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function add(params: HostRule): void {
export interface HostRuleSearch {
hostType?: string;
url?: string;
readOnly?: boolean;
}

function matchesHost(url: string, matchHost: string): boolean {
Expand Down Expand Up @@ -107,8 +108,9 @@ function fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number {
return a.matchHost.length - b.matchHost.length;
}

function hostRuleRank({ hostType, matchHost }: HostRule): number {
if (hostType && matchHost) {
function hostRuleRank({ hostType, matchHost, readOnly }: HostRule): number {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if ((hostType || readOnly) && matchHost) {
return 3;
}

Expand Down Expand Up @@ -142,6 +144,7 @@ export function find(search: HostRuleSearch): CombinedHostRule {
for (const rule of sortedRules) {
let hostTypeMatch = true;
let hostMatch = true;
let readOnlyMatch = true;

if (rule.hostType) {
hostTypeMatch = false;
Expand All @@ -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));
}
}
Expand All @@ -166,6 +177,7 @@ export function find(search: HostRuleSearch): CombinedHostRule {
delete res.hostType;
delete res.resolvedHost;
delete res.matchHost;
delete res.readOnly;
return res;
}

Expand Down
12 changes: 11 additions & 1 deletion lib/util/http/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export class GithubHttp extends Http<GithubHttpOptions> {
options?: InternalHttpOptions & GithubHttpOptions,
okToRetry = true,
): Promise<HttpResponse<T>> {
const opts: GithubHttpOptions = {
const opts: InternalHttpOptions & GithubHttpOptions = {
baseUrl,
...options,
throwHttpErrors: true,
Expand All @@ -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(), {
hostType: this.hostType,
readOnly,
});
opts.token = token;
}
Expand Down Expand Up @@ -393,6 +402,7 @@ export class GithubHttp extends Http<GithubHttpOptions> {
baseUrl: baseUrl.replace('/v3/', '/'), // GHE uses unversioned graphql path
body,
headers: { accept: options?.acceptHeader },
readOnly: options.readOnly,
};
if (options.token) {
opts.token = options.token;
Expand Down
9 changes: 5 additions & 4 deletions lib/util/http/host-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { matchRegexOrGlobList } from '../string-match';
import { parseUrl } from '../url';
import { dnsLookup } from './dns';
import { keepAliveAgents } from './keep-alive';
import type { GotOptions } from './types';
import type { GotOptions, InternalHttpOptions } from './types';

export type HostRulesGotOptions = Pick<
GotOptions,
GotOptions & InternalHttpOptions,
| 'hostType'
| 'url'
| 'noAuth'
Expand All @@ -34,14 +34,15 @@ export type HostRulesGotOptions = Pick<
| 'agent'
| 'http2'
| 'https'
| 'readOnly'
>;

export function findMatchingRule<GotOptions extends HostRulesGotOptions>(
url: string,
options: GotOptions,
): HostRule {
const { hostType } = options;
let res = hostRules.find({ hostType, url });
const { hostType, readOnly } = options;
let res = hostRules.find({ hostType, url, readOnly });

if (
is.nonEmptyString(res.token) ||
Expand Down
15 changes: 15 additions & 0 deletions lib/util/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ export class Http<Opts extends HttpOptions = HttpOptions> {

applyDefaultHeaders(options);

if (
is.undefined(options.readOnly) &&
['head', 'get'].includes(options.method)
) {
options.readOnly = true;
}

const hostRule = findMatchingRule(url, options);
options = applyHostRule(url, options, hostRule);
if (options.enabled === false) {
Expand Down Expand Up @@ -457,6 +464,14 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
}

applyDefaultHeaders(combinedOptions);

if (
is.undefined(combinedOptions.readOnly) &&
['head', 'get'].includes(combinedOptions.method)
) {
combinedOptions.readOnly = true;
}

const hostRule = findMatchingRule(url, combinedOptions);
combinedOptions = applyHostRule(resolvedUrl, combinedOptions, hostRule);
if (combinedOptions.enabled === false) {
Expand Down
2 changes: 2 additions & 0 deletions lib/util/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface GraphqlOptions {
cursor?: string | null;
acceptHeader?: string;
token?: string;
readOnly?: boolean;
}

export interface HttpOptions {
Expand All @@ -67,6 +68,7 @@ export interface HttpOptions {
token?: string;
memCache?: boolean;
cacheProvider?: HttpCacheProvider;
readOnly?: boolean;
}

export interface InternalHttpOptions extends HttpOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class GitHubChangeLogSource extends ChangeLogSource {
const { token } = hostRules.find({
hostType: 'github',
url,
readOnly: true,
});
// istanbul ignore if
if (host && !token) {
Expand Down

0 comments on commit 5c0628b

Please sign in to comment.