diff --git a/app/image/image-controller.js b/app/image/image-controller.js index 6c6c365..60ec54f 100644 --- a/app/image/image-controller.js +++ b/app/image/image-controller.js @@ -13,8 +13,25 @@ angular.module('image-controller', ['registry-services', 'app-mode-services']) $scope.appMode = AppMode.query(); - $scope.totalImageSize = 0; - $scope.imageDetails = Manifest.query({repository: $scope.repository, tagName: $scope.tagName}); + Manifest.query({repository: $scope.repository, tagName: $scope.tagName}) + .$promise.then(function(data) { + $scope.imageDetails = angular.copy(data); + + return !data.isSchemaV2 + ? undefined + : Blob.query({repository: $scope.repository, digest: 'sha256:'+data.id}) + .$promise.then(function(config) { + $scope.imageDetails.created = config.created; + $scope.imageDetails.docker_version = config.docker_version; + $scope.imageDetails.os = config.os; + $scope.imageDetails.architecture = config.architecture; + $scope.imageDetails.labels = config.container_config && config.container_config.Labels; + $scope.imageDetails.dockerfile = config.dockerfile; + $scope.imageDetails.layers = config.dockerfile.length; + + $scope.totalImageSize = $scope.imageDetails.size; + }); + }); @@ -22,15 +39,15 @@ angular.module('image-controller', ['registry-services', 'app-mode-services']) * Calculates the total download size for the image based on * it's layers. */ - $scope.totalImageSize = null; $scope.calculateTotalImageSize = function() { $scope.totalImageSize = 0; angular.forEach($scope.imageDetails.fsLayers, function (id, key) { - Blob.query({repository: $scope.repository, digest: id.blobSum}).$promise.then(function(data){ - if(!isNaN(data.contentLength-0)){ - $scope.totalImageSize += data.contentLength; - } - }); + Blob.querySize({repository: $scope.repository, digest: id.blobSum}) + .$promise.then(function(data){ + if(!isNaN(data.contentLength-0)){ + $scope.totalImageSize += data.contentLength; + } + }); }); }; }]); diff --git a/app/image/image-details-directive.html b/app/image/image-details-directive.html index 79d3961..9fb2857 100644 --- a/app/image/image-details-directive.html +++ b/app/image/image-details-directive.html @@ -14,13 +14,13 @@

General information
-
+

{{imageDetails.labels.maintainer || imageDetails.author}}

-
+

@@ -28,7 +28,7 @@

-
+

@@ -37,25 +37,25 @@

-
+

{{imageDetails.layers}}

-
+

{{imageDetails.docker_version}}

-
+

{{imageDetails.os}}/{{imageDetails.architecture}}

-
+

@@ -75,10 +75,10 @@

- + {{totalImageSize/1024/1024 | number: 2}} MB -

