Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrates [Nexus] service to new service model #2520

Merged
merged 19 commits into from
Dec 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
eb8c0f6
feat: ported Nexus to new service model
calebcartwright Dec 13, 2018
09144a9
feat: updated nexus service schema validation
calebcartwright Dec 13, 2018
c394c8c
chore: minor tweak to nexus examples
calebcartwright Dec 13, 2018
eae8940
feat: add authentication for nexus service
calebcartwright Dec 13, 2018
ad736cc
chore: fixed prettier issue on nexus service
calebcartwright Dec 13, 2018
1b82548
Merge branch 'master' into master
calebcartwright Dec 14, 2018
7e96daa
chore: minor update for nexus badge examples
calebcartwright Dec 14, 2018
0146680
Merge branch 'master' into master
calebcartwright Dec 17, 2018
2292c8c
Merge branch 'master' into master
calebcartwright Dec 18, 2018
9a10be1
Merge branch 'master' into master
calebcartwright Dec 18, 2018
6833790
Merge branch 'master' into master
calebcartwright Dec 18, 2018
fbb7ea9
feat: updated nexus service and tests
calebcartwright Dec 18, 2018
1c8fb0c
Merge branch 'master' of https://github.com/calebcartwright/shields
calebcartwright Dec 18, 2018
1a31178
chore: remove duplicative keywords from nexus service
calebcartwright Dec 18, 2018
5aed5e3
feat: updated nexus service
calebcartwright Dec 19, 2018
35144bf
feat: updated nexus service
calebcartwright Dec 19, 2018
28c27f5
Merge branch 'master' into master
calebcartwright Dec 19, 2018
a9fc585
feat: more nexus service cleanup
calebcartwright Dec 19, 2018
c9f38b2
Merge branch 'master' of https://github.com/calebcartwright/shields
calebcartwright Dec 19, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious from reading this what it's supposed to do. For future reference, stuff like this probably should be its own static method (or function) with a unit test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. The original implementation was plugging the query params directly into the url, but that was a lot more readable.

I'll extract this out into its own function with a comment. 🤦‍♂️ on the test. Not sure how I forgot that one!

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