refactor(datasource/nuget): Convert to class (#14140)

* refactor(datasource/nuget): Convert to class

* Fix strict nulls and obsolete URL

* Fixes

* Fix mutability

Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
Sergei Zharinov 2022-02-13 16:55:03 +03:00 committed by GitHub
parent f72517dc2f
commit b0ce30b59a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 133 additions and 112 deletions

View file

@ -26,7 +26,7 @@ import { JenkinsPluginsDatasource } from './jenkins-plugins';
import * as maven from './maven';
import { NodeDatasource } from './node';
import * as npm from './npm';
import * as nuget from './nuget';
import { NugetDatasource } from './nuget';
import { OrbDatasource } from './orb';
import { PackagistDatasource } from './packagist';
import { PodDatasource } from './pod';
@ -71,7 +71,7 @@ api.set(JenkinsPluginsDatasource.id, new JenkinsPluginsDatasource());
api.set('maven', maven);
api.set(NodeDatasource.id, new NodeDatasource());
api.set('npm', npm);
api.set('nuget', nuget);
api.set(NugetDatasource.id, new NugetDatasource());
api.set(OrbDatasource.id, new OrbDatasource());
api.set(PackagistDatasource.id, new PackagistDatasource());
api.set(PodDatasource.id, new PodDatasource());

View file

@ -1,9 +1,37 @@
import { logger } from '../../logger';
import { regEx } from '../../util/regex';
export const id = 'nuget';
import { parseUrl } from '../../util/url';
import type { ParsedRegistryUrl } from './types';
const buildMetaRe = regEx(/\+.+$/g);
export function removeBuildMeta(version: string): string {
return version?.replace(buildMetaRe, '');
}
const protocolVersionRegExp = regEx(/#protocolVersion=(?<protocol>2|3)/);
export function parseRegistryUrl(registryUrl: string): ParsedRegistryUrl {
const parsedUrl = parseUrl(registryUrl);
if (!parsedUrl) {
logger.debug(
{ urL: registryUrl },
`nuget registry failure: can't parse ${registryUrl}`
);
return { feedUrl: registryUrl, protocolVersion: null };
}
let protocolVersion = 2;
const protocolVersionMatch = protocolVersionRegExp.exec(
parsedUrl.hash
)?.groups;
if (protocolVersionMatch) {
const { protocol } = protocolVersionMatch;
parsedUrl.hash = '';
protocolVersion = Number.parseInt(protocol, 10);
} else if (parsedUrl.pathname.endsWith('.json')) {
protocolVersion = 3;
}
const feedUrl = parsedUrl.href;
return { feedUrl, protocolVersion };
}

View file

@ -3,7 +3,10 @@ import * as httpMock from '../../../test/http-mock';
import { loadFixture } from '../../../test/util';
import * as _hostRules from '../../util/host-rules';
import { id as versioning } from '../../versioning/nuget';
import { id as datasource, parseRegistryUrl } from '.';
import { parseRegistryUrl } from './common';
import { NugetDatasource } from '.';
const datasource = NugetDatasource.id;
const hostRules: any = _hostRules;
@ -118,11 +121,9 @@ describe('datasource/nuget/index', () => {
});
it('returns null for unparseable', () => {
const parsed = parseRegistryUrl(
'https://test:malfor%5Med@test.example.com'
);
const parsed = parseRegistryUrl('https://test.example.com:abc');
expect(parsed.feedUrl).toBe('https://test:malfor%5Med@test.example.com');
expect(parsed.feedUrl).toBe('https://test.example.com:abc');
expect(parsed.protocolVersion).toBeNull();
});
});

View file

