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