Skip to content

Commit

Permalink
Migrates [Nexus] service to new service model (#2520)
Browse files Browse the repository at this point in the history
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
calebcartwright authored and paulmelnikow committed Dec 19, 2018
1 parent 63f8b25 commit 6d3798f
Show file tree
Hide file tree
Showing 3 changed files with 391 additions and 134 deletions.
296 changes: 196 additions & 100 deletions services/nexus/nexus.service.js
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 }
}
}
Loading

0 comments on commit 6d3798f

Please sign in to comment.