@ -1,54 +1,42 @@
import urlApi from 'url';
import { logger } from '../../logger';
import { regEx } from '../../util/regex';
import * as nugetVersioning from '../../versioning/nuget';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import { parseRegistryUrl } from './common';
import * as v2 from './v2';
import * as v3 from './v3';
export { id } from './common';
// https://api.nuget.org/v3/index.json is a default official nuget feed
export const defaultRegistryUrls = ['https://api.nuget.org/v3/index.json'];
export const customRegistrySupport = true;
export const defaultRegistryUrls = [v3.getDefaultFeed()];
export const defaultVersioning = nugetVersioning.id;
export const registryStrategy = 'merge';
export class NugetDatasource extends Datasource {
static readonly id = 'nuget';
export function parseRegistryUrl(registryUrl: string): {
feedUrl: string;
protocolVersion: number;
} {
try {
const parsedUrl = urlApi.parse(registryUrl);
let protocolVersion = 2;
const protocolVersionRegExp = regEx(/#protocolVersion=(2|3)/);
const protocolVersionMatch = protocolVersionRegExp.exec(parsedUrl.hash);
if (protocolVersionMatch) {
parsedUrl.hash = '';
protocolVersion = Number.parseInt(protocolVersionMatch[1], 10);
} else if (parsedUrl.pathname.endsWith('.json')) {
protocolVersion = 3;
override readonly defaultRegistryUrls = defaultRegistryUrls;
override readonly defaultVersioning = nugetVersioning.id;
override readonly registryStrategy = 'merge';
constructor() {
super(NugetDatasource.id);
}
return { feedUrl: urlApi.format(parsedUrl), protocolVersion };
} catch (err) {
logger.debug({ err }, `nuget registry failure: can't parse ${registryUrl}`);
return { feedUrl: registryUrl, protocolVersion: null };
}
}
export async function getReleases({
async getReleases({
lookupName,
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult> {
}: GetReleasesConfig): Promise<ReleaseResult> {
logger.trace(`nuget.getReleases(${lookupName})`);
const { feedUrl, protocolVersion } = parseRegistryUrl(registryUrl);
if (protocolVersion === 2) {
return v2.getReleases(feedUrl, lookupName);
return v2.getReleases(this.http, feedUrl, lookupName);
}
if (protocolVersion === 3) {
const queryUrl = await v3.getResourceUrl(feedUrl);
const queryUrl = await v3.getResourceUrl(this.http, feedUrl);
if (queryUrl) {
return v3.getReleases(feedUrl, queryUrl, lookupName);
return v3.getReleases(this.http, feedUrl, queryUrl, lookupName);
}
}
return null;
}
}

View file

@ -22,3 +22,8 @@ export interface CatalogPage {
export interface PackageRegistration {
items: CatalogPage[];
}
export interface ParsedRegistryUrl {
feedUrl: string;
protocolVersion: number | null;
}

View file

@ -1,17 +1,16 @@
import { XmlDocument, XmlElement } from 'xmldoc';
import { logger } from '../../logger';
import { Http } from '../../util/http';
import type { Http } from '../../util/http';
import { regEx } from '../../util/regex';
import type { ReleaseResult } from '../types';
import { id, removeBuildMeta } from './common';
const http = new Http(id);
import { removeBuildMeta } from './common';
function getPkgProp(pkgInfo: XmlElement, propName: string): string {
return pkgInfo.childNamed('m:properties').childNamed(`d:${propName}`)?.val;
}
export async function getReleases(
http: Http,
feedUrl: string,
pkgName: string
): Promise<ReleaseResult | null> {

View file

@ -5,12 +5,12 @@ import { XmlDocument } from 'xmldoc';
import { logger } from '../../logger';
import { ExternalHostError } from '../../types/errors/external-host-error';
import * as packageCache from '../../util/cache/package';
import { Http } from '../../util/http';
import type { Http } from '../../util/http';
import { HttpError } from '../../util/http/types';
import { regEx } from '../../util/regex';
import { ensureTrailingSlash } from '../../util/url';
import type { Release, ReleaseResult } from '../types';
import { id, removeBuildMeta } from './common';
import { removeBuildMeta } from './common';
import type {
CatalogEntry,
CatalogPage,
@ -18,17 +18,10 @@ import type {
ServicesIndexRaw,
} from './types';
const http = new Http(id);
// https://api.nuget.org/v3/index.json is a default official nuget feed
const defaultNugetFeed = 'https://api.nuget.org/v3/index.json';
const cacheNamespace = 'datasource-nuget';
export function getDefaultFeed(): string {
return defaultNugetFeed;
}
export async function getResourceUrl(
http: Http,
url: string,
resourceType = 'RegistrationsBaseUrl'
): Promise<string | null> {
@ -101,6 +94,7 @@ export async function getResourceUrl(
}
async function getCatalogEntry(
http: Http,
catalogPage: CatalogPage
): Promise<CatalogEntry[]> {
let items = catalogPage.items;
@ -113,6 +107,7 @@ async function getCatalogEntry(
}
export async function getReleases(
http: Http,
registryUrl: string,
feedUrl: string,
pkgName: string
@ -122,7 +117,7 @@ export async function getReleases(
const packageRegistration = await http.getJson<PackageRegistration>(url);
const catalogPages = packageRegistration.body.items || [];
const catalogPagesQueue = catalogPages.map(
(page) => (): Promise<CatalogEntry[]> => getCatalogEntry(page)
(page) => (): Promise<CatalogEntry[]> => getCatalogEntry(http, page)
);
const catalogEntries = (
await pAll(catalogPagesQueue, { concurrency: 5 })
@ -164,6 +159,7 @@ export async function getReleases(
try {
const packageBaseAddress = await getResourceUrl(
http,
registryUrl,
'PackageBaseAddress'
);

View file

@ -1,6 +1,6 @@
import moo from 'moo';
import { ProgrammingLanguage } from '../../constants';
import { id as datasource } from '../../datasource/nuget';
import { NugetDatasource } from '../../datasource/nuget';
import { regEx } from '../../util/regex';
import type { PackageDependency, PackageFile } from '../types';
@ -36,7 +36,11 @@ function parseDependencyLine(line: string): PackageDependency | null {
const depName = searchParams.get('package');
const currentValue = searchParams.get('version');
const result: PackageDependency = { datasource, depName, currentValue };
const result: PackageDependency = {
datasource: NugetDatasource.id,
depName,
currentValue,
};
if (!isEmptyHost) {
if (protocol.startsWith('http')) {
@ -69,4 +73,4 @@ export function extractPackageFile(content: string): PackageFile {
return { deps };
}
export const supportedDatasources = [datasource];
export const supportedDatasources = [NugetDatasource.id];

View file

@ -2,7 +2,8 @@ import { join } from 'path';
import { quote } from 'shlex';
import { GlobalConfig } from '../../config/global';
import { TEMPORARY_ERROR } from '../../constants/error-messages';
import { id, parseRegistryUrl } from '../../datasource/nuget';
import { NugetDatasource } from '../../datasource/nuget';
import { parseRegistryUrl } from '../../datasource/nuget/common';
import { logger } from '../../logger';
import { exec } from '../../util/exec';
import type { ExecOptions } from '../../util/exec/types';
@ -39,7 +40,7 @@ async function addSourceCmds(
const result = [];
for (const registry of registries) {
const { username, password } = hostRules.find({
hostType: id,
hostType: NugetDatasource.id,
url: registry.url,
});
const registryInfo = parseRegistryUrl(registry.url);

View file

@ -1,6 +1,6 @@
import { XmlDocument, XmlElement, XmlNode } from 'xmldoc';
import { GlobalConfig } from '../../config/global';
import * as datasourceNuget from '../../datasource/nuget';
import { NugetDatasource } from '../../datasource/nuget';
import { logger } from '../../logger';
import { getSiblingFileName, localPathExists } from '../../util/fs';
import { hasKey } from '../../util/object';
@ -54,7 +54,7 @@ function extractDepsFromXml(xmlNode: XmlDocument): PackageDependency[] {
?.groups?.currentValue?.trim();
if (depName && currentValue) {
results.push({
datasource: datasourceNuget.id,
datasource: NugetDatasource.id,
depType: 'nuget',
depName,
currentValue,
@ -103,7 +103,7 @@ export async function extractPackageFile(
depType: 'nuget',
depName,
currentValue,
datasource: datasourceNuget.id,
datasource: NugetDatasource.id,
};
if (registryUrls) {
dep.registryUrls = registryUrls;

View file

@ -1,4 +1,4 @@
import * as datasourceNuget from '../../../datasource/nuget';
import { NugetDatasource } from '../../../datasource/nuget';
import { logger } from '../../../logger';
import type { PackageDependency, PackageFile } from '../../types';
import type { MsbuildGlobalManifest } from '../types';
@ -41,7 +41,7 @@ export function extractMsbuildGlobalManifest(
depType: 'msbuild-sdk',
depName,
currentValue,
datasource: datasourceNuget.id,
datasource: NugetDatasource.id,
};
deps.push(dep);

View file

@ -1,5 +1,5 @@
import { ProgrammingLanguage } from '../../constants';
import * as datasourceNuget from '../../datasource/nuget';
import { NugetDatasource } from '../../datasource/nuget';
export { extractPackageFile } from './extract';
export { updateArtifacts } from './artifacts';
@ -15,4 +15,4 @@ export const defaultConfig = {
],
};
export const supportedDatasources = [datasourceNuget.id];
export const supportedDatasources = [NugetDatasource.id];

View file

@ -2,7 +2,7 @@ import cryptoRandomString from 'crypto-random-string';
import findUp from 'find-up';
import upath from 'upath';
import { XmlDocument } from 'xmldoc';
import * as datasourceNuget from '../../datasource/nuget';
import { defaultRegistryUrls } from '../../datasource/nuget';
import { logger } from '../../logger';
import { readFile } from '../../util/fs';
import { regEx } from '../../util/regex';
@ -22,13 +22,12 @@ export function getRandomString(): string {
return cryptoRandomString({ length: 16 });
}
const defaultRegistries = defaultRegistryUrls.map(
(registryUrl) => ({ url: registryUrl } as Registry)
);
export function getDefaultRegistries(): Registry[] {
return datasourceNuget.defaultRegistryUrls.map(
(registryUrl) =>
({
url: registryUrl,
} as Registry)
);
return [...defaultRegistries];
}
export async function getConfiguredRegistries(

View file

@ -1,5 +1,5 @@
import { PlatformId } from '../constants';
import * as datasourceNuget from '../datasource/nuget';
import { NugetDatasource } from '../datasource/nuget';
import { add, clear, find, findAll, getAll, hosts } from './host-rules';
describe('util/host-rules', () => {
@ -55,21 +55,21 @@ describe('util/host-rules', () => {
});
it('needs exact host matches', () => {
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
hostName: 'nuget.org',
username: 'root',
password: 'p4$$w0rd',
token: undefined,
} as any);
expect(find({ hostType: datasourceNuget.id })).toEqual({});
expect(find({ hostType: NugetDatasource.id })).toEqual({});
expect(
find({ hostType: datasourceNuget.id, url: 'https://nuget.org' })
find({ hostType: NugetDatasource.id, url: 'https://nuget.org' })
).not.toEqual({});
expect(
find({ hostType: datasourceNuget.id, url: 'https://not.nuget.org' })
find({ hostType: NugetDatasource.id, url: 'https://not.nuget.org' })
).not.toEqual({});
expect(
find({ hostType: datasourceNuget.id, url: 'https://not-nuget.org' })
find({ hostType: NugetDatasource.id, url: 'https://not-nuget.org' })
).toEqual({});
});
it('matches on empty rules', () => {
@ -77,16 +77,16 @@ describe('util/host-rules', () => {
enabled: true,
});
expect(
find({ hostType: datasourceNuget.id, url: 'https://api.github.com' })
find({ hostType: NugetDatasource.id, url: 'https://api.github.com' })
).toEqual({ enabled: true });
});
it('matches on hostType', () => {
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
token: 'abc',
});
expect(
find({ hostType: datasourceNuget.id, url: 'https://nuget.local/api' })
find({ hostType: NugetDatasource.id, url: 'https://nuget.local/api' })
).toEqual({ token: 'abc' });
});
it('matches on domainName', () => {
@ -95,14 +95,14 @@ describe('util/host-rules', () => {
token: 'def',
} as any);
expect(
find({ hostType: datasourceNuget.id, url: 'https://api.github.com' })
find({ hostType: NugetDatasource.id, url: 'https://api.github.com' })
.token
).toBe('def');
expect(
find({ hostType: datasourceNuget.id, url: 'https://github.com' }).token
find({ hostType: NugetDatasource.id, url: 'https://github.com' }).token
).toBe('def');
expect(
find({ hostType: datasourceNuget.id, url: 'https://apigithub.com' })
find({ hostType: NugetDatasource.id, url: 'https://apigithub.com' })
.token
).toBeUndefined();
});
@ -178,7 +178,7 @@ describe('util/host-rules', () => {
token: 'abc',
} as any);
expect(
find({ hostType: datasourceNuget.id, url: 'https://nuget.local/api' })
find({ hostType: NugetDatasource.id, url: 'https://nuget.local/api' })
).toEqual({ token: 'abc' });
});
it('matches on matchHost with protocol', () => {
@ -190,7 +190,7 @@ describe('util/host-rules', () => {
expect(find({ url: 'https://domain.com' }).token).toBe('def');
expect(
find({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
url: 'https://domain.com/renovatebot',
}).token
).toBe('def');
@ -215,55 +215,55 @@ describe('util/host-rules', () => {
});
it('matches on hostType and endpoint', () => {
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
matchHost: 'https://nuget.local/api',
token: 'abc',
} as any);
expect(
find({ hostType: datasourceNuget.id, url: 'https://nuget.local/api' })
find({ hostType: NugetDatasource.id, url: 'https://nuget.local/api' })
.token
).toBe('abc');
});
it('matches on endpoint subresource', () => {
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
matchHost: 'https://nuget.local/api',
token: 'abc',
} as any);
expect(
find({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
url: 'https://nuget.local/api/sub-resource',
})
).toEqual({ token: 'abc' });
});
it('returns hosts', () => {
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
token: 'aaaaaa',
});
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
matchHost: 'https://nuget.local/api',
token: 'abc',
} as any);
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
hostName: 'my.local.registry',
token: 'def',
} as any);
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
matchHost: 'another.local.registry',
token: 'xyz',
});
add({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
matchHost: 'https://yet.another.local.registry',
token: '123',
});
const res = hosts({
hostType: datasourceNuget.id,
hostType: NugetDatasource.id,
});
expect(res).toEqual([
'nuget.local',

View file

@ -2,7 +2,7 @@ import type { PackageRule, RenovateConfig } from '../../../config/types';
import { NO_VULNERABILITY_ALERTS } from '../../../constants/error-messages';
import * as datasourceMaven from '../../../datasource/maven';
import { id as npmId } from '../../../datasource/npm';
import * as datasourceNuget from '../../../datasource/nuget';
import { NugetDatasource } from '../../../datasource/nuget';
import { PypiDatasource } from '../../../datasource/pypi';
import { RubyGemsDatasource } from '../../../datasource/rubygems';
import { logger } from '../../../logger';
@ -89,7 +89,7 @@ export async function detectVulnerabilityAlerts(
const datasourceMapping: Record<string, string> = {
MAVEN: datasourceMaven.id,
NPM: npmId,
NUGET: datasourceNuget.id,
NUGET: NugetDatasource.id,
PIP: PypiDatasource.id,
RUBYGEMS: RubyGemsDatasource.id,
};