From 7254b5f16c6fe4566a4d77d9fe461a7ad807545d Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Fri, 20 Jul 2018 09:09:01 +0200 Subject: [PATCH] feat: use generic lookup for docker (#2280) Removes custom Docker lookup code and instead integrates it with the generic lookup routine used by other package managers. Logic for digest support was added but is used by Docker-only for now. Closes #2081, Closes #2276 --- lib/datasource/docker.js | 21 +- lib/datasource/index.js | 18 ++ lib/manager/circleci/extract.js | 4 +- lib/manager/circleci/index.js | 2 - lib/manager/circleci/update.js | 7 +- lib/manager/docker-compose/extract.js | 4 +- lib/manager/docker-compose/index.js | 2 - lib/manager/docker-compose/update.js | 7 +- lib/manager/docker/extract.js | 19 ++ lib/manager/docker/index.js | 2 - lib/manager/docker/package.js | 176 ------------ lib/manager/docker/update.js | 26 +- lib/versioning/semver/index.js | 8 +- .../repository/process/lookup/index.js | 261 ++++++++++-------- .../__snapshots__/docker.spec.js.snap | 27 ++ test/datasource/docker.spec.js | 28 +- test/datasource/index.spec.js | 5 + .../__snapshots__/extract.spec.js.snap | 4 + test/manager/circleci/update.spec.js | 12 +- .../__snapshots__/extract.spec.js.snap | 7 + test/manager/docker-compose/update.spec.js | 10 +- .../docker/__snapshots__/extract.spec.js.snap | 41 +++ .../docker/__snapshots__/package.spec.js.snap | 159 ----------- .../docker/__snapshots__/update.spec.js.snap | 10 +- test/manager/docker/extract.spec.js | 8 + test/manager/docker/package.spec.js | 235 ---------------- test/manager/docker/update.spec.js | 34 +-- .../lookup/__snapshots__/index.spec.js.snap | 111 ++++++++ .../repository/process/lookup/index.spec.js | 102 +++++++ 29 files changed, 605 insertions(+), 745 deletions(-) delete mode 100644 lib/manager/docker/package.js create mode 100644 test/datasource/__snapshots__/docker.spec.js.snap delete mode 100644 test/manager/docker/__snapshots__/package.spec.js.snap delete mode 100644 test/manager/docker/package.spec.js diff --git a/lib/datasource/docker.js b/lib/datasource/docker.js index 0f652f7b93..a6324262e5 100644 --- a/lib/datasource/docker.js +++ b/lib/datasource/docker.js @@ -1,10 +1,10 @@ const got = require('got'); const parseLinkHeader = require('parse-link-header'); -const { isVersion } = require('../versioning/docker'); +const { isVersion, sortVersions } = require('../versioning/docker'); module.exports = { getDigest, - getTags, + getDependency, }; function massageRegistry(input) { @@ -117,10 +117,12 @@ async function getDigest(registry, name, tag = 'latest') { } } -async function getTags(registry, name, suffix) { - logger.debug(`getTags(${registry}, ${name}, ${suffix})`); +async function getDependency(purl) { + const { fullname, qualifiers } = purl; + const { registry, suffix } = qualifiers; + logger.debug({ fullname, registry, suffix }, 'docker.getDependencies()'); const massagedRegistry = massageRegistry(registry); - const repository = getRepository(name); + const repository = getRepository(fullname); try { let url = `${massagedRegistry}/v2/${repository}/tags/list?n=10000`; const headers = await getAuthHeaders(massagedRegistry, repository); @@ -138,15 +140,18 @@ async function getTags(registry, name, suffix) { } while (url && page < 20); logger.debug({ length: tags.length }, 'Got docker tags'); logger.trace({ tags }); - return tags + const releases = tags .filter(tag => !suffix || tag.endsWith(`-${suffix}`)) .map(tag => (suffix ? tag.replace(new RegExp(`-${suffix}$`), '') : tag)) - .filter(isVersion); + .filter(isVersion) + .sort(sortVersions) + .map(version => ({ version })); + return { releases }; } catch (err) /* istanbul ignore next */ { logger.debug( { err, - name, + fullname, message: err.message, body: err.response ? err.response.body : undefined, }, diff --git a/lib/datasource/index.js b/lib/datasource/index.js index dcf9abb3c3..0eeb0c7c79 100644 --- a/lib/datasource/index.js +++ b/lib/datasource/index.js @@ -1,5 +1,6 @@ const { parse } = require('../util/purl'); +const docker = require('./docker'); const github = require('./github'); const npm = require('./npm'); const nuget = require('./nuget'); @@ -7,6 +8,7 @@ const packagist = require('./packagist'); const pypi = require('./pypi'); const datasources = { + docker, github, npm, nuget, @@ -26,6 +28,22 @@ function getDependency(purlStr, config) { return datasources[purl.type].getDependency(purl, config); } +function supportsDigests(purlStr) { + const purl = parse(purlStr); + return !!datasources[purl.type].getDependency; +} + +function getDigest(purlStr, value) { + const purl = parse(purlStr); + return datasources[purl.type].getDigest( + purl.qualifiers.registry, + purl.fullname, + value + ); +} + module.exports = { getDependency, + supportsDigests, + getDigest, }; diff --git a/lib/manager/circleci/extract.js b/lib/manager/circleci/extract.js index 06306c9cd9..c57f8230b5 100644 --- a/lib/manager/circleci/extract.js +++ b/lib/manager/circleci/extract.js @@ -1,4 +1,4 @@ -const { splitImageParts } = require('../docker/extract'); +const { splitImageParts, getPurl } = require('../docker/extract'); module.exports = { extractDependencies, @@ -26,6 +26,7 @@ function extractDependencies(content) { { dockerRegistry, depName, currentTag, currentDigest }, 'CircleCI docker image' ); + const purl = getPurl(dockerRegistry, depName, tagSuffix); const dep = { lineNumber, currentFrom, @@ -38,6 +39,7 @@ function extractDependencies(content) { currentTag, currentValue, tagSuffix, + purl, versionScheme: 'docker', }; if (depName === 'node' || depName.endsWith('/node')) { diff --git a/lib/manager/circleci/index.js b/lib/manager/circleci/index.js index 293eec585b..5549e42b60 100644 --- a/lib/manager/circleci/index.js +++ b/lib/manager/circleci/index.js @@ -1,12 +1,10 @@ const { extractDependencies } = require('./extract'); -const { getPackageUpdates } = require('../docker/package'); const { updateDependency } = require('./update'); const language = 'docker'; module.exports = { extractDependencies, - getPackageUpdates, language, updateDependency, }; diff --git a/lib/manager/circleci/update.js b/lib/manager/circleci/update.js index ae29c6e6b0..a2eeefe5b1 100644 --- a/lib/manager/circleci/update.js +++ b/lib/manager/circleci/update.js @@ -1,10 +1,13 @@ +const { getNewFrom } = require('../docker/update'); + module.exports = { updateDependency, }; function updateDependency(fileContent, upgrade) { try { - logger.debug(`circleci.updateDependency(): ${upgrade.newFrom}`); + const newFrom = getNewFrom(upgrade); + logger.debug(`circleci.updateDependency(): ${newFrom}`); const lines = fileContent.split('\n'); const lineToChange = lines[upgrade.lineNumber]; const imageLine = new RegExp(/^(\s*- image:\s*'?"?)[^\s'"]+('?"?\s*)$/); @@ -12,7 +15,7 @@ function updateDependency(fileContent, upgrade) { logger.debug('No image line found'); return null; } - const newLine = lineToChange.replace(imageLine, `$1${upgrade.newFrom}$2`); + const newLine = lineToChange.replace(imageLine, `$1${newFrom}$2`); if (newLine === lineToChange) { logger.debug('No changes necessary'); return fileContent; diff --git a/lib/manager/docker-compose/extract.js b/lib/manager/docker-compose/extract.js index e933638d5e..91c1be9497 100644 --- a/lib/manager/docker-compose/extract.js +++ b/lib/manager/docker-compose/extract.js @@ -1,4 +1,4 @@ -const { splitImageParts } = require('../docker/extract'); +const { splitImageParts, getPurl } = require('../docker/extract'); module.exports = { extractDependencies, @@ -26,6 +26,7 @@ function extractDependencies(content) { { dockerRegistry, depName, currentTag, currentDigest }, 'Docker Compose image' ); + const purl = getPurl(dockerRegistry, depName, tagSuffix); const dep = { lineNumber, currentFrom, @@ -37,6 +38,7 @@ function extractDependencies(content) { currentTag, currentValue, tagSuffix, + purl, versionScheme: 'docker', }; if (depName === 'node' || depName.endsWith('/node')) { diff --git a/lib/manager/docker-compose/index.js b/lib/manager/docker-compose/index.js index 293eec585b..5549e42b60 100644 --- a/lib/manager/docker-compose/index.js +++ b/lib/manager/docker-compose/index.js @@ -1,12 +1,10 @@ const { extractDependencies } = require('./extract'); -const { getPackageUpdates } = require('../docker/package'); const { updateDependency } = require('./update'); const language = 'docker'; module.exports = { extractDependencies, - getPackageUpdates, language, updateDependency, }; diff --git a/lib/manager/docker-compose/update.js b/lib/manager/docker-compose/update.js index 424b060fa0..a5f303ab7a 100644 --- a/lib/manager/docker-compose/update.js +++ b/lib/manager/docker-compose/update.js @@ -1,10 +1,13 @@ +const { getNewFrom } = require('../docker/update'); + module.exports = { updateDependency, }; function updateDependency(fileContent, upgrade) { try { - logger.debug(`docker-compose.updateDependency(): ${upgrade.newFrom}`); + const newFrom = getNewFrom(upgrade); + logger.debug(`docker-compose.updateDependency(): ${newFrom}`); const lines = fileContent.split('\n'); const lineToChange = lines[upgrade.lineNumber]; const imageLine = new RegExp(/^(\s*image:\s*'?"?)[^\s'"]+('?"?\s*)$/); @@ -12,7 +15,7 @@ function updateDependency(fileContent, upgrade) { logger.debug('No image line found'); return null; } - const newLine = lineToChange.replace(imageLine, `$1${upgrade.newFrom}$2`); + const newLine = lineToChange.replace(imageLine, `$1${newFrom}$2`); if (newLine === lineToChange) { logger.debug('No changes necessary'); return fileContent; diff --git a/lib/manager/docker/extract.js b/lib/manager/docker/extract.js index d43fe9fef4..9c403e3979 100644 --- a/lib/manager/docker/extract.js +++ b/lib/manager/docker/extract.js @@ -1,5 +1,6 @@ module.exports = { splitImageParts, + getPurl, extractDependencies, }; @@ -31,6 +32,22 @@ function splitImageParts(currentFrom) { }; } +function getPurl(dockerRegistry, depName, tagSuffix) { + let purl = `pkg:docker/${depName}`; + if (dockerRegistry) { + purl += `?registry=${dockerRegistry}`; + } + if (tagSuffix) { + if (!purl.includes('?')) { + purl += '?'; + } else { + purl += '&'; + } + purl += `suffix=${tagSuffix}`; + } + return purl; +} + function extractDependencies(content) { const deps = []; const stageNames = []; @@ -61,6 +78,7 @@ function extractDependencies(content) { } else if (stageNames.includes(currentFrom)) { logger.debug({ currentFrom }, 'Skipping alias FROM'); } else { + const purl = getPurl(dockerRegistry, depName, tagSuffix); const dep = { language: 'docker', lineNumber, @@ -76,6 +94,7 @@ function extractDependencies(content) { currentTag, currentValue, tagSuffix, + purl, versionScheme: 'docker', }; if (depName === 'node' || depName.endsWith('/node')) { diff --git a/lib/manager/docker/index.js b/lib/manager/docker/index.js index 0e96876493..75e547b3e9 100644 --- a/lib/manager/docker/index.js +++ b/lib/manager/docker/index.js @@ -1,9 +1,7 @@ const { extractDependencies } = require('./extract'); -const { getPackageUpdates } = require('./package'); const { updateDependency } = require('./update'); module.exports = { extractDependencies, - getPackageUpdates, updateDependency, }; diff --git a/lib/manager/docker/package.js b/lib/manager/docker/package.js deleted file mode 100644 index 4479fede9e..0000000000 --- a/lib/manager/docker/package.js +++ /dev/null @@ -1,176 +0,0 @@ -const compareVersions = require('compare-versions'); -const versioning = require('../../versioning'); -const dockerApi = require('../../datasource/docker'); - -module.exports = { - isStable, - getPackageUpdates, -}; - -async function getPackageUpdates(config) { - const { - currentFrom, - dockerRegistry, - depName, - currentDepTag, - currentTag, - currentValue, - tagSuffix, - currentDigest, - unstablePattern, - ignoreUnstable, - } = config; - logger.debug(`getPackageUpdate(${currentFrom}`); - const { getMajor, isValid } = versioning('semver'); - const upgrades = []; - if (currentDigest || config.pinDigests) { - logger.debug('Checking docker pinDigests'); - const newDigest = await dockerApi.getDigest( - dockerRegistry, - depName, - currentTag - ); - if (!newDigest) { - logger.info( - { currentFrom, dockerRegistry, depName, currentTag }, - 'Dockerfile no digest' - ); - return []; - } - if (newDigest && config.currentDigest !== newDigest) { - const upgrade = {}; - upgrade.newTag = currentTag || 'latest'; - upgrade.newDigest = newDigest; - upgrade.newDigestShort = newDigest.slice(7, 13); - upgrade.newValue = upgrade.newDigestShort; - if (dockerRegistry) { - upgrade.newFrom = `${dockerRegistry}/`; - } else { - upgrade.newFrom = ''; - } - upgrade.newFrom += `${depName}:${upgrade.newTag}@${newDigest}`; - - if (currentDigest) { - upgrade.updateType = 'digest'; - } else { - upgrade.updateType = 'pin'; - } - upgrades.push(upgrade); - } - } - if (currentTag) { - if (!(currentValue && currentValue.length && isValid(currentValue))) { - logger.info( - { currentDepTag }, - 'Docker tag is not valid semver - skipping' - ); - return upgrades.map(upgrade => ({ ...upgrade, isRange: true })); - } - const currentMajor = getMajor(currentValue); - const currentlyStable = isStable(currentValue, unstablePattern); - let versionList = []; - const allTags = await dockerApi.getTags( - dockerRegistry, - config.depName, - tagSuffix - ); - if (allTags) { - versionList = allTags - .filter( - version => - // All stable are allowed - isStable(version, unstablePattern) || - // All unstable are allowed if we aren't ignoring them - !ignoreUnstable || - // Allow unstable of same major version - (!currentlyStable && getMajor(version) === currentMajor) - ) - .filter( - prefix => prefix.split('.').length === currentValue.split('.').length - ) - .filter(prefix => compareVersions(prefix, currentValue) > 0); - } - logger.trace({ versionList }, 'upgrades versionList'); - const versionUpgrades = {}; - for (const version of versionList) { - const newMajor = getMajor(version); - const updateType = newMajor > currentMajor ? 'major' : 'minor'; - let upgradeKey; - if ( - !config.separateMajorMinor || - config.groupName || - config.major.automerge === true - ) { - // If we're not separating releases then we use a common lookup key - upgradeKey = 'latest'; - } else if (!config.separateMultipleMajor && updateType === 'major') { - upgradeKey = 'major'; - } else { - // Use major version as lookup key - upgradeKey = newMajor; - } - if ( - !versionUpgrades[upgradeKey] || - compareVersions(version, versionUpgrades[upgradeKey]) > 0 - ) { - versionUpgrades[upgradeKey] = version; - } - } - logger.debug({ versionUpgrades }, 'Docker versionUpgrades'); - for (const upgradeKey of Object.keys(versionUpgrades)) { - const upgrade = {}; - const newVersion = versionUpgrades[upgradeKey]; - upgrade.newMajor = `${getMajor(newVersion)}`; - upgrade.newTag = tagSuffix ? `${newVersion}-${tagSuffix}` : newVersion; - upgrade.newValue = newVersion; - upgrade.newDepTag = `${config.depName}:${upgrade.newTag}`; - if (dockerRegistry) { - upgrade.newFrom = `${dockerRegistry}/`; - } else { - upgrade.newFrom = ''; - } - upgrade.newFrom += `${depName}:${upgrade.newTag}`; - if (config.currentDigest || config.pinDigests) { - upgrade.newDigest = await dockerApi.getDigest( - dockerRegistry, - config.depName, - upgrade.newTag - ); - // istanbul ignore else - if (upgrade.newDigest) { - upgrade.newFrom += `@${upgrade.newDigest}`; - } else { - logger.warn( - { dockerRegistry, depName, tag: upgrade.newTag }, - 'Dockerfile no digest' - ); - throw new Error('registry-failure'); - } - } - if (upgrade.newMajor > currentMajor) { - upgrade.updateType = 'major'; - } else { - upgrade.updateType = 'minor'; - } - upgrades.push(upgrade); - logger.info( - { currentDepTag, newDepTag: upgrade.newDepTag }, - 'Docker tag version upgrade found' - ); - } - } - if (upgrades.some(upgrade => upgrade.updateType === 'pin')) { - for (const upgrade of upgrades) { - if (upgrade.updateType !== 'pin') { - upgrade.blockedByPin = true; - } - } - } - return upgrades.filter(u => u.newDigest !== null); -} - -function isStable(tag, unstablePattern) { - return unstablePattern - ? tag.match(new RegExp(unstablePattern)) === null - : true; -} diff --git a/lib/manager/docker/update.js b/lib/manager/docker/update.js index 44968ea3a8..70dcad73ec 100644 --- a/lib/manager/docker/update.js +++ b/lib/manager/docker/update.js @@ -1,13 +1,31 @@ module.exports = { + getNewFrom, updateDependency, }; +function getNewFrom(upgrade) { + const { dockerRegistry, depName, newValue, tagSuffix, newDigest } = upgrade; + let newFrom = dockerRegistry ? `${dockerRegistry}/` : ''; + newFrom += `${depName}`; + if (newValue) { + newFrom += `:${newValue}`; + if (tagSuffix) { + newFrom += `-${tagSuffix}`; + } + } + if (newDigest) { + newFrom += `@${newDigest}`; + } + return newFrom; +} + function updateDependency(fileContent, upgrade) { - const { fromPrefix, newFrom, fromSuffix } = upgrade; try { - logger.debug(`docker.updateDependency(): ${upgrade.newFrom}`); + const { lineNumber, fromPrefix, fromSuffix } = upgrade; + const newFrom = getNewFrom(upgrade); + logger.debug(`docker.updateDependency(): ${newFrom}`); const lines = fileContent.split('\n'); - const lineToChange = lines[upgrade.lineNumber]; + const lineToChange = lines[lineNumber]; const imageLine = new RegExp(/^FROM /i); if (!lineToChange.match(imageLine)) { logger.debug('No image line found'); @@ -18,7 +36,7 @@ function updateDependency(fileContent, upgrade) { logger.debug('No changes necessary'); return fileContent; } - lines[upgrade.lineNumber] = newLine; + lines[lineNumber] = newLine; return lines.join('\n'); } catch (err) { logger.info({ err }, 'Error setting new Dockerfile value'); diff --git a/lib/versioning/semver/index.js b/lib/versioning/semver/index.js index f1d2774288..da8286a87c 100644 --- a/lib/versioning/semver/index.js +++ b/lib/versioning/semver/index.js @@ -8,6 +8,7 @@ const { compare: sortVersions, maxSatisfying: maxSatisfyingVersion, minSatisfying: minSatisfyingVersion, + major: getMajor, minor: getMinor, satisfies: matches, valid, @@ -17,13 +18,6 @@ const { eq: equals, } = semver; -const padRange = range => range + '.0'.repeat(3 - range.split('.').length); - -const getMajor = input => { - const version = isVersion(input) ? input : padRange(input); - return semver.major(version); -}; - // If this is left as an alias, inputs like "17.04.0" throw errors const isValid = input => validRange(input); const isVersion = input => valid(input); diff --git a/lib/workers/repository/process/lookup/index.js b/lib/workers/repository/process/lookup/index.js index f9146864ff..279bef3694 100644 --- a/lib/workers/repository/process/lookup/index.js +++ b/lib/workers/repository/process/lookup/index.js @@ -2,7 +2,11 @@ const versioning = require('../../../../versioning'); const { getRollbackUpdate } = require('./rollback'); const { getRangeStrategy } = require('../../../../manager'); const { filterVersions } = require('./filter'); -const { getDependency } = require('../../../../datasource'); +const { + getDependency, + supportsDigests, + getDigest, +} = require('../../../../datasource'); module.exports = { lookupUpdates, @@ -17,136 +21,177 @@ async function lookupUpdates(config) { getMinor, isGreaterThan, isSingleVersion, + isValid, isVersion, matches, getNewValue, } = versioning(config.versionScheme); const res = { updates: [] }; - const dependency = await getDependency(config.purl, config); - if (!dependency) { - // If dependency lookup fails then warn and return - const result = { - updateType: 'warning', - message: `Failed to look up dependency ${depName}`, - }; - logger.info( - { dependency: depName, packageFile: config.packageFile }, - result.message - ); - // TODO: return warnings in own field - res.updates.push(result); - return res; - } - // istanbul ignore if - if (dependency.deprecationMessage) { - logger.info('Setting deprecationMessage'); - res.deprecationMessage = dependency.deprecationMessage; - } - res.repositoryUrl = - dependency.repositoryUrl && dependency.repositoryUrl.length - ? dependency.repositoryUrl - : null; - const { releases } = dependency; - // Filter out any results from datasource that don't comply with our versioning scheme - const allVersions = releases - .map(release => release.version) - .filter(v => isVersion(v)); - // istanbul ignore if - if (allVersions.length === 0) { - const message = `No versions returned from registry for this package`; - logger.warn({ dependency: depName, result: dependency }, message); - // TODO: return an object - res.updates.push([ - { + if (isValid(currentValue)) { + const dependency = await getDependency(config.purl, config); + if (!dependency) { + // If dependency lookup fails then warn and return + const result = { updateType: 'warning', - message, - }, - ]); - return res; - } - // Check that existing constraint can be satisfied - const allSatisfyingVersions = allVersions.filter(version => - matches(version, currentValue) - ); - if (!allSatisfyingVersions.length) { - const rollback = getRollbackUpdate(config, allVersions); + message: `Failed to look up dependency ${depName}`, + }; + logger.info( + { dependency: depName, packageFile: config.packageFile }, + result.message + ); + // TODO: return warnings in own field + res.updates.push(result); + return res; + } + logger.debug({ dependency }); + // istanbul ignore if + if (dependency.deprecationMessage) { + logger.info('Setting deprecationMessage'); + res.deprecationMessage = dependency.deprecationMessage; + } + res.repositoryUrl = + dependency.repositoryUrl && dependency.repositoryUrl.length + ? dependency.repositoryUrl + : null; + const { releases } = dependency; + // Filter out any results from datasource that don't comply with our versioning scheme + const allVersions = releases + .map(release => release.version) + .filter(v => isVersion(v)); // istanbul ignore if - if (!rollback) { + if (allVersions.length === 0) { + const message = `No versions returned from registry for this package`; + logger.warn({ dependency: depName, result: dependency }, message); + // TODO: return an object res.updates.push([ { updateType: 'warning', - message: `Can't find version matching ${currentValue} for ${depName}`, + message, }, ]); return res; } - res.updates.push(rollback); - } - const rangeStrategy = getRangeStrategy(config); - const fromVersion = getFromVersion(config, rangeStrategy, allVersions); - if (rangeStrategy === 'pin' && !isSingleVersion(currentValue)) { - res.updates.push({ - updateType: 'pin', - isPin: true, - newValue: getNewValue( - currentValue, - rangeStrategy, - fromVersion, - fromVersion - ), - newMajor: getMajor(fromVersion), - }); - } - // Filter latest, unstable, etc - const filteredVersions = filterVersions( - config, - fromVersion, - dependency.latestVersion, - allVersions, - releases - ); - if (!filteredVersions.length) { - return res; - } - const buckets = {}; - for (const toVersion of filteredVersions) { - const update = { fromVersion, toVersion }; - update.newValue = getNewValue( - currentValue, - rangeStrategy, - fromVersion, - toVersion + // Check that existing constraint can be satisfied + const allSatisfyingVersions = allVersions.filter(version => + matches(version, currentValue) ); - if (!update.newValue || update.newValue === currentValue) { - continue; // eslint-disable-line no-continue + if (!allSatisfyingVersions.length) { + const rollback = getRollbackUpdate(config, allVersions); + // istanbul ignore if + if (!rollback) { + res.updates.push([ + { + updateType: 'warning', + message: `Can't find version matching ${currentValue} for ${depName}`, + }, + ]); + return res; + } + res.updates.push(rollback); } - update.newMajor = getMajor(toVersion); - update.newMinor = getMinor(toVersion); - update.updateType = getType(config, fromVersion, toVersion); - update.isSingleVersion = !!isSingleVersion(update.newValue); - if (!isVersion(update.newValue)) { - update.isRange = true; + const rangeStrategy = getRangeStrategy(config); + const fromVersion = getFromVersion(config, rangeStrategy, allVersions); + if (rangeStrategy === 'pin' && !isSingleVersion(currentValue)) { + res.updates.push({ + updateType: 'pin', + isPin: true, + newValue: getNewValue( + currentValue, + rangeStrategy, + fromVersion, + fromVersion + ), + newMajor: getMajor(fromVersion), + }); } - const updateRelease = releases.find(release => - equals(release.version, toVersion) + // Filter latest, unstable, etc + const filteredVersions = filterVersions( + config, + fromVersion, + dependency.latestVersion, + allVersions, + releases ); - update.releaseTimestamp = updateRelease.releaseTimestamp; - update.canBeUnpublished = updateRelease.canBeUnpublished; + if (!filteredVersions.length) { + return res; + } + const buckets = {}; + for (const toVersion of filteredVersions) { + const update = { fromVersion, toVersion }; + update.newValue = getNewValue( + currentValue, + rangeStrategy, + fromVersion, + toVersion + ); + if (!update.newValue || update.newValue === currentValue) { + continue; // eslint-disable-line no-continue + } + update.newMajor = getMajor(toVersion); + update.newMinor = getMinor(toVersion); + update.updateType = getType(config, fromVersion, toVersion); + update.isSingleVersion = !!isSingleVersion(update.newValue); + if (!isVersion(update.newValue)) { + update.isRange = true; + } + const updateRelease = releases.find(release => + equals(release.version, toVersion) + ); + update.releaseTimestamp = updateRelease.releaseTimestamp; + update.canBeUnpublished = updateRelease.canBeUnpublished; - const bucket = getBucket(config, update); - if (buckets[bucket]) { - if (isGreaterThan(update.toVersion, buckets[bucket].toVersion)) { + const bucket = getBucket(config, update); + if (buckets[bucket]) { + if (isGreaterThan(update.toVersion, buckets[bucket].toVersion)) { + buckets[bucket] = update; + } + } else { buckets[bucket] = update; } - } else { - buckets[bucket] = update; + } + res.updates = res.updates.concat(Object.values(buckets)); + res.releases = releases.filter( + release => + filteredVersions.includes(release.version) || + release.version === fromVersion + ); + } else { + logger.debug(`Dependency ${depName} has unsupported value ${currentValue}`); + if (!config.pinDigests && !config.currentDigest) { + res.skipReason = 'unsupported-value'; + } + } + // Add digests if necessary + if (supportsDigests(config.purl)) { + if (config.currentDigest) { + // digest update + res.updates.push({ + updateType: 'digest', + newValue: config.currentValue, + }); + } else if (config.pinDigests) { + // Create a pin only if one doesn't already exists + if (!res.updates.some(update => update.updateType === 'pin')) { + // pin digest + res.updates.push({ + updateType: 'pin', + newValue: config.currentValue, + }); + } + } + // update digest for all + for (const update of res.updates) { + if (config.pinDigests || config.currentDigest) { + update.newDigest = await getDigest(config.purl, update.newValue); + update.newDigestShort = update.newDigest.slice(7, 13); + } } } - res.updates = res.updates.concat(Object.values(buckets)); - res.releases = releases.filter( - release => - filteredVersions.includes(release.version) || - release.version === fromVersion + // Strip out any non-changed ones + res.updates = res.updates.filter( + update => + update.newValue !== config.currentValue || + update.newDigest !== config.currentDigest ); if (res.updates.some(update => update.updateType === 'pin')) { for (const update of res.updates) { diff --git a/test/datasource/__snapshots__/docker.spec.js.snap b/test/datasource/__snapshots__/docker.spec.js.snap new file mode 100644 index 0000000000..25c50f87f1 --- /dev/null +++ b/test/datasource/__snapshots__/docker.spec.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`api/docker getDependency returns tags with no suffix 1`] = ` +Object { + "releases": Array [ + Object { + "version": "1.0.0", + }, + Object { + "version": "1.1.0-alpine", + }, + Object { + "version": "1.1.0", + }, + ], +} +`; + +exports[`api/docker getDependency returns tags with suffix 1`] = ` +Object { + "releases": Array [ + Object { + "version": "1.1.0", + }, + ], +} +`; diff --git a/test/datasource/docker.spec.js b/test/datasource/docker.spec.js index ac96041d1e..1527f62f93 100644 --- a/test/datasource/docker.spec.js +++ b/test/datasource/docker.spec.js @@ -35,29 +35,43 @@ describe('api/docker', () => { expect(res).toBe('some-digest'); }); }); - describe('getTags', () => { + describe('getDependency', () => { it('returns null if no token', async () => { got.mockReturnValueOnce({ body: {} }); - const res = await docker.getTags(undefined, 'node'); + const res = await docker.getDependency({ + fullname: 'node', + qualifiers: {}, + }); expect(res).toBe(null); }); it('returns tags with no suffix', async () => { const tags = ['a', 'b', '1.0.0', '1.1.0', '1.1.0-alpine']; got.mockReturnValueOnce({ headers: {}, body: { token: 'some-token ' } }); got.mockReturnValueOnce({ headers: {}, body: { tags } }); - const res = await docker.getTags(undefined, 'my/node'); - expect(res).toEqual(['1.0.0', '1.1.0', '1.1.0-alpine']); + const res = await docker.getDependency({ + fullname: 'my/node', + qualifiers: {}, + }); + expect(res).toMatchSnapshot(); + expect(res.releases).toHaveLength(3); }); it('returns tags with suffix', async () => { const tags = ['a', 'b', '1.0.0', '1.1.0-alpine']; got.mockReturnValueOnce({ headers: {}, body: { token: 'some-token ' } }); got.mockReturnValueOnce({ headers: {}, body: { tags } }); - const res = await docker.getTags(undefined, 'my/node', 'alpine'); - expect(res).toEqual(['1.1.0']); + const res = await docker.getDependency({ + fullname: 'my/node', + qualifiers: { suffix: 'alpine' }, + }); + expect(res).toMatchSnapshot(); + expect(res.releases).toHaveLength(1); }); it('returns null on error', async () => { got.mockReturnValueOnce({}); - const res = await docker.getTags(undefined, 'node'); + const res = await docker.getDependency({ + fullname: 'my/node', + qualifiers: {}, + }); expect(res).toBe(null); }); }); diff --git a/test/datasource/index.spec.js b/test/datasource/index.spec.js index 86c46f7874..d560bd284b 100644 --- a/test/datasource/index.spec.js +++ b/test/datasource/index.spec.js @@ -1,7 +1,12 @@ const datasource = require('../../lib/datasource'); +jest.mock('../../lib/datasource/docker'); + describe('datasource/index', () => { it('returns null for invalid purl', async () => { expect(await datasource.getDependency('pkggithub/some/dep')).toBeNull(); }); + it('returns getDigest', async () => { + expect(await datasource.getDigest('pkg:docker/node')).toBeUndefined(); + }); }); diff --git a/test/manager/circleci/__snapshots__/extract.spec.js.snap b/test/manager/circleci/__snapshots__/extract.spec.js.snap index b001e9ceb2..ff66f0033f 100644 --- a/test/manager/circleci/__snapshots__/extract.spec.js.snap +++ b/test/manager/circleci/__snapshots__/extract.spec.js.snap @@ -14,6 +14,7 @@ Array [ "dockerRegistry": undefined, "fromVersion": "node", "lineNumber": 12, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -29,6 +30,7 @@ Array [ "dockerRegistry": undefined, "fromVersion": "node:4", "lineNumber": 57, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -44,6 +46,7 @@ Array [ "dockerRegistry": undefined, "fromVersion": "node:6", "lineNumber": 61, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -59,6 +62,7 @@ Array [ "dockerRegistry": undefined, "fromVersion": "node:8.9.0", "lineNumber": 65, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, diff --git a/test/manager/circleci/update.spec.js b/test/manager/circleci/update.spec.js index c394909ff3..1e3b08b66c 100644 --- a/test/manager/circleci/update.spec.js +++ b/test/manager/circleci/update.spec.js @@ -8,16 +8,18 @@ describe('manager/circleci/update', () => { it('replaces existing value', () => { const upgrade = { lineNumber: 65, - newFrom: 'node:8.10.0@sha256:abcdefghijklmnop', + depName: 'node', + newValue: '8.10.0', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dcUpdate.updateDependency(yamlFile, upgrade); expect(res).not.toEqual(yamlFile); - expect(res.includes(upgrade.newFrom)).toBe(true); + expect(res.includes(upgrade.newDigest)).toBe(true); }); it('returns same', () => { const upgrade = { lineNumber: 12, - newFrom: 'node', + depName: 'node', }; const res = dcUpdate.updateDependency(yamlFile, upgrade); expect(res).toEqual(yamlFile); @@ -25,7 +27,9 @@ describe('manager/circleci/update', () => { it('returns null if mismatch', () => { const upgrade = { lineNumber: 17, - newFrom: 'postgres:9.6.8@sha256:abcdefghijklmnop', + depName: 'postgres', + newValue: '9.6.8', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dcUpdate.updateDependency(yamlFile, upgrade); expect(res).toBe(null); diff --git a/test/manager/docker-compose/__snapshots__/extract.spec.js.snap b/test/manager/docker-compose/__snapshots__/extract.spec.js.snap index f3ceef49cc..1b7c71532f 100644 --- a/test/manager/docker-compose/__snapshots__/extract.spec.js.snap +++ b/test/manager/docker-compose/__snapshots__/extract.spec.js.snap @@ -12,6 +12,7 @@ Array [ "depName": "something/redis", "dockerRegistry": "quay.io", "lineNumber": 4, + "purl": "pkg:docker/something/redis?registry=quay.io", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -26,6 +27,7 @@ Array [ "depName": "node", "dockerRegistry": undefined, "lineNumber": 18, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -39,6 +41,7 @@ Array [ "depName": "postgres", "dockerRegistry": undefined, "lineNumber": 21, + "purl": "pkg:docker/postgres", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -52,6 +55,7 @@ Array [ "depName": "dockersamples/examplevotingapp_vote", "dockerRegistry": undefined, "lineNumber": 31, + "purl": "pkg:docker/dockersamples/examplevotingapp_vote", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -65,6 +69,7 @@ Array [ "depName": "dockersamples/examplevotingapp_result", "dockerRegistry": undefined, "lineNumber": 46, + "purl": "pkg:docker/dockersamples/examplevotingapp_result", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -78,6 +83,7 @@ Array [ "depName": "dockersamples/examplevotingapp_worker", "dockerRegistry": undefined, "lineNumber": 62, + "purl": "pkg:docker/dockersamples/examplevotingapp_worker", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -91,6 +97,7 @@ Array [ "depName": "dockersamples/visualizer", "dockerRegistry": undefined, "lineNumber": 79, + "purl": "pkg:docker/dockersamples/visualizer", "tagSuffix": undefined, "versionScheme": "docker", }, diff --git a/test/manager/docker-compose/update.spec.js b/test/manager/docker-compose/update.spec.js index 9337c55ed1..722abdcf4f 100644 --- a/test/manager/docker-compose/update.spec.js +++ b/test/manager/docker-compose/update.spec.js @@ -11,16 +11,20 @@ describe('manager/docker-compose/update', () => { it('replaces existing value', () => { const upgrade = { lineNumber: 18, - newFrom: 'postgres:9.6.8@sha256:abcdefghijklmnop', + depName: 'postgres', + newValue: '9.6.8', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dcUpdate.updateDependency(yamlFile, upgrade); expect(res).not.toEqual(yamlFile); - expect(res.includes(upgrade.newFrom)).toBe(true); + expect(res.includes(upgrade.newDigest)).toBe(true); }); it('returns same', () => { const upgrade = { lineNumber: 4, - newFrom: 'quay.io/something/redis:alpine', + dockerRegistry: 'quay.io', + depName: 'something/redis', + newValue: 'alpine', }; const res = dcUpdate.updateDependency(yamlFile, upgrade); expect(res).toEqual(yamlFile); diff --git a/test/manager/docker/__snapshots__/extract.spec.js.snap b/test/manager/docker/__snapshots__/extract.spec.js.snap index 1419d28190..745adbf0ba 100644 --- a/test/manager/docker/__snapshots__/extract.spec.js.snap +++ b/test/manager/docker/__snapshots__/extract.spec.js.snap @@ -17,6 +17,7 @@ Array [ "fromSuffix": "AS node", "language": "docker", "lineNumber": 2, + "purl": "pkg:docker/node?suffix=alpine", "tagSuffix": "alpine", "versionScheme": "docker", }, @@ -34,6 +35,7 @@ Array [ "fromSuffix": "AS puppeteer", "language": "docker", "lineNumber": 3, + "purl": "pkg:docker/buildkite/puppeteer", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -57,6 +59,7 @@ Array [ "fromSuffix": "as frontend", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -74,6 +77,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 4, + "purl": "pkg:docker/python?suffix=slim", "tagSuffix": "slim", "versionScheme": "docker", }, @@ -97,6 +101,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node?registry=registry.allmine.info:5005", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -120,6 +125,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 3, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -143,12 +149,37 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node?registry=registry2.something.info", "tagSuffix": undefined, "versionScheme": "docker", }, ] `; +exports[`lib/manager/docker/extract extractDependencies() handles custom hosts and suffix 1`] = ` +Array [ + Object { + "commitMessageTopic": "Node.js", + "currentDepTag": "node:8-alpine", + "currentDepTagDigest": "node:8-alpine", + "currentDigest": undefined, + "currentFrom": "registry2.something.info/node:8-alpine", + "currentTag": "8-alpine", + "currentValue": "8", + "depName": "node", + "dockerRegistry": "registry2.something.info", + "fromLine": "FROM registry2.something.info/node:8-alpine", + "fromPrefix": "FROM", + "fromSuffix": "", + "language": "docker", + "lineNumber": 0, + "purl": "pkg:docker/node?registry=registry2.something.info&suffix=alpine", + "tagSuffix": "alpine", + "versionScheme": "docker", + }, +] +`; + exports[`lib/manager/docker/extract extractDependencies() handles custom hosts with namespace 1`] = ` Array [ Object { @@ -166,6 +197,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/someaccount/node?registry=registry2.something.info", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -189,6 +221,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node?registry=registry2.something.info:5005", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -212,6 +245,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -235,6 +269,7 @@ Array [ "fromSuffix": "as base", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node?suffix=alpine", "tagSuffix": "alpine", "versionScheme": "docker", }, @@ -258,6 +293,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -281,6 +317,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/mynamespace/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -304,6 +341,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node?suffix=alpine", "tagSuffix": "alpine", "versionScheme": "docker", }, @@ -327,6 +365,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -350,6 +389,7 @@ Array [ "fromSuffix": "", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, @@ -373,6 +413,7 @@ Array [ "fromSuffix": "as frontend", "language": "docker", "lineNumber": 0, + "purl": "pkg:docker/node", "tagSuffix": undefined, "versionScheme": "docker", }, diff --git a/test/manager/docker/__snapshots__/package.spec.js.snap b/test/manager/docker/__snapshots__/package.spec.js.snap deleted file mode 100644 index 0a7e53eb0d..0000000000 --- a/test/manager/docker/__snapshots__/package.spec.js.snap +++ /dev/null @@ -1,159 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`lib/manager/docker/package getPackageUpdates adds digest 1`] = ` -Array [ - Object { - "newDigest": "sha256:one", - "newDigestShort": "one", - "newFrom": "some-dep:1.0.0-something@sha256:one", - "newTag": "1.0.0-something", - "newValue": "one", - "updateType": "pin", - }, - Object { - "blockedByPin": true, - "newDepTag": "some-dep:1.1.0-something", - "newDigest": "sha256:two", - "newFrom": "some-dep:1.1.0-something@sha256:two", - "newMajor": "1", - "newTag": "1.1.0-something", - "newValue": "1.1.0", - "updateType": "minor", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates ignores unstable upgrades 1`] = ` -Array [ - Object { - "newDepTag": "node:8", - "newFrom": "node:8", - "newMajor": "8", - "newTag": "8", - "newValue": "8", - "updateType": "major", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates returns a digest when registry is present 1`] = ` -Array [ - Object { - "newDigest": "sha256:1234567890", - "newDigestShort": "123456", - "newFrom": "docker.io/some-dep:1.0.0@sha256:1234567890", - "newTag": "1.0.0", - "newValue": "123456", - "updateType": "digest", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates returns major and minor upgrades 1`] = ` -Array [ - Object { - "newDepTag": "some-dep:1.2.0", - "newDigest": "sha256:one", - "newFrom": "some-dep:1.2.0@sha256:one", - "newMajor": "1", - "newTag": "1.2.0", - "newValue": "1.2.0", - "updateType": "minor", - }, - Object { - "newDepTag": "some-dep:2.0.0", - "newDigest": "sha256:two", - "newFrom": "some-dep:2.0.0@sha256:two", - "newMajor": "2", - "newTag": "2.0.0", - "newValue": "2.0.0", - "updateType": "major", - }, - Object { - "newDepTag": "some-dep:3.0.0", - "newDigest": "sha256:three", - "newFrom": "some-dep:3.0.0@sha256:three", - "newMajor": "3", - "newTag": "3.0.0", - "newValue": "3.0.0", - "updateType": "major", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates returns only one major 1`] = ` -Array [ - Object { - "newDepTag": "some-dep:1.2.0", - "newDigest": "sha256:one", - "newFrom": "some-dep:1.2.0@sha256:one", - "newMajor": "1", - "newTag": "1.2.0", - "newValue": "1.2.0", - "updateType": "minor", - }, - Object { - "newDepTag": "some-dep:3.0.0", - "newDigest": "sha256:two", - "newFrom": "some-dep:3.0.0@sha256:two", - "newMajor": "3", - "newTag": "3.0.0", - "newValue": "3.0.0", - "updateType": "major", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates returns only one upgrade 1`] = ` -Array [ - Object { - "newDepTag": "some-dep:3.0.0", - "newDigest": "sha256:one", - "newFrom": "some-dep:3.0.0@sha256:one", - "newMajor": "3", - "newTag": "3.0.0", - "newValue": "3.0.0", - "updateType": "major", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates returns only one upgrade if automerging major 1`] = ` -Array [ - Object { - "newDepTag": "some-dep:3.0.0", - "newDigest": "sha256:one", - "newFrom": "docker.io/some-dep:3.0.0@sha256:one", - "newMajor": "3", - "newTag": "3.0.0", - "newValue": "3.0.0", - "updateType": "major", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates upgrades from unstable to stable 1`] = ` -Array [ - Object { - "newDepTag": "node:8", - "newFrom": "node:8", - "newMajor": "8", - "newTag": "8", - "newValue": "8", - "updateType": "major", - }, -] -`; - -exports[`lib/manager/docker/package getPackageUpdates upgrades from unstable to unstable if not ignoring 1`] = ` -Array [ - Object { - "newDepTag": "node:9", - "newFrom": "node:9", - "newMajor": "9", - "newTag": "9", - "newValue": "9", - "updateType": "major", - }, -] -`; diff --git a/test/manager/docker/__snapshots__/update.spec.js.snap b/test/manager/docker/__snapshots__/update.spec.js.snap index d8d876f50d..de3f74481d 100644 --- a/test/manager/docker/__snapshots__/update.spec.js.snap +++ b/test/manager/docker/__snapshots__/update.spec.js.snap @@ -1,27 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`workers/branch/dockerfile updateDependency handles similar FROM 1`] = ` +exports[`manager/docker/update updateDependency handles similar FROM 1`] = ` "FROM debian:wheezy@sha256:abcdefghijklmnop as stage-1 RUN something FROM debian:wheezy@sha256:abcdefghijklmnop RUN something else" `; -exports[`workers/branch/dockerfile updateDependency handles strange whitespace 1`] = ` +exports[`manager/docker/update updateDependency handles strange whitespace 1`] = ` "# comment FROM node:8 FROM node:8@sha256:abcdefghijklmnop as base RUN something " `; -exports[`workers/branch/dockerfile updateDependency replaces existing value 1`] = ` +exports[`manager/docker/update updateDependency replaces existing value 1`] = ` "# comment FROM node:8 -FROM node:8@sha256:abcdefghijklmnop +FROM node:8-alpine@sha256:abcdefghijklmnop RUN something " `; -exports[`workers/branch/dockerfile updateDependency replaces existing value with suffix 1`] = ` +exports[`manager/docker/update updateDependency replaces existing value with suffix 1`] = ` "# comment FROM node:8 FROM node:8@sha256:abcdefghijklmnop as base RUN something diff --git a/test/manager/docker/extract.spec.js b/test/manager/docker/extract.spec.js index 72c2d4bf5a..40226fd05b 100644 --- a/test/manager/docker/extract.spec.js +++ b/test/manager/docker/extract.spec.js @@ -62,6 +62,14 @@ describe('lib/manager/docker/extract', () => { expect(res).toMatchSnapshot(); expect(res[0].dockerRegistry).toEqual('registry2.something.info'); }); + it('handles custom hosts and suffix', () => { + const res = extractDependencies( + 'FROM registry2.something.info/node:8-alpine\n', + config + ).deps; + expect(res).toMatchSnapshot(); + expect(res[0].dockerRegistry).toEqual('registry2.something.info'); + }); it('handles custom hosts with port', () => { const res = extractDependencies( 'FROM registry2.something.info:5005/node:8\n', diff --git a/test/manager/docker/package.spec.js b/test/manager/docker/package.spec.js deleted file mode 100644 index d8e6d45996..0000000000 --- a/test/manager/docker/package.spec.js +++ /dev/null @@ -1,235 +0,0 @@ -const dockerApi = require('../../../lib/datasource/docker'); -const docker = require('../../../lib/manager/docker/package'); -const defaultConfig = require('../../../lib/config/defaults').getConfig(); - -// jest.mock('../../../lib/manager/docker/registry'); -dockerApi.getDigest = jest.fn(); -dockerApi.getTags = jest.fn(); - -describe('lib/manager/docker/package', () => { - describe('isStable', () => { - it('returns true if no pattern', () => { - expect(docker.isStable('8', null)).toBe(true); - }); - it('returns true if no match', () => { - const unstablePattern = '^\\d*[13579]($|.)'; - expect(docker.isStable('8', unstablePattern)).toBe(true); - expect(docker.isStable('8.9.1', unstablePattern)).toBe(true); - }); - it('returns false if match', () => { - const unstablePattern = '^\\d*[13579]($|.)'; - expect(docker.isStable('9.0', unstablePattern)).toBe(false); - expect(docker.isStable('15.04', unstablePattern)).toBe(false); - }); - }); - describe('getPackageUpdates', () => { - let config; - beforeEach(() => { - config = { - ...defaultConfig, - depName: 'some-dep', - currentFrom: 'some-dep:1.0.0@sha256:abcdefghijklmnop', - currentDepTag: 'some-dep:1.0.0', - currentTag: '1.0.0', - currentValue: '1.0.0', - currentDigest: 'sha256:abcdefghijklmnop', - pinDigests: true, - }; - }); - it('returns empty if no digest', async () => { - expect(await docker.getPackageUpdates(config)).toEqual([]); - }); - it('returns empty if digest is same', async () => { - dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); - expect(await docker.getPackageUpdates(config)).toEqual([]); - }); - it('returns a digest', async () => { - dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); - const res = await docker.getPackageUpdates(config); - expect(res).toHaveLength(1); - expect(res[0].updateType).toEqual('digest'); - }); - it('returns a digest when registry is present', async () => { - config.dockerRegistry = 'docker.io'; - config.currentFrom = 'docker.io/some-dep:1.0.0@sha256:abcdefghijklmnop'; - dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(1); - expect(res[0].updateType).toEqual('digest'); - }); - it('adds latest tag', async () => { - delete config.currentTag; - delete config.currentValue; - dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); - const res = await docker.getPackageUpdates(config); - expect(res).toHaveLength(1); - expect(res[0].updateType).toEqual('digest'); - }); - it('returns a pin', async () => { - delete config.currentDigest; - config.currentTag = 'some-text-tag'; - config.currentValue = 'some'; - config.tagSuffix = 'text-tag'; - dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); - const res = await docker.getPackageUpdates(config); - expect(res).toHaveLength(1); - expect(res[0].updateType).toEqual('pin'); - }); - it('returns empty if current tag is not valid version', async () => { - config.currentTag = 'some-text-tag'; - config.currentValue = 'some'; - config.tagSuffix = 'text-tag'; - dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); - expect(await docker.getPackageUpdates(config)).toEqual([]); - }); - it('returns only one upgrade if automerging major', async () => { - config.dockerRegistry = 'docker.io'; - dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); - dockerApi.getDigest.mockReturnValueOnce('sha256:one'); - dockerApi.getTags.mockReturnValueOnce([ - '1.1.0', - '1.2.0', - '2.0.0', - '3.0.0', - ]); - config.major.automerge = true; - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(1); - expect(res[0].newMajor).toEqual('3'); - config.major.automerge = false; - }); - it('returns major and minor upgrades', async () => { - config.separateMultipleMajor = true; - dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); - dockerApi.getDigest.mockReturnValueOnce('sha256:one'); - dockerApi.getDigest.mockReturnValueOnce('sha256:two'); - dockerApi.getDigest.mockReturnValueOnce('sha256:three'); - dockerApi.getTags.mockReturnValueOnce([ - '1.1.0', - '1.2.0', - '2.0.0', - '3.0.0', - ]); - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(3); - expect(res[0].updateType).toEqual('minor'); - expect(res[0].newValue).toEqual('1.2.0'); - expect(res[1].updateType).toEqual('major'); - expect(res[2].newMajor).toEqual('3'); - }); - it('returns only one major', async () => { - dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); - dockerApi.getDigest.mockReturnValueOnce('sha256:one'); - dockerApi.getDigest.mockReturnValueOnce('sha256:two'); - dockerApi.getTags.mockReturnValueOnce([ - '1.1.0', - '1.2.0', - '2.0.0', - '3.0.0', - ]); - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(2); - expect(res[0].updateType).toEqual('minor'); - expect(res[0].newValue).toEqual('1.2.0'); - expect(res[1].updateType).toEqual('major'); - expect(res[1].newMajor).toEqual('3'); - }); - it('returns only one upgrade', async () => { - dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); - dockerApi.getDigest.mockReturnValueOnce('sha256:one'); - dockerApi.getTags.mockReturnValueOnce([ - '1.1.0', - '1.2.0', - '2.0.0', - '3.0.0', - ]); - config.major.automerge = true; - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(1); - expect(res[0].updateType).toEqual('major'); - expect(res[0].newMajor).toEqual('3'); - }); - it('ignores unstable upgrades', async () => { - config = { - ...defaultConfig, - depName: 'node', - currentFrom: 'node:6', - currentDepTag: 'node:6', - currentTag: '6', - currentValue: '6', - currentDigest: undefined, - pinDigests: false, - unstablePattern: '^\\d*[13579]($|.)', - }; - dockerApi.getTags.mockReturnValueOnce(['4', '6', '6.1', '7', '8', '9']); - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(1); - expect(res[0].updateType).toEqual('major'); - expect(res[0].newValue).toEqual('8'); - }); - it('upgrades from unstable to stable', async () => { - config = { - ...defaultConfig, - depName: 'node', - currentFrom: 'node:7', - currentDepTag: 'node:7', - currentTag: '7', - currentValue: '7', - currentDigest: undefined, - pinDigests: false, - unstablePattern: '^\\d*[13579]($|.)', - }; - dockerApi.getTags.mockReturnValueOnce(['4', '6', '6.1', '7', '8', '9']); - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(1); - expect(res[0].updateType).toEqual('major'); - expect(res[0].newValue).toEqual('8'); - }); - it('upgrades from unstable to unstable if not ignoring', async () => { - config = { - ...defaultConfig, - depName: 'node', - currentFrom: 'node:7', - currentDepTag: 'node:7', - currentTag: '7', - currentValue: '7', - currentDigest: undefined, - pinDigests: false, - unstablePattern: '^\\d*[13579]($|.)', - ignoreUnstable: false, - }; - dockerApi.getTags.mockReturnValueOnce(['4', '6', '6.1', '7', '8', '9']); - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(1); - expect(res[0].newMajor).toEqual('9'); - }); - it('adds digest', async () => { - delete config.currentDigest; - config.currentTag = '1.0.0-something'; - config.currentValue = '1.0.0'; - config.tagSuffix = 'something'; - dockerApi.getDigest.mockReturnValueOnce('sha256:one'); - dockerApi.getDigest.mockReturnValueOnce('sha256:two'); - dockerApi.getTags.mockReturnValueOnce(['1.1.0']); - const res = await docker.getPackageUpdates(config); - expect(res).toMatchSnapshot(); - expect(res).toHaveLength(2); - expect(res[1].updateType).toEqual('minor'); - expect(res[1].newValue).toEqual('1.1.0'); - }); - it('ignores deps with custom registry', async () => { - delete config.currentDigest; - config.dockerRegistry = 'registry.something.info:5005'; - const res = await docker.getPackageUpdates(config); - expect(res).toHaveLength(0); - }); - }); -}); diff --git a/test/manager/docker/update.spec.js b/test/manager/docker/update.spec.js index 2db1099da6..4ff90fb7c5 100644 --- a/test/manager/docker/update.spec.js +++ b/test/manager/docker/update.spec.js @@ -1,16 +1,17 @@ const dockerfile = require('../../../lib/manager/docker/update'); -describe('workers/branch/dockerfile', () => { +describe('manager/docker/update', () => { describe('updateDependency', () => { it('replaces existing value', () => { const fileContent = '# comment FROM node:8\nFROM node:8\nRUN something\n'; const upgrade = { lineNumber: 1, depName: 'node', - currentValue: 'node:8', + newValue: '8', fromPrefix: 'FROM', fromSuffix: '', - newFrom: 'node:8@sha256:abcdefghijklmnop', + tagSuffix: 'alpine', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dockerfile.updateDependency(fileContent, upgrade); expect(res).toMatchSnapshot(); @@ -21,10 +22,10 @@ describe('workers/branch/dockerfile', () => { const upgrade = { lineNumber: 1, depName: 'node', - currentValue: 'node:8', + newValue: '8', fromPrefix: 'FROM', fromSuffix: 'as base', - newFrom: 'node:8@sha256:abcdefghijklmnop', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dockerfile.updateDependency(fileContent, upgrade); expect(res).toMatchSnapshot(); @@ -35,10 +36,10 @@ describe('workers/branch/dockerfile', () => { const upgrade = { lineNumber: 1, depName: 'node', - currentValue: 'node:8', + newValue: '8', fromPrefix: 'FROM', fromSuffix: 'as base', - newFrom: 'node:8@sha256:abcdefghijklmnop', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dockerfile.updateDependency(fileContent, upgrade); expect(res).toMatchSnapshot(); @@ -49,10 +50,10 @@ describe('workers/branch/dockerfile', () => { const upgrade = { lineNumber: 0, depName: 'node', - currentValue: 'node:8', + newValue: '8', fromPrefix: 'FROM', fromSuffix: '', - newFrom: 'node:8@sha256:abcdefghijklmnop', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dockerfile.updateDependency(fileContent, upgrade); expect(res).toBe(null); @@ -63,10 +64,9 @@ describe('workers/branch/dockerfile', () => { const upgrade = { lineNumber: 1, depName: 'node', - currentValue: 'node:8', + newValue: '8', fromPrefix: 'FROM', fromSuffix: 'as base', - newFrom: 'node:8', }; const res = dockerfile.updateDependency(fileContent, upgrade); expect(res).toBe(fileContent); @@ -76,10 +76,10 @@ describe('workers/branch/dockerfile', () => { const upgrade = { lineNumber: 1, depName: 'node', - currentValue: 'node:8', + newValue: '8', fromPrefix: 'FROM', fromSuffix: '', - newFrom: 'node:8@sha256:abcdefghijklmnop', + newDigest: 'sha256:abcdefghijklmnop', }; const res = dockerfile.updateDependency(fileContent, upgrade); expect(res).toBe(null); @@ -90,18 +90,18 @@ describe('workers/branch/dockerfile', () => { const upgrade1 = { lineNumber: 0, depName: 'debian', - currentValue: 'debian:wheezy', + newValue: 'wheezy', fromPrefix: 'FROM', fromSuffix: 'as stage-1', - newFrom: 'debian:wheezy@sha256:abcdefghijklmnop', + newDigest: 'sha256:abcdefghijklmnop', }; const upgrade2 = { lineNumber: 2, depName: 'debian', - currentValue: 'debian:wheezy', + newValue: 'wheezy', fromPrefix: 'FROM', fromSuffix: '', - newFrom: 'debian:wheezy@sha256:abcdefghijklmnop', + newDigest: 'sha256:abcdefghijklmnop', }; let res = dockerfile.updateDependency(fileContent, upgrade1); res = dockerfile.updateDependency(res, upgrade2); diff --git a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap index a1b40de861..722dfa265b 100644 --- a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap +++ b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap @@ -76,6 +76,103 @@ Array [ ] `; +exports[`manager/npm/lookup .lookupUpdates() handles digest pin 1`] = ` +Object { + "releases": Array [ + Object { + "version": "8.0.0", + }, + Object { + "version": "8.1.0", + }, + ], + "repositoryUrl": null, + "updates": Array [ + Object { + "blockedByPin": true, + "canBeUnpublished": undefined, + "fromVersion": "8.0.0", + "isSingleVersion": true, + "newDigest": "sha256:aaaaaaaaaaaaaaaa", + "newDigestShort": "aaaaaa", + "newMajor": 8, + "newMinor": 1, + "newValue": "8.1.0", + "releaseTimestamp": undefined, + "toVersion": "8.1.0", + "updateType": "minor", + }, + Object { + "newDigest": "sha256:bbbbbbbbbbbbbbbb", + "newDigestShort": "bbbbbb", + "newValue": "8.0.0", + "updateType": "pin", + }, + ], +} +`; + +exports[`manager/npm/lookup .lookupUpdates() handles digest pin for non-version 1`] = ` +Object { + "updates": Array [ + Object { + "newDigest": "sha256:aaaaaaaaaaaaaaaa", + "newDigestShort": "aaaaaa", + "newValue": "alpine", + "updateType": "pin", + }, + ], +} +`; + +exports[`manager/npm/lookup .lookupUpdates() handles digest update 1`] = ` +Object { + "releases": Array [ + Object { + "version": "8.0.0", + }, + Object { + "version": "8.1.0", + }, + ], + "repositoryUrl": null, + "updates": Array [ + Object { + "canBeUnpublished": undefined, + "fromVersion": "8.0.0", + "isSingleVersion": true, + "newDigest": "sha256:aaaaaaaaaaaaaaaa", + "newDigestShort": "aaaaaa", + "newMajor": 8, + "newMinor": 1, + "newValue": "8.1.0", + "releaseTimestamp": undefined, + "toVersion": "8.1.0", + "updateType": "minor", + }, + Object { + "newDigest": "sha256:bbbbbbbbbbbbbbbb", + "newDigestShort": "bbbbbb", + "newValue": "8.0.0", + "updateType": "digest", + }, + ], +} +`; + +exports[`manager/npm/lookup .lookupUpdates() handles digest update for non-version 1`] = ` +Object { + "updates": Array [ + Object { + "newDigest": "sha256:aaaaaaaaaaaaaaaa", + "newDigestShort": "aaaaaa", + "newValue": "alpine", + "updateType": "digest", + }, + ], +} +`; + exports[`manager/npm/lookup .lookupUpdates() handles github 404 1`] = ` Array [ Object { @@ -641,6 +738,20 @@ Array [ ] `; +exports[`manager/npm/lookup .lookupUpdates() skips undefined values 1`] = ` +Object { + "skipReason": "unsupported-value", + "updates": Array [], +} +`; + +exports[`manager/npm/lookup .lookupUpdates() skips unsupported values 1`] = ` +Object { + "skipReason": "unsupported-value", + "updates": Array [], +} +`; + exports[`manager/npm/lookup .lookupUpdates() supports > latest versions if configured 1`] = ` Array [ Object { diff --git a/test/workers/repository/process/lookup/index.spec.js b/test/workers/repository/process/lookup/index.spec.js index 8fd4807ae6..5738e1abe0 100644 --- a/test/workers/repository/process/lookup/index.spec.js +++ b/test/workers/repository/process/lookup/index.spec.js @@ -7,6 +7,9 @@ const webpackJson = require('../../../../_fixtures/npm/webpack.json'); const nextJson = require('../../../../_fixtures/npm/next.json'); const vueJson = require('../../../../_fixtures/npm/vue.json'); const typescriptJson = require('../../../../_fixtures/npm/typescript.json'); +const docker = require('../../../../../lib/datasource/docker'); + +jest.mock('../../../../../lib/datasource/docker'); qJson.latestVersion = '1.4.1'; @@ -873,5 +876,104 @@ describe('manager/npm/lookup', () => { expect(res.releases).toHaveLength(2); expect(res.updates[0].toVersion).toEqual('1.4.0'); }); + it('skips unsupported values', async () => { + config.currentValue = 'alpine'; + config.depName = 'node'; + config.purl = 'pkg:docker/node'; + const res = await lookup.lookupUpdates(config); + expect(res).toMatchSnapshot(); + }); + it('skips undefined values', async () => { + config.depName = 'node'; + config.purl = 'pkg:docker/node'; + const res = await lookup.lookupUpdates(config); + expect(res).toMatchSnapshot(); + }); + it('handles digest pin', async () => { + config.currentValue = '8.0.0'; + config.depName = 'node'; + config.purl = 'pkg:docker/node'; + config.pinDigests = true; + docker.getDependency.mockReturnValueOnce({ + releases: [ + { + version: '8.0.0', + }, + { + version: '8.1.0', + }, + ], + }); + docker.getDigest.mockReturnValueOnce('sha256:aaaaaaaaaaaaaaaa'); + docker.getDigest.mockReturnValueOnce('sha256:bbbbbbbbbbbbbbbb'); + const res = await lookup.lookupUpdates(config); + expect(res).toMatchSnapshot(); + }); + it('handles digest pin for non-version', async () => { + config.currentValue = 'alpine'; + config.depName = 'node'; + config.purl = 'pkg:docker/node'; + config.pinDigests = true; + docker.getDependency.mockReturnValueOnce({ + releases: [ + { + version: '8.0.0', + }, + { + version: '8.1.0', + }, + { + version: 'alpine', + }, + ], + }); + docker.getDigest.mockReturnValueOnce('sha256:aaaaaaaaaaaaaaaa'); + const res = await lookup.lookupUpdates(config); + expect(res).toMatchSnapshot(); + }); + it('handles digest update', async () => { + config.currentValue = '8.0.0'; + config.depName = 'node'; + config.purl = 'pkg:docker/node'; + config.currentDigest = 'sha256:zzzzzzzzzzzzzzz'; + config.pinDigests = true; + docker.getDependency.mockReturnValueOnce({ + releases: [ + { + version: '8.0.0', + }, + { + version: '8.1.0', + }, + ], + }); + docker.getDigest.mockReturnValueOnce('sha256:aaaaaaaaaaaaaaaa'); + docker.getDigest.mockReturnValueOnce('sha256:bbbbbbbbbbbbbbbb'); + const res = await lookup.lookupUpdates(config); + expect(res).toMatchSnapshot(); + }); + it('handles digest update for non-version', async () => { + config.currentValue = 'alpine'; + config.depName = 'node'; + config.purl = 'pkg:docker/node'; + config.currentDigest = 'sha256:zzzzzzzzzzzzzzz'; + config.pinDigests = true; + docker.getDependency.mockReturnValueOnce({ + releases: [ + { + version: 'alpine', + }, + { + version: '8.0.0', + }, + { + version: '8.1.0', + }, + ], + }); + docker.getDigest.mockReturnValueOnce('sha256:aaaaaaaaaaaaaaaa'); + const res = await lookup.lookupUpdates(config); + expect(res).toMatchSnapshot(); + }); }); }); -- GitLab