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

implementation for an artifactory service. #9666

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
211 changes: 211 additions & 0 deletions services/artifactory/artifactory.service.js
@@ -0,0 +1,211 @@
import Joi from 'joi'
import { optionalUrl } from '../validators.js'
import { BaseService } from '../index.js'
import { renderVersionBadge } from '../version.js'
import { getCachedResource } from '../../core/base-service/resource-cache.js'

const ARTIFACTORY_LATEST_VERSION_ENDPOINT = 'api/search/latestVersion'
const ONE_HOUR = 1 * 3600 * 1000

const queryParamSchema = Joi.object({
// auth related optional properties
server: optionalUrl.default(''),
token: Joi.string().default(''),
username: Joi.string().default(''),
password: Joi.string().default(''),

// api related optional properties
repos: Joi.string().default(''),
remote: Joi.string().default(''),
version: Joi.string().default(''),
}).required()

const documentation = `
<p>
Query for the last released version of an artifact using the <b>Latest Version</b> endpoint.
<br />
<br />
This service will cache results for <b>1 hour</b> so as not to overload a given Artifactory instance with potentially long running query operations.
<br />
<br />
Authentication is done either through basic auth or using a bearer token. While the options exist to pass along credentials as query params, users
are strongly encouraged to instead set the relevant auth related environment variables server side which this service will pick up and read at runtime.
<br />
<br />
The following environment variables are supported:
<ul>
<li>ARTIFACTORY_URL</li>
<li>ARTIFACTORY_TOKEN</li>
<li>ARTIFACTORY_USERNAME</li>
<li>ARTIFACTORY_PASSWORD</li>
</ul>
If using a <b>Token</b> for authentication you do not need to set the username and password env-vars.
<br />
<br />
For more technical information on the available properties the <b>Latest Version</b> endpoint uses, and their potential drawbacks and limitations, please consult the following <a target="_blank" href="https://jfrog.com/help/r/jfrog-rest-apis/artifact-latest-version-search-based-on-layout">developer documentation here</a>.
</p>
`

export default class Artifactory extends BaseService {
static category = 'version'
static route = {
base: 'artifactory/v',
pattern: ':group/:artifact?',
queryParamSchema,
}

static examples = [
{
title: 'Artifactory Latest Version',
documentation,
namedParams: {
group: 'org.jfrog.artifactory.client',
artifact: 'artifactory-java-client-services',
},
queryParams: {
repos: 'libs-release-local',
remote: '1',
version: '1.0-SNAPSHOT',
server: 'https://my.artifactory.server/artifactory',
token: 'OTEfj876SDF2345JKIB/H&+FFGH',
username: 'jerry',
password: 'seinfeld',
},
staticPreview: Artifactory.render('2.1.0'),
Copy link
Author

Choose a reason for hiding this comment

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

No matter what I did I couldn't get any of this to show up in the local "integ test server". I assume I'm doing something wrong but filled out all the relevant links/options/docs/etc anyway if only for completeness sake.

},
]

static defaultBadgeData = { label: 'artifactory' }

static render(version) {
return renderVersionBadge({ version })
}

static orElseEnvVar(possibleValue, envVarName) {
if (possibleValue !== '') {
return possibleValue
} else {
const envValue = process.env[envVarName]
if (envValue && envValue !== 'undefined') {
return envValue
} else {
return ''
}
}
}

static getAuthorizationHeaderValue(
possibleToken,
possibleUsername,
possiblePassword,
) {
// check for token value first
const artToken = Artifactory.orElseEnvVar(
possibleToken,
'ARTIFACTORY_TOKEN',
)
if (artToken !== '') {
return `Bearer ${artToken}`
}

// if no token then check for username and password
const artUsername = Artifactory.orElseEnvVar(
possibleUsername,
'ARTIFACTORY_USERNAME',
)
if (artUsername !== '') {
const artPassword = Artifactory.orElseEnvVar(
possiblePassword,
'ARTIFACTORY_PASSWORD',
)
if (artUsername !== '') {
return `Basic ${btoa(`${artUsername}:${artPassword}`)}`
}
}

return ''
}

async fetchVersion({ requestParams }) {
// cache results so as not to overload
const result = await getCachedResource({
url: requestParams.artifactoryEndpoint,
ttl: ONE_HOUR,
json: false,
scraper: response => response,
options: requestParams.options,
})

// Request path here does not use caching but does
// allow us to catch specific errors and return
// more specific responses: can we do something
// similar when using the cachedResource function?
//
// const { res, buffer } = await this._request({
// url: requestParams.artifactoryEndpoint,
// options: requestParams.options,
// httpErrors: { 404: 'Not Found', 401: 'Bad Credentials' },
// })

return result
}

async handle(
{ group, artifact },
{ server, token, username, password, repos, remote, version },
) {
// Artifactory can return plain/text or json for this endpoint we're hitting so
// we need to effectively accept all as we're not sure what we're going to be returned
const defaultHeaders = { Accept: '*/*' }

// dump query params to debug console
console.debug(`***** artifactory found the following query properties *****
Copy link
Author

Choose a reason for hiding this comment

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

Don't know if you all care about keeping this in but it was generally useful for me when debugging to see what was coming through as values for the query parameters. Let me know if you want me to rip it out.


group=${group}
artifact=${artifact}
server=${server}
token=${token}
username=${username}
password=${password}
repos=${repos}
remote=${remote}
version=${version}
`)

// set and check Artifactory URL
const artifactoryUrl = Artifactory.orElseEnvVar(server, 'ARTIFACTORY_URL')
if (artifactoryUrl === '') {
const msg = 'No Artifactory URL found'
return this.constructor.render({ msg })
}
const artifactoryEndpoint = `${artifactoryUrl}/${ARTIFACTORY_LATEST_VERSION_ENDPOINT}`

// set and check authorization values
const authValue = Artifactory.getAuthorizationHeaderValue(
token,
username,
password,
)
if (authValue) {
defaultHeaders.Authorization = authValue
}

// set our request parameters to query Artifactory
const requestParams = {
artifactoryEndpoint,
options: {
headers: defaultHeaders,
searchParams: {
g: group,
a: artifact,
v: version,
remote,
repos,
},
},
}

const foundVersion = await this.fetchVersion({ requestParams })
return Artifactory.render(foundVersion)
}
}