-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrates [Nexus] service to new service model (#2520)
Ports the Nexus service to the new service model. Some related/relevant conversation in #2347 (and closes #2347). Also adds support for authentication which resolves #1699.
- Loading branch information
1 parent
63f8b25
commit 6d3798f
Showing
3 changed files
with
391 additions
and
134 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,130 +1,226 @@ | ||
'use strict' | ||
|
||
const LegacyService = require('../legacy-service') | ||
const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') | ||
const { isSnapshotVersion: isNexusSnapshotVersion } = require('./nexus-version') | ||
const { addv: versionText } = require('../../lib/text-formatters') | ||
const Joi = require('joi') | ||
|
||
const BaseJsonService = require('../base-json') | ||
const { InvalidResponse, NotFound } = require('../errors') | ||
const { isSnapshotVersion } = require('./nexus-version') | ||
const { version: versionColor } = require('../../lib/color-formatters') | ||
const { addv } = require('../../lib/text-formatters') | ||
const serverSecrets = require('../../lib/server-secrets') | ||
const { | ||
optionalDottedVersionNClausesWithOptionalSuffix, | ||
} = require('../validators') | ||
|
||
const searchApiSchema = Joi.object({ | ||
data: Joi.array() | ||
.items( | ||
Joi.object({ | ||
latestRelease: optionalDottedVersionNClausesWithOptionalSuffix, | ||
latestSnapshot: optionalDottedVersionNClausesWithOptionalSuffix, | ||
version: optionalDottedVersionNClausesWithOptionalSuffix, | ||
}) | ||
) | ||
.required(), | ||
}).required() | ||
|
||
const resolveApiSchema = Joi.object({ | ||
data: Joi.object({ | ||
baseVersion: optionalDottedVersionNClausesWithOptionalSuffix, | ||
version: optionalDottedVersionNClausesWithOptionalSuffix, | ||
}).required(), | ||
}).required() | ||
|
||
module.exports = class Nexus extends BaseJsonService { | ||
static render({ version }) { | ||
return { | ||
message: addv(version), | ||
color: versionColor(version), | ||
} | ||
} | ||
|
||
// This legacy service should be rewritten to use e.g. BaseJsonService. | ||
// | ||
// Tips for rewriting: | ||
// https://github.com/badges/shields/blob/master/doc/rewriting-services.md | ||
// | ||
// Do not base new services on this code. | ||
module.exports = class Nexus extends LegacyService { | ||
static get category() { | ||
return 'version' | ||
} | ||
|
||
static get route() { | ||
return { | ||
base: 'nexus', | ||
// API pattern: | ||
// /nexus/(r|s|<repo-name>)/(http|https)/<nexus.host>[:port][/<entry-path>]/<group>/<artifact>[:k1=v1[:k2=v2[...]]] | ||
format: | ||
'(r|s|[^/]+)/(https?)/((?:[^/]+)(?:/[^/]+)?)/([^/]+)/([^/:]+)(:.+)?', | ||
capture: ['repo', 'scheme', 'host', 'groupId', 'artifactId', 'queryOpt'], | ||
} | ||
} | ||
|
||
static get defaultBadgeData() { | ||
return { color: 'blue', label: 'nexus' } | ||
} | ||
|
||
static get examples() { | ||
return [ | ||
{ | ||
title: 'Sonatype Nexus (Releases)', | ||
previewUrl: 'r/https/oss.sonatype.org/com.google.guava/guava', | ||
pattern: 'r/:scheme/:host/:groupId/:artifactId', | ||
namedParams: { | ||
scheme: 'https', | ||
host: 'oss.sonatype.org', | ||
groupId: 'com.google.guava', | ||
artifactId: 'guava', | ||
}, | ||
staticExample: this.render({ | ||
version: 'v27.0.1-jre', | ||
}), | ||
}, | ||
{ | ||
title: 'Sonatype Nexus (Snapshots)', | ||
previewUrl: 's/https/oss.sonatype.org/com.google.guava/guava', | ||
pattern: 's/:scheme/:host/:groupId/:artifactId', | ||
namedParams: { | ||
scheme: 'https', | ||
host: 'oss.sonatype.org', | ||
groupId: 'com.google.guava', | ||
artifactId: 'guava', | ||
}, | ||
staticExample: this.render({ | ||
version: 'v24.0-SNAPSHOT', | ||
}), | ||
}, | ||
{ | ||
title: 'Sonatype Nexus (Repository)', | ||
pattern: ':repo/:scheme/:host/:groupId/:artifactId', | ||
namedParams: { | ||
repo: 'developer', | ||
scheme: 'https', | ||
host: 'repository.jboss.org/nexus', | ||
groupId: 'ai.h2o', | ||
artifactId: 'h2o-automl', | ||
}, | ||
staticExample: this.render({ | ||
version: '3.22.0.2', | ||
}), | ||
}, | ||
{ | ||
title: 'Sonatype Nexus (Query Options)', | ||
pattern: ':repo/:scheme/:host/:groupId/:artifactId/:queryOpt', | ||
namedParams: { | ||
repo: 'fs-public-snapshots', | ||
scheme: 'https', | ||
host: 'repository.jboss.org/nexus', | ||
groupId: 'com.progress.fuse', | ||
artifactId: 'fusehq', | ||
queryOpt: ':c=agent-apple-osx:p=tar.gz', | ||
}, | ||
staticExample: this.render({ | ||
version: '7.0.1-SNAPSHOT', | ||
}), | ||
documentation: ` | ||
<p> | ||
Note that you can use query options with any Nexus badge type (Releases, Snapshots, or Repository) | ||
</p> | ||
<p> | ||
Query options should be provided as key=value pairs separated by a semicolon | ||
</p> | ||
`, | ||
}, | ||
] | ||
} | ||
|
||
static registerLegacyRouteHandler({ camp, cache }) { | ||
// standalone sonatype nexus installation | ||
transform({ repo, json }) { | ||
if (repo === 'r') { | ||
return { version: json.data[0].latestRelease } | ||
} else if (repo === 's') { | ||
// only want to match 1.2.3-SNAPSHOT style versions, which may not always be in | ||
// 'latestSnapshot' so check 'version' as well before continuing to next entry | ||
for (const artifact of json.data) { | ||
if (isSnapshotVersion(artifact.latestSnapshot)) { | ||
return { version: artifact.latestSnapshot } | ||
} | ||
if (isSnapshotVersion(artifact.version)) { | ||
return { version: artifact.version } | ||
} | ||
} | ||
throw new InvalidResponse({ prettyMessage: 'no snapshot versions found' }) | ||
} else { | ||
return { version: json.data.baseVersion || json.data.version } | ||
} | ||
} | ||
|
||
async handle({ repo, scheme, host, groupId, artifactId, queryOpt }) { | ||
const { json } = await this.fetch({ | ||
repo, | ||
scheme, | ||
host, | ||
groupId, | ||
artifactId, | ||
queryOpt, | ||
}) | ||
if (json.data.length === 0) { | ||
throw new NotFound({ prettyMessage: 'artifact or version not found' }) | ||
} | ||
const { version } = this.transform({ repo, json }) | ||
if (!version) { | ||
throw new InvalidResponse({ prettyMessage: 'invalid artifact version' }) | ||
} | ||
return this.constructor.render({ version }) | ||
} | ||
|
||
addQueryParamsToQueryString({ qs, queryOpt }) { | ||
// Users specify query options with 'key=value' pairs, using a | ||
// semicolon delimiter between pairs ([:k1=v1[:k2=v2[...]]]). | ||
// queryOpt will be a string containing those key/value pairs, | ||
// For example: :c=agent-apple-osx:p=tar.gz | ||
const keyValuePairs = queryOpt.split(':') | ||
keyValuePairs.forEach(keyValuePair => { | ||
const paramParts = keyValuePair.split('=') | ||
const paramKey = paramParts[0] | ||
const paramValue = paramParts[1] | ||
qs[paramKey] = paramValue | ||
}) | ||
} | ||
|
||
async fetch({ repo, scheme, host, groupId, artifactId, queryOpt }) { | ||
const qs = { | ||
g: groupId, | ||
a: artifactId, | ||
} | ||
let schema | ||
let url = `${scheme}://${host}/` | ||
// API pattern: | ||
// /nexus/(r|s|<repo-name>)/(http|https)/<nexus.host>[:port][/<entry-path>]/<group>/<artifact>[:k1=v1[:k2=v2[...]]].<format> | ||
// for /nexus/[rs]/... pattern, use the search api of the nexus server, and | ||
// for /nexus/<repo-name>/... pattern, use the resolve api of the nexus server. | ||
camp.route( | ||
/^\/nexus\/(r|s|[^/]+)\/(https?)\/((?:[^/]+)(?:\/[^/]+)?)\/([^/]+)\/([^/:]+)(:.+)?\.(svg|png|gif|jpg|json)$/, | ||
cache((data, match, sendBadge, request) => { | ||
const repo = match[1] // r | s | repo-name | ||
const scheme = match[2] // http | https | ||
const host = match[3] // eg, `nexus.example.com` | ||
const groupId = encodeURIComponent(match[4]) // eg, `com.google.inject` | ||
const artifactId = encodeURIComponent(match[5]) // eg, `guice` | ||
const queryOpt = (match[6] || '').replace(/:/g, '&') // eg, `&p=pom&c=doc` | ||
const format = match[7] | ||
|
||
const badgeData = getBadgeData('nexus', data) | ||
|
||
const apiUrl = `${scheme}://${host}${ | ||
repo === 'r' || repo === 's' | ||
? `/service/local/lucene/search?g=${groupId}&a=${artifactId}${queryOpt}` | ||
: `/service/local/artifact/maven/resolve?r=${repo}&g=${groupId}&a=${artifactId}&v=LATEST${queryOpt}` | ||
}` | ||
|
||
request( | ||
apiUrl, | ||
{ headers: { Accept: 'application/json' } }, | ||
(err, res, buffer) => { | ||
if (err != null) { | ||
badgeData.text[1] = 'inaccessible' | ||
sendBadge(format, badgeData) | ||
return | ||
} else if (res && res.statusCode === 404) { | ||
badgeData.text[1] = 'no-artifact' | ||
sendBadge(format, badgeData) | ||
return | ||
} | ||
try { | ||
const parsed = JSON.parse(buffer) | ||
let version = '0' | ||
switch (repo) { | ||
case 'r': | ||
if (parsed.data.length === 0) { | ||
badgeData.text[1] = 'no-artifact' | ||
sendBadge(format, badgeData) | ||
return | ||
} | ||
version = parsed.data[0].latestRelease | ||
break | ||
case 's': | ||
if (parsed.data.length === 0) { | ||
badgeData.text[1] = 'no-artifact' | ||
sendBadge(format, badgeData) | ||
return | ||
} | ||
// only want to match 1.2.3-SNAPSHOT style versions, which may not always be in | ||
// 'latestSnapshot' so check 'version' as well before continuing to next entry | ||
parsed.data.every(artifact => { | ||
if (isNexusSnapshotVersion(artifact.latestSnapshot)) { | ||
version = artifact.latestSnapshot | ||
return | ||
} | ||
if (isNexusSnapshotVersion(artifact.version)) { | ||
version = artifact.version | ||
return | ||
} | ||
return true | ||
}) | ||
break | ||
default: | ||
version = parsed.data.baseVersion || parsed.data.version | ||
break | ||
} | ||
if (version !== '0') { | ||
badgeData.text[1] = versionText(version) | ||
badgeData.colorscheme = versionColor(version) | ||
} else { | ||
badgeData.text[1] = 'undefined' | ||
badgeData.colorscheme = 'orange' | ||
} | ||
sendBadge(format, badgeData) | ||
} catch (e) { | ||
badgeData.text[1] = 'invalid' | ||
sendBadge(format, badgeData) | ||
} | ||
} | ||
) | ||
}) | ||
) | ||
if (repo === 'r' || repo === 's') { | ||
schema = searchApiSchema | ||
url += 'service/local/lucene/search' | ||
} else { | ||
schema = resolveApiSchema | ||
url += 'service/local/artifact/maven/resolve' | ||
qs.r = repo | ||
qs.v = 'LATEST' | ||
} | ||
|
||
if (queryOpt) { | ||
this.addQueryParamsToQueryString({ qs, queryOpt }) | ||
} | ||
|
||
const options = { qs } | ||
|
||
if (serverSecrets && serverSecrets.nexus_user) { | ||
options.auth = { | ||
user: serverSecrets.nexus_user, | ||
pass: serverSecrets.nexus_pass, | ||
} | ||
} | ||
|
||
const json = await this._requestJson({ | ||
schema, | ||
url, | ||
options, | ||
errorMessages: { | ||
404: 'artifact not found', | ||
}, | ||
}) | ||
|
||
return { json } | ||
} | ||
} |
Oops, something went wrong.