* allow serviceData to override cacheSeconds with a longer value * prevent [endpoint] json cacheSeconds property exceeding service default * allow ShieldsRuntimeError to specify a cacheSeconds property By default error responses use the cacheLength of the service class throwing the error. This allows error to tell the handling layer the maxAge that should be set on the error badge response. * add customExceptions param This 1. allows us to specify custom properties to pass to the exception constructor if we throw any of the standard got errors e.g: `ETIMEDOUT`, `ECONNRESET`, etc 2. uses a custom `cacheSeconds` property (if set on the exception) to set the response maxAge * customExceptions --> systemErrors * errorMessages --> httpErrors
147 lines
4.2 KiB
JavaScript
147 lines
4.2 KiB
JavaScript
import Joi from 'joi'
|
|
import { nonNegativeInteger } from '../validators.js'
|
|
import { latest, renderVersionBadge } from '../version.js'
|
|
import { BaseJsonService, NotFound, InvalidResponse } from '../index.js'
|
|
import {
|
|
archSchema,
|
|
buildDockerUrl,
|
|
getDockerHubUser,
|
|
getMultiPageData,
|
|
getDigestSemVerMatches,
|
|
} from './docker-helpers.js'
|
|
|
|
const buildSchema = Joi.object({
|
|
count: nonNegativeInteger.required(),
|
|
results: Joi.array().items(
|
|
Joi.object({
|
|
name: Joi.string().required(),
|
|
images: Joi.array().items(
|
|
Joi.object({
|
|
digest: Joi.string(),
|
|
architecture: Joi.string().required(),
|
|
})
|
|
),
|
|
})
|
|
),
|
|
}).required()
|
|
|
|
const queryParamSchema = Joi.object({
|
|
sort: Joi.string().valid('date', 'semver').default('date'),
|
|
arch: archSchema.default('amd64'),
|
|
}).required()
|
|
|
|
export default class DockerVersion extends BaseJsonService {
|
|
static category = 'version'
|
|
static route = { ...buildDockerUrl('v', true), queryParamSchema }
|
|
static examples = [
|
|
{
|
|
title: 'Docker Image Version (latest by date)',
|
|
pattern: ':user/:repo',
|
|
namedParams: { user: '_', repo: 'alpine' },
|
|
queryParams: { sort: 'date', arch: 'amd64' },
|
|
staticPreview: this.render({ version: '3.9.5' }),
|
|
},
|
|
{
|
|
title: 'Docker Image Version (latest semver)',
|
|
pattern: ':user/:repo',
|
|
namedParams: { user: '_', repo: 'alpine' },
|
|
queryParams: { sort: 'semver' },
|
|
staticPreview: this.render({ version: '3.11.3' }),
|
|
},
|
|
{
|
|
title: 'Docker Image Version (tag latest semver)',
|
|
pattern: ':user/:repo/:tag',
|
|
namedParams: { user: '_', repo: 'alpine', tag: '3.6' },
|
|
staticPreview: this.render({ version: '3.6.5' }),
|
|
},
|
|
]
|
|
|
|
static defaultBadgeData = { label: 'version', color: 'blue' }
|
|
|
|
static render({ version }) {
|
|
return renderVersionBadge({ version })
|
|
}
|
|
|
|
async fetch({ user, repo, page }) {
|
|
page = page ? `&page=${page}` : ''
|
|
return this._requestJson({
|
|
schema: buildSchema,
|
|
url: `https://registry.hub.docker.com/v2/repositories/${getDockerHubUser(
|
|
user
|
|
)}/${repo}/tags?page_size=100&ordering=last_updated${page}`,
|
|
httpErrors: { 404: 'repository or tag not found' },
|
|
})
|
|
}
|
|
|
|
transform({ tag, sort, data, pagedData, arch = 'amd64' }) {
|
|
let version
|
|
|
|
if (!tag && sort === 'date') {
|
|
version = data.results[0].name
|
|
if (version !== 'latest') {
|
|
return { version }
|
|
}
|
|
const imageTag = data.results[0].images.find(i => i.architecture === arch) // Digest is the unique field that we utilise to match images
|
|
if (!imageTag) {
|
|
throw new InvalidResponse({
|
|
prettyMessage: 'digest not found for latest tag',
|
|
})
|
|
}
|
|
const { digest } = imageTag
|
|
return { version: getDigestSemVerMatches({ data: pagedData, digest }) }
|
|
} else if (!tag && sort === 'semver') {
|
|
const matches = data.map(d => d.name)
|
|
return { version: latest(matches) }
|
|
} else {
|
|
version = data.find(d => d.name === tag)
|
|
if (!version) {
|
|
throw new NotFound({ prettyMessage: 'tag not found' })
|
|
}
|
|
if (Object.keys(version.images).length === 0) {
|
|
return { version: version.name }
|
|
}
|
|
const image = version.images.find(i => i.architecture === arch)
|
|
if (!image) {
|
|
throw new InvalidResponse({
|
|
prettyMessage: 'digest not found for given tag',
|
|
})
|
|
}
|
|
const { digest } = image
|
|
return { version: getDigestSemVerMatches({ data, digest }) }
|
|
}
|
|
}
|
|
|
|
async handle({ user, repo, tag }, { sort, arch }) {
|
|
let data, pagedData
|
|
|
|
if (!tag && sort === 'date') {
|
|
data = await this.fetch({ user, repo })
|
|
if (data.count === 0) {
|
|
throw new NotFound({ prettyMessage: 'repository not found' })
|
|
}
|
|
if (data.results[0].name === 'latest') {
|
|
pagedData = await getMultiPageData({
|
|
user,
|
|
repo,
|
|
fetch: this.fetch.bind(this),
|
|
})
|
|
}
|
|
} else {
|
|
data = await getMultiPageData({
|
|
user,
|
|
repo,
|
|
fetch: this.fetch.bind(this),
|
|
})
|
|
}
|
|
|
|
const { version } = await this.transform({
|
|
tag,
|
|
sort,
|
|
data,
|
|
pagedData,
|
|
arch,
|
|
})
|
|
return this.constructor.render({ version })
|
|
}
|
|
}
|