diff --git a/app/services/registry-services.js b/app/services/registry-services.js index 1241c62..7ac0709 100644 --- a/app/services/registry-services.js +++ b/app/services/registry-services.js @@ -51,6 +51,113 @@ function linkParser(linkHeader) { return { repository: repository }; } +function handelSchemaV1(data) { + // https://docs.docker.com/registry/spec/manifest-v2-1/ + /** Response example: + * { + * "schemaVersion": 1, + * "name": "arthur/busybox", + * "tag": "demo", + * "architecture": "amd64", + * "fsLayers": [ + * { + * "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + * }, + * { + * "blobSum": "sha256:d7e8ec85c5abc60edf74bd4b8d68049350127e4102a084f22060f7321eac3586" + * } + * ], + * "history": [ + * { + * "v1Compatibility": "{\"id\":\"3e1018ee907f25aef8c50016296ab33624796511fdbfdbbdeca6a3ed2d0ba4e2\",\"parent\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"created\":\"2016-01-12T17:47:39.251310827Z\",\"container\":\"2732d16efa11ab7da6393645e85a7f2070af94941a782a69e86457a2284f4a69\",\"container_config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL awesome=Not yet!\"],\"Image\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"awesome\":\"Not yet!\",\"test\":\"yes\",\"working\":\"true\"}},\"docker_version\":\"1.9.1\",\"author\":\"Arthur\",\"config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"sh\"],\"Image\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"awesome\":\"Not yet!\",\"test\":\"yes\",\"working\":\"true\"}},\"architecture\":\"amd64\",\"os\":\"linux\"}" + * }, + * { + * "v1Compatibility": "{\"id\":\"5c5fb281b01ee091a0fffa5b4a4c7fb7d358e7fb7c49c263d6d7a4e35d199fd0\",\"created\":\"2015-12-08T18:31:50.979824705Z\",\"container\":\"ea7fe68f39fd0df314e841247fb940ddef4c02ab7b5edb0ee724adc3174bc8d9\",\"container_config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:c295b0748bf05d4527f500b62ff269bfd0037f7515f1375d2ee474b830bad382 in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.3\",\"config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":1113436}" + * } + * ], + * } + **/ + + var dockerFile; + var res = {}; + var history = data.history.map(function(history) { + return angular.fromJson(history.v1Compatibility); + }).filter(function(history) { + return history !== undefined; + }).map(function(history) { + return { + id: history.id, + os: history.os, + docker_version: history.docker_version, + created: history.created, + author: history.author, + labels: history.config && history.config.Labels, + layerCmd: history.container_config && history.container_config.Cmd.join(' ') + .replace(/^\/bin\/sh -c #\(nop\)\s*/, '') + .replace('/bin/sh -c', 'RUN') + .replace(/\t\t/g, '\\\n\t'), + }; + }); + + dockerFile = history.map(function(history) { + return history.layerCmd; + }).reverse(); + + if(history.length > 0){ + res = history.shift(); + res.history = history; + } + + res.dockerfile = dockerFile + res.layers = dockerFile.length + res.fsLayers = data.fsLayers; + res.architecture = data.architecture; + + return res; +} + +function handelSchemaV2(data) { + // https://docs.docker.com/registry/spec/manifest-v2-2/#image-manifest-field-descriptions + /** Response example: + * { + * "schemaVersion": 2, + * "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + * "config": { + * "mediaType": "application/vnd.docker.container.image.v1+json", + * "size": 7023, + * "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + * }, + * "layers": [ + * { + * "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + * "size": 32654, + * "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + * }, + * { + * "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + * "size": 16724, + * "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + * }, + * { + * "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + * "size": 73109, + * "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + * } + * ] + * } + **/ + + var res = { + id: data.config.digest.replace(/^sha256:/, ''), + }; + + res.size = data.layers.reduce(function(size, layer) { + return size + layer.size; + }, data.config.size); + + return res; +} + angular.module('registry-services', ['ngResource']) .factory('RegistryHost', ['$resource', function($resource){ return $resource('registry-host.json', {}, { @@ -60,23 +167,24 @@ angular.module('registry-services', ['ngResource']) }, }); }]) - // Repository returns: - // - // { - // repos: [ - // {username: 'SomeNamespace', name: 'SomeNamespace/SomeRepo1', selected: true|false}, - // {username: 'SomeOtherNamespace', name: 'SomeOtherNamespace/SomeRepo2', selected: true|false}, - // {username: 'SomeCompletelyDifferenNamespace', name: 'SomeCompletelyDifferenNamespace/SomeRepo3', selected: true|false} - // ], - // nextLink: '/v2/_catalog?last=SomeNamespace%F2SomeRepo&n=1' - // } - // - // The "nextLink" element is a preparation for supporting pagination - // (see https://github.com/docker/distribution/blob/master/docs/spec/api.md#pagination) - // - // On subsequent calls to "Repository()" you may pass in "n" as the number of - // elements per page as well as "last" which is the "nextLink" from the last - // call to Repository. + /* Repository returns: + * + * { + * repos: [ + * {username: 'SomeNamespace', name: 'SomeNamespace/SomeRepo1', selected: true|false}, + * {username: 'SomeOtherNamespace', name: 'SomeOtherNamespace/SomeRepo2', selected: true|false}, + * {username: 'SomeCompletelyDifferenNamespace', name: 'SomeCompletelyDifferenNamespace/SomeRepo3', selected: true|false} + * ], + * nextLink: '/v2/_catalog?last=SomeNamespace%F2SomeRepo&n=1' + * } + * + * The "nextLink" element is a preparation for supporting pagination + * (see https: *github.com/docker/distribution/blob/master/docs/spec/api.md#pagination) + * + * On subsequent calls to "Repository()" you may pass in "n" as the number of + * elements per page as well as "last" which is the "nextLink" from the last + * call to Repository. + **/ .factory('Repository', ['$resource', function($resource){ return $resource('/v2/_catalog?n=:n&last=:last', {}, { 'query': { @@ -153,31 +261,7 @@ angular.module('registry-services', ['ngResource']) }); }]) .factory('Manifest', ['$resource', function($resource){ - return $resource('/v2/:repository/manifests/:tagName', {}, { - // Response example: - // { - // "schemaVersion": 1, - // "name": "arthur/busybox", - // "tag": "demo", - // "architecture": "amd64", - // "fsLayers": [ - // { - // "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" - // }, - // { - // "blobSum": "sha256:d7e8ec85c5abc60edf74bd4b8d68049350127e4102a084f22060f7321eac3586" - // } - // ], - // "history": [ - // { - // "v1Compatibility": "{\"id\":\"3e1018ee907f25aef8c50016296ab33624796511fdbfdbbdeca6a3ed2d0ba4e2\",\"parent\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"created\":\"2016-01-12T17:47:39.251310827Z\",\"container\":\"2732d16efa11ab7da6393645e85a7f2070af94941a782a69e86457a2284f4a69\",\"container_config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL awesome=Not yet!\"],\"Image\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"awesome\":\"Not yet!\",\"test\":\"yes\",\"working\":\"true\"}},\"docker_version\":\"1.9.1\",\"author\":\"Arthur\",\"config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"sh\"],\"Image\":\"176dfc9032a1ec3ac8586b383e325e1a65d1f5b5e6f46c2a55052b5aea8310f7\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"awesome\":\"Not yet!\",\"test\":\"yes\",\"working\":\"true\"}},\"architecture\":\"amd64\",\"os\":\"linux\"}" - // }, - // { - // "v1Compatibility": "{\"id\":\"5c5fb281b01ee091a0fffa5b4a4c7fb7d358e7fb7c49c263d6d7a4e35d199fd0\",\"created\":\"2015-12-08T18:31:50.979824705Z\",\"container\":\"ea7fe68f39fd0df314e841247fb940ddef4c02ab7b5edb0ee724adc3174bc8d9\",\"container_config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:c295b0748bf05d4527f500b62ff269bfd0037f7515f1375d2ee474b830bad382 in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.3\",\"config\":{\"Hostname\":\"ea7fe68f39fd\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":1113436}" - // } - // ], - // } 'query': { method:'GET', headers: { @@ -185,50 +269,14 @@ angular.module('registry-services', ['ngResource']) }, isArray: false, transformResponse: function(data, headers){ - var res = {}; - var history = []; - var tmp; var resp = angular.fromJson(data); - var v1Compatibility = undefined; - var dockerfile = []; - var cmd; - var instruction; - - for (var idx in resp.history){ - - v1Compatibility = angular.fromJson(resp.history[idx].v1Compatibility); - - if(v1Compatibility !== undefined){ - tmp = { - id : v1Compatibility.id, - os : v1Compatibility.os, - docker_version: v1Compatibility.docker_version, - created: v1Compatibility.created, - // parentLayer: v1Compatibility.parent - }; - if(v1Compatibility.author){ - tmp.author = v1Compatibility.author; - } - if(v1Compatibility.config && v1Compatibility.config.Labels){ - tmp.labels = v1Compatibility.config.Labels; - } - if(v1Compatibility.container_config && v1Compatibility.container_config.Cmd){ - cmd = v1Compatibility.container_config.Cmd - instruction = cmd.join(' ').replace('/bin/sh -c #(nop) ', '').replace('/bin/sh -c ', 'RUN ') - dockerfile.unshift(instruction) - } - history.push(tmp); - } - } - if(history.length > 0){ - res = history[0]; - res.history = history; - } - res.fsLayers = resp.fsLayers; + var isSchemaV2 = (headers('content-type') === 'application/vnd.docker.distribution.manifest.v2+json'); + var res = isSchemaV2 + ? handelSchemaV2(resp) + : handelSchemaV1(resp); res.digest = headers('docker-content-digest'); - res.architecture = resp.architecture; - res.dockerfile = dockerfile - res.layers = dockerfile.length + res.isSchemaV2 = isSchemaV2; + return res; }, }, @@ -240,7 +288,7 @@ angular.module('registry-services', ['ngResource']) }]) .factory('Blob', ['$resource', function($resource){ return $resource('/v2/:repository/blobs/:digest', {}, { - 'query': { + 'querySize': { method:'HEAD', interceptor: { response: function(response){ @@ -248,6 +296,43 @@ angular.module('registry-services', ['ngResource']) return res; } } + }, + /** Example Response: + * { + * "architecture": "amd64", + * "config": {}, + * "container": "caab3f21c75adc3560754e71374cd01cb1bbe39b2b9c2809ff6c22bbcd39206c", + * "container_config": {}, + * "created": "2017-04-25T03:44:24.620936172Z", + * "docker_version": "17.04.0-ce", + * "history": [ + * { + * "created": "2017-04-24T19:20:41.290148217Z", + * "created_by": "/bin/sh -c #(nop) ADD file:712c48086043553b85ffb031d8f6c5de857a2e53974df30cdfbc1e85c1b00a25 in / " + * }, + * { + * "created": "2017-04-24T19:20:42.022627269Z", + * "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + * "empty_layer": true + * } + * ], + * "os": "linux", + * "rootfs": {} + * } + **/ + 'query': { + method: 'GET', + transformResponse: function(data, headers){ + data = angular.fromJson(data); + data.dockerfile = data.history.map(function(history) { + return history.created_by + .replace(new RegExp('^/bin/sh -c #\\(nop\\)\\s*'), '') + .replace(new RegExp('^/bin/sh -c\\s*'), 'RUN ') + .replace(/\t\t/g, '\\\n\t'); + }); + + return data; + } } }); }]); diff --git a/app/tag/tag-controller.js b/app/tag/tag-controller.js index 17a9974..ef446e5 100644 --- a/app/tag/tag-controller.js +++ b/app/tag/tag-controller.js @@ -8,8 +8,8 @@ * Controller of the docker-registry-frontend */ angular.module('tag-controller', ['ui.bootstrap', 'registry-services', 'app-mode-services']) - .controller('TagController', ['$scope', '$route', '$location', '$filter', 'Manifest', 'Tag', 'AppMode', 'filterFilter', '$modal', - function($scope, $route, $location, $filter, Manifest, Tag, AppMode, filterFilter, $modal){ + .controller('TagController', ['$scope', '$route', '$location', '$filter', 'Manifest', 'Tag', 'AppMode', 'filterFilter', '$modal', 'Blob', + function($scope, $route, $location, $filter, Manifest, Tag, AppMode, filterFilter, $modal, Blob){ $scope.$route = $route; $scope.$location = $location; @@ -60,16 +60,28 @@ angular.module('tag-controller', ['ui.bootstrap', 'registry-services', 'app-mode idxShift = ($scope.tagsCurrentPage - 1) * $scope.tagsPerPage; $scope.displayedTags = $scope.displayedTags.slice(idxShift, ($scope.tagsCurrentPage ) * $scope.tagsPerPage ); } - var tmpIdx; + // Fetch wanted manifests - for (var idx in $scope.displayedTags){ - if(!isNaN(idx)){ - tmpIdx = parseInt(idx) + idxShift; - if ( result[tmpIdx].hasOwnProperty('name') ) { - result[tmpIdx].details = Manifest.query({repository: $scope.repository, tagName: result[tmpIdx].name}); - } + $scope.displayedTags.forEach(function(tag) { + if ( tag.hasOwnProperty('name') ) { + Manifest.query({repository: $scope.repository, tagName: tag.name}) + .$promise.then(function(data) { + tag.details = angular.copy(data); + return !data.isSchemaV2 + ? undefined + : Blob.query({repository: $scope.repository, digest: 'sha256:'+data.id}) + .$promise.then(function(config) { + tag.details.created = config.created; + tag.details.docker_version = config.docker_version; + tag.details.os = config.os; + tag.details.architecture = config.architecture; + tag.details.labels = config.container_config && config.container_config.Labels; + tag.details.dockerfile = config.dockerfile; + tag.details.layers = config.dockerfile.length; + }); + }); } - } + }); $scope.$watch('displayedTags|filter:{selected:true}', function(nv) { $scope.selection = nv.map(function (tag) {