Skip to content

Commit

Permalink
DOCKER-929 Support the docker v2.2 manifest format
Browse files Browse the repository at this point in the history
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
  • Loading branch information
twhiteman committed Jul 20, 2017
1 parent 0539282 commit 032fccd
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 105 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

(nothing yet)

## 3.0.0

- BREAKING change, DOCKER-929 Support registry 2.0 docker images.
https://docs.docker.com/registry/spec/api/

## 2.3.0

- OS-5798 Allow '+' in the image manifest "version" field.
Expand Down
135 changes: 79 additions & 56 deletions lib/imgmanifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1669,51 +1669,45 @@ function clip(s, length) {
// ---- docker

/**
* This is OBSOLETE: use `imgUuidFromDockerInfo`.
* Keeping this around for now to assist with migration and testing.
* Return a uuid from the given string.
*/
function obsoleteImgUuidFromDockerId(dockerId) {
assert.ok(dockerId.length === 64);
return (dockerId.slice(0, 8)
+ '-' + dockerId.slice(8, 12)
+ '-' + dockerId.slice(12, 16)
+ '-' + dockerId.slice(16, 20)
+ '-' + dockerId.slice(20, 32));
function uuidFromString(str) {
assert.string(str, 'str');
assert.ok(str.length >= 32);
return (str.slice(0, 8) +
'-' + str.slice(8, 12) +
'-' + str.slice(12, 16) +
'-' + str.slice(16, 20) +
'-' + str.slice(20, 32));
}

/**
* Generate the SDC/SmartOS image uuid for this docker image.
*
* An img UUID for a Docker image/layer is a hash of the repository host
* (a.k.a the "index name") and the Docker image/layer id.
* See https://smartos.org/bugview/DOCKER-257 for design discussion on this.
* Generate the SDC/SmartOS image uuid from the docker digest chain. See RFD 57
* for why we generated the uuid this way.
*
* @param opts {Object}
* - id {String} The full docker ID
* - indexName {String} The canonicalized Docker index name, as
* from `require('docker-registry-client').parseIndex()`.
* - obsoleteUuid {Boolean} Optional. Default false. Set to true to
* get the `obsoleteImgUuidFromDockerId` behaviour for the UUID.
* @param digests {Array} The docker digest chain, base is first.
* @returns {String} uuid
* @throws {Error} If the given id does not look like a full 64-char docker id.
*/
function imgUuidFromDockerInfo(opts) {
assert.string(opts.id, 'opts.id');
assert.string(opts.indexName, 'opts.indexName');
assert.optionalBool(opts.obsoleteUuid, 'opts.obsoleteUuid');

if (opts.id.length !== 64) {
throw new Error('docker id "' + opts.id + '" is too short, full ' +
'64-char id was expected');
}

if (opts.obsoleteUuid) {
return obsoleteImgUuidFromDockerId(opts.id);
function imgUuidFromDockerDigests(digests) {
assert.arrayOfString(digests, 'digests');

// Sanity check the digests.
var badDigests = digests.filter(function (d) {
var sp = d.split(':');
if (sp.length !== 2 || sp[0] !== 'sha256' || sp[1].length !== 64) {
return true;
}
return false;
});
if (badDigests.length > 0) {
throw new Error(
'docker digests should be of the form "sha256:xxx", got: ' +
badDigests);
}

// Make a stack RFC 4122 v5 UUID using name='$indexName:$id'.
var name = opts.indexName + ':' + opts.id;
return _uuidv5(name);
var sha256sum = crypto.createHash('sha256');
sha256sum.update(digests.join(' '));
return uuidFromString(sha256sum.digest('hex'));
}

function imgManifestOsFromDockerOs(dockerOs) {
Expand All @@ -1729,40 +1723,40 @@ function imgManifestOsFromDockerOs(dockerOs) {
* @param opts {Object}
* - imgJson {Object} The Docker Registry API v1 "image json" as from
* `require('docker-registry-client').RegistryClient.getImgJson`.
* - layerDigests {Array} Array of docker layer digest strings.
* - repo {Object} A structure holding the repo host (aka "index.name")
* of the Docker image as from
* `require('docker-registry-client').parseRepo`.
* - uuid {String} Optional.
* - owner {String} Optional.
* - tags {String} Optional.
* - public {Boolean} Optional. Defaults to true.
* - obsoleteUuid {Boolean} Optional. Default false. Set to true to
* get the `obsoleteImgUuidFromDockerId` behaviour for the UUID.
* - tags {String} Optional.
* - uuid {String} Optional.
*/
function imgManifestFromDockerInfo(opts) {
assert.object(opts, 'opts');
assert.arrayOfString(opts.layerDigests, 'opts.layerDigests');
assert.ok(opts.layerDigests.length >= 1, 'opts.layerDigests.length >= 1');
assert.object(opts.imgJson, 'opts.imgJson');
assert.object(opts.repo, 'opts.repo');
assert.string(opts.repo.index.name, 'opts.repo.index.name');
assert.string(opts.repo.localName, 'opts.repo.localName');
assert.optionalString(opts.uuid, 'opts.uuid');
assert.optionalString(opts.owner, 'opts.owner');
assert.optionalArrayOfString(opts.tags, 'opts.tags'); // docker repo tags
assert.optionalBool(opts.public, 'opts.public');
assert.optionalBool(opts.obsoleteUuid, 'opts.obsoleteUuid');
var imgJson = opts.imgJson;
var indexName = opts.repo.index.name;
assert.optionalString(opts.origin, 'opts.origin');

var digest = opts.layerDigests[opts.layerDigests.length - 1];
var imgJson = opts.imgJson;
var public_ = opts.public === undefined ? true : opts.public;
var uuid = opts.uuid || (opts.obsoleteUuid
? obsoleteImgUuidFromDockerId(imgJson.id)
: imgUuidFromDockerInfo({id: imgJson.id, indexName: indexName}));
var shortId = shortDockerId(dockerIdFromDigest(digest));
var uuid = opts.uuid || imgUuidFromDockerDigests(opts.layerDigests);

var manifest = {
v: 2,
uuid: uuid,
owner: opts.owner || '00000000-0000-0000-0000-000000000000',
name: 'docker-layer',
version: imgJson.id.slice(0, 12),
version: shortId,
disabled: false,
public: public_,
published_at: new Date(imgJson.created).toISOString(),
Expand All @@ -1775,15 +1769,13 @@ function imgManifestFromDockerInfo(opts) {
MAX_DESCRIPTION_LENGTH),
tags: {
'docker:repo': opts.repo.localName,
'docker:id': imgJson.id,
'docker:id': digest,
'docker:architecture': imgJson.architecture
}
};
if (imgJson.parent) {
manifest.origin = (opts.obsoleteUuid
? obsoleteImgUuidFromDockerId(imgJson.parent)
: imgUuidFromDockerInfo(
{id: imgJson.parent, indexName: indexName}));
if (imgJson.parent || (opts.layerDigests && opts.layerDigests.length > 1)) {
manifest.origin = opts.origin ||
imgUuidFromDockerDigests(opts.layerDigests.slice(0, -1));
}
if (opts.tags) {
opts.tags.forEach(function (tag) {
Expand All @@ -1806,6 +1798,35 @@ function imgManifestFromDockerInfo(opts) {
}


/**
* Return the docker id (a string of length 64) from the docker digest.
*
* A docker digest looks like this:
* 'sha256:8ddc19f16526912237dd8af81971d5e4dd0587907234be2b83e249518d5b673f'
*
* @param digest {String} The full docker digest.
* @returns {String} The full 64-character docker id.
*/
function dockerIdFromDigest(digest) {
assert.string(digest, 'digest');
var parts = digest.split(':');
assert.ok(parts.length === 2, 'digest should contain exactly one colon');
return parts[1];
}


/**
* Return a short docker id (a string of length 12) from the full docker id.
*
* @param id {String} The full 64-character docker id.
* @returns {String} The short 12-character docker id.
*/
function shortDockerId(dockerId) {
assert.string(dockerId, 'dockerId');
assert.ok(dockerId.length === 64);
return dockerId.substr(0, 12);
}


// ---- exports

Expand All @@ -1819,6 +1840,8 @@ module.exports = {
validatePrivateManifest: validatePrivateManifest,

imgManifestFromDockerInfo: imgManifestFromDockerInfo,
obsoleteImgUuidFromDockerId: obsoleteImgUuidFromDockerId,
imgUuidFromDockerInfo: imgUuidFromDockerInfo
imgUuidFromDockerDigests: imgUuidFromDockerDigests,

dockerIdFromDigest: dockerIdFromDigest,
shortDockerId: shortDockerId
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "imgmanifest",
"description": "Library for working with Triton/SDC/SmartOS image manifests",
"version": "2.3.0",
"version": "3.0.0",
"author": "Joyent (joyent.com)",
"main": "./lib/imgmanifest.js",
"dependencies": {
"assert-plus": "0.1.2"
"assert-plus": "1.0.0"
},
"devDependencies": {
"jshint": "0.5",
Expand Down
74 changes: 27 additions & 47 deletions test/docker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ function validateManifest(fn, t, expect) {

/* BEGIN JSSTYLED */
var DOCKER_IMG_JSON = {
'id': '0e68275c469ee5a5040b8e01688fb8fac1f06138a8247265bff8ced103d01c4f',
'parent': '5ac32a0bed16ae74a155782de096f856ac1b8d016313d60d93af948a6b06f709',
'digest': 'sha256:0e68275c469ee5a5040b8e01688fb8fac1f06138a8247265bff8ced103d01c4f',
'parent': 'sha256:5ac32a0bed16ae74a155782de096f856ac1b8d016313d60d93af948a6b06f709',
'created': '2014-11-21T02:55:35.850292608Z',
'container': '860121553d31b54f88b355beadb4d76f48493f89b3e0ed1536481186444694e5',
'container_config': {
Expand Down Expand Up @@ -115,7 +115,7 @@ var DOCKER_IMG_JSON = {
'OnBuild': [],
'SecurityOpt': null
},
'docker_version': '1.3.1',
'docker_version': '1.9.1',
'config': {
'Hostname': '064f0e1ce709',
'Domainname': '',
Expand Down Expand Up @@ -156,7 +156,7 @@ var DOCKER_IMG_JSON = {
},
'architecture': 'amd64',
'os': 'linux',
'Size': 0
'size': 0
};
/* END JSSTYLED */

Expand All @@ -178,31 +178,30 @@ var EXPECTED = {
description: '/bin/sh -c #(nop) CMD [mongod]',
tags: {
/*JSSTYLED*/
'docker:id': '0e68275c469ee5a5040b8e01688fb8fac1f06138a8247265bff8ced103d01c4f'
'docker:id': 'sha256:0e68275c469ee5a5040b8e01688fb8fac1f06138a8247265bff8ced103d01c4f'
},
origin: '5ac32a0b-ed16-ae74-a155-782de096f856'
}
};

test('obsoleteImgUuidFromDockerId', function (t) {
t.equal('5ac32a0b-ed16-ae74-a155-782de096f856',
imgmanifest.obsoleteImgUuidFromDockerId(
'5ac32a0bed16ae74a155782de096f856ac1b8d016313d60d93af948a6b06f709'));
t.end();
});

test('imgUuidFromDockerInfo', function (t) {
t.equal('06fa62d6-c0ad-2054-bcae-0cf0db0443b4',
imgmanifest.imgUuidFromDockerInfo({
// JSSTYLED
id: '5ac32a0bed16ae74a155782de096f856ac1b8d016313d60d93af948a6b06f709',
indexName: 'docker.io'
}));
test('imgUuidFromDockerDigests', function (t) {
var uuid = imgmanifest.imgUuidFromDockerDigests([
DOCKER_IMG_JSON.parent,
DOCKER_IMG_JSON.digest
]);
// JSSTYLED
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
t.ok(UUID_RE.test(uuid));
t.end();
});

test('imgManifestFromDockerInfo', function (t) {
var layerDigests = [
DOCKER_IMG_JSON.parent,
DOCKER_IMG_JSON.digest
];
var manifest = imgmanifest.imgManifestFromDockerInfo({
layerDigests: layerDigests,
imgJson: DOCKER_IMG_JSON,
repo: {
index: {
Expand All @@ -217,43 +216,24 @@ test('imgManifestFromDockerInfo', function (t) {
});
t.ok(manifest);
t.ok(manifest.uuid);
t.equal(manifest.uuid, imgmanifest.imgUuidFromDockerDigests(layerDigests));
validateManifest(imgmanifest.validateMinimalManifest, t, EXPECTED);
validateManifest(imgmanifest.validateDcManifest, t, EXPECTED);
validateManifest(imgmanifest.validatePublicManifest, t, EXPECTED);
t.end();
});


// Test being able to calls the new methods but get the obsolete image UUID
// behaviour. This will be for dev/testing/rollout.
test('imgUuidFromDockerInfo (obsoleteUuid)', function (t) {
t.equal('5ac32a0b-ed16-ae74-a155-782de096f856',
imgmanifest.imgUuidFromDockerInfo({
// JSSTYLED
id: '5ac32a0bed16ae74a155782de096f856ac1b8d016313d60d93af948a6b06f709',
indexName: 'docker.io',
obsoleteUuid: true
}));
test('dockerIdFromDigest', function (t) {
t.equal(imgmanifest.dockerIdFromDigest(DOCKER_IMG_JSON.digest),
DOCKER_IMG_JSON.digest.substr(7));
t.end();
});

test('imgManifestFromDockerInfo (obsoleteUuid)', function (t) {
var manifest = imgmanifest.imgManifestFromDockerInfo({
imgJson: DOCKER_IMG_JSON,
repo: {
index: {
name: 'docker.io',
official: true
},
official: true,
remoteName: 'library/busybox',
localName: 'busybox',
canonicalName: 'docker.io/busybox'
},
obsoleteUuid: true
});
t.ok(manifest);
t.equal(manifest.uuid,
imgmanifest.obsoleteImgUuidFromDockerId(DOCKER_IMG_JSON.id));

test('shortDockerId', function (t) {
t.equal(imgmanifest.shortDockerId(
imgmanifest.dockerIdFromDigest(DOCKER_IMG_JSON.digest)),
DOCKER_IMG_JSON.digest.substr(7, 12));
t.end();
});

0 comments on commit 032fccd

Please sign in to comment.