From d5dd87d1c950770064481bef113182e85db1321b Mon Sep 17 00:00:00 2001 From: Arthur De Kimpe Date: Mon, 11 Apr 2016 09:46:40 +0200 Subject: [PATCH] Add image details support with API v2 (#84) Revert changes on default projet file Add detailed information for tag listing Removed useless comments, divs, etc -> refactoring Prevent bugs from future changes of API Add digest attribute to Manifest query response Add basic pagination support to tag listing Add environment variable for tags per page Fix bug of image history without config + disabled parent id Fix tags pagination system - Fetch infos for all pages add missing comma Fetch infos for all pages -> simpler fix Disable apache directory listing feature Update tag pagination system to make it feel more like repository pagination system --- README.md | 6 ++ apache-site.conf | 1 + app/app-mode.json | 2 +- app/app.js | 6 +- app/image/image-controller.js | 35 +++++--- app/image/image-details-directive.html | 51 ++++++----- app/index.html | 2 +- .../repository-detail-controller.js | 15 ++++ app/repository/repository-detail.html | 35 ++++++++ app/repository/repository-list-controller.js | 5 +- app/repository/repository-list.html | 2 +- app/services/registry-services.js | 89 +++++++++++++++++-- app/tag/tag-controller.js | 49 +++++++--- app/tag/tag-list-directive.html | 29 +++++- start-apache.sh | 3 +- 15 files changed, 267 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 5680be1..5fda07d 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,12 @@ By default 20 repositories will be listed per page. To adjust this number, to let's say 50 pass `-e ENV_DEFAULT_REPOSITORIES_PER_PAGE=50` to your `docker run` command. +# Default tags per page + +By default 10 tags will be listed per page. To adjust this number, to +let's say 5 pass `-e ENV_DEFAULT_TAGS_PER_PAGE=5` to your `docker run` +command. Note that providing a big number will result in a heavy load on browsers. + # Contributions are welcome! If you like the application, I invite you to contribute and report bugs or feature request on the project's github page: [https://github.com/kwk/docker-registry-frontend][3]. diff --git a/apache-site.conf b/apache-site.conf index f4b66fe..60e30b1 100644 --- a/apache-site.conf +++ b/apache-site.conf @@ -10,6 +10,7 @@ # See https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-configure-your-server-to-work-with-html5mode + Options -Indexes RewriteEngine on # Don't rewrite files or directories diff --git a/app/app-mode.json b/app/app-mode.json index fdcec86..624f182 100644 --- a/app/app-mode.json +++ b/app/app-mode.json @@ -1 +1 @@ -{"browseOnly": true, "defaultRepositoriesPerPage": 20} +{"browseOnly": true, "defaultRepositoriesPerPage": 20 , "defaultTagsPerPage":10} diff --git a/app/app.js b/app/app.js index 4bf4a0e..1b82e60 100644 --- a/app/app.js +++ b/app/app.js @@ -64,6 +64,10 @@ angular templateUrl: 'repository/repository-detail.html', controller: 'RepositoryDetailController', }). + when('/repository/:repositoryUser/:repositoryName/:tagsPerPage?/:tagPage?', { + templateUrl: 'repository/repository-detail.html', + controller: 'RepositoryDetailController' + }). when('/repository/:repositoryUser/:repositoryName/tags/:searchName?', { templateUrl: 'repository/repository-detail.html', controller: 'RepositoryController', @@ -71,7 +75,7 @@ angular when('/about', { templateUrl: 'about.html', }). - when('/tag/:repositoryUser/:repositoryName/:tagName/:imageId', { + when('/tag/:repositoryUser/:repositoryName/:tagName/', { templateUrl: 'tag/tag-detail.html', controller: 'TagController', }). diff --git a/app/image/image-controller.js b/app/image/image-controller.js index 5c6cabd..65da230 100644 --- a/app/image/image-controller.js +++ b/app/image/image-controller.js @@ -8,26 +8,39 @@ * Controller of the docker-registry-frontend */ angular.module('image-controller', ['registry-services', 'app-mode-services']) - .controller('ImageController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Image', 'Ancestry', 'AppMode', - function($scope, $route, $routeParams, $location, $log, $filter, Image, Ancestry, AppMode){ - $scope.imageId = $route.current.params.imageId; - $scope.imageDetails = Image.query( {imageId: $scope.imageId} ); - $scope.imageAncestry = Ancestry.query( {imageId: $scope.imageId} ); + .controller('ImageController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Manifest', 'AppMode', + function($scope, $route, $routeParams, $location, $log, $filter, Manifest, AppMode){ + + $scope.appMode = AppMode.query(); + $scope.totalImageSize = 0; + $scope.imageDetails = Manifest.query({repoUser: $scope.repositoryUser, repoName: $scope.repositoryName, tagName: $scope.tagName}); + + + + + // This is not totally working right now (problem with big layers) /** * Calculates the total download size for the image based on - * it's ancestry. + * it's layers. */ + /* $scope.totalImageSize = null; $scope.calculateTotalImageSize = function() { $scope.totalImageSize = 0; - angular.forEach($scope.imageAncestry, function (id, key) { - /* We have to use the $promise object here to be sure the result is accessible */ - Image.get( {imageId: id} ).$promise.then(function (result) { - if (!isNaN(result.Size-0)) { - $scope.totalImageSize += result.Size; + var size; + angular.forEach($scope.imageDetails.fsLayers, function (id, key) { + + Blob.query({repoUser: $scope.repositoryUser, repoName: $scope.repositoryName, digest: id.blobSum}).$promise.then( function(data, headers){ + size = data; + console.log(data) + console.log(size) + 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 7d0792e..f66e1b2 100644 --- a/app/image/image-details-directive.html +++ b/app/image/image-details-directive.html @@ -13,7 +13,7 @@

General information -
+
@@ -28,7 +28,7 @@

-
+

@@ -52,22 +52,21 @@

-

{{imageDetails.id | limitTo: 12}}

+

+ {{imageDetails.id | limitTo: 12}} +

-
- + + + - Image Ancestry + Labels - + + + + + + + + + + + + + +
KeyValue
{{key}}{{value}}
diff --git a/app/index.html b/app/index.html index 849e244..2d03b8e 100644 --- a/app/index.html +++ b/app/index.html @@ -69,7 +69,7 @@

Docker Registry Frontend

- + diff --git a/app/repository/repository-detail-controller.js b/app/repository/repository-detail-controller.js index 1968d9a..24aa874 100644 --- a/app/repository/repository-detail-controller.js +++ b/app/repository/repository-detail-controller.js @@ -21,7 +21,22 @@ angular.module('repository-detail-controller', ['registry-services', 'app-mode-s $scope.repository = $scope.repositoryUser + '/' + $scope.repositoryName; $scope.appMode = AppMode.query(); + $scope.maxTagsPage = undefined; + // Method used to disable next & previous links + $scope.getNextHref = function (){ + if($scope.maxTagsPage > $scope.tagsCurrentPage){ + var nextPageNumber = $scope.tagsCurrentPage + 1; + return '/repository/'+$scope.repository+'/'+ $scope.tagsPerPage +'/' +nextPageNumber; + } + return '#' + } + $scope.getFirstHref = function (){ + if($scope.tagsCurrentPage > 1){ + return '/repository/'+$scope.repository+'/' + $scope.tagsPerPage +'/1'; + } + return '#' + } // selected repos $scope.selectedRepositories = []; diff --git a/app/repository/repository-detail.html b/app/repository/repository-detail.html index 5c45cbc..f4b4b14 100644 --- a/app/repository/repository-detail.html +++ b/app/repository/repository-detail.html @@ -23,3 +23,38 @@

+ diff --git a/app/repository/repository-list-controller.js b/app/repository/repository-list-controller.js index 8b6a48c..3981956 100644 --- a/app/repository/repository-list-controller.js +++ b/app/repository/repository-list-controller.js @@ -19,8 +19,9 @@ angular.module('repository-list-controller', ['registry-services', 'app-mode-ser $scope.repositoryName = $route.current.params.repositoryName; $scope.repository = $scope.repositoryUser + '/' + $scope.repositoryName; - $scope.appMode = AppMode.query(); - + $scope.appMode = AppMode.query( function (result){ + $scope.defaultTagsPerPage = result.defaultTagsPerPage + }); // How to query the repository $scope.reposPerPage = $route.current.params.reposPerPage; $scope.lastNamespace = $route.current.params.lastNamespace; diff --git a/app/repository/repository-list.html b/app/repository/repository-list.html index b088666..78a36da 100644 --- a/app/repository/repository-list.html +++ b/app/repository/repository-list.html @@ -38,7 +38,7 @@

Repositories

- {{repo.name|trim:username+'/'}} + {{repo.name|trim:username+'/'}} diff --git a/app/services/registry-services.js b/app/services/registry-services.js index acb6ee0..ed4f843 100644 --- a/app/services/registry-services.js +++ b/app/services/registry-services.js @@ -110,7 +110,6 @@ angular.module('registry-services', ['ngResource']) isArray: true, transformResponse: function(data/*, headers*/){ var res = []; - console.log(data); var resp = angular.fromJson(data); for (var idx in resp.tags){ res.push({ @@ -142,13 +141,87 @@ angular.module('registry-services', ['ngResource']) }, }); }]) - .factory('Image', ['$resource', function($resource){ - return $resource('/v1/images/:imageId/json', {}, { - 'query': { method:'GET', isArray: false}, + .factory('Manifest', ['$resource', function($resource){ + + return $resource('/v2/:repoUser/:repoName/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', + isArray: false, + transformResponse: function(data, headers){ + var res = {}; + var history = []; + var tmp; + var resp = angular.fromJson(data); + var v1Compatibility = undefined; + + 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; + } + history.push(tmp); + } + } + if(history.length > 0){ + res = history[0]; + res.history = history; + } + res.fsLayers = resp.fsLayers; + res.digest = headers('docker-content-digest'); + res.architecture = resp.architecture; + return res; + }, + } }); }]) - .factory('Ancestry', ['$resource', function($resource){ - return $resource('/v1/images/:imageId/ancestry', {}, { - 'query': { method:'GET', isArray: true}, + // This is not totally working right now (problem with big layers) + /* + .factory('Blob', ['$resource', function($resource){ + return $resource('/v2/:repoUser/:repoName/blobs/:digest', {}, { + + 'query': { + method:'HEAD', + interceptor: function(data, headers){ + var res = {contentLength: parseInt(headers('content-length'))}; + return res; + } + } + }); - }]); + }]) */ ; diff --git a/app/tag/tag-controller.js b/app/tag/tag-controller.js index 6e5a9e8..5bd7c50 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', ['registry-services']) - .controller('TagController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Tag', 'filterFilter', '$modal', - function($scope, $route, $routeParams, $location, $log, $filter, Tag, filterFilter, $modal){ + .controller('TagController', ['$scope', '$route', '$routeParams', '$location', '$log', '$filter', 'Manifest', 'Tag', 'filterFilter', '$modal', + function($scope, $route, $routeParams, $location, $log, $filter, Manifest, Tag, filterFilter, $modal){ $scope.$route = $route; $scope.$location = $location; @@ -20,16 +20,50 @@ angular.module('tag-controller', ['registry-services']) $scope.repositoryName = $route.current.params.repositoryName; $scope.repository = $scope.repositoryUser + '/' + $scope.repositoryName; $scope.tagName = $route.current.params.tagName; + $scope.tagsPerPage = $route.current.params.tagsPerPage; - // How to query the tags + // Fetch tags $scope.tags = Tag.query({ repoUser: $scope.repositoryUser, repoName: $scope.repositoryName + }, function(result){ + // Determine the number of pages + $scope.maxTagsPage = parseInt(Math.ceil(parseFloat(result.length)/parseFloat($scope.tagsPerPage))); + // Compute the right current page number + $scope.tagsCurrentPage = $route.current.params.tagPage; + if(! $scope.tagsCurrentPage){ + $scope.tagsCurrentPage = 1; + }else{ + $scope.tagsCurrentPage = parseInt($scope.tagsCurrentPage) + if($scope.tagsCurrentPage > $scope.maxTagsPage || $scope.tagsCurrentPage < 1){ + $scope.tagsCurrentPage = 1; + } + } + // Select wanted tags + var idxShift = 0; + $scope.displayedTags = $scope.tags; + if($scope.tagsPerPage){ + 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({repoUser: $scope.repositoryUser, repoName: $scope.repositoryName, tagName: result[tmpIdx].name}); + } + } + } }); + + // Copy collection for rendering in a smart-table $scope.displayedTags = [].concat($scope.tags); + // selected tags $scope.selection = []; @@ -38,13 +72,6 @@ angular.module('tag-controller', ['registry-services']) return filterFilter($scope.displayedTags, { selected: true }); }; - // watch fruits for changes - $scope.$watch('tags|filter:{selected:true}', function(nv) { - $scope.selection = nv.map(function (tag) { - return $scope.repository + ':' + tag.name; - }); - }, true); - $scope.openConfirmTagDeletionDialog = function(size) { var modalInstance = $modal.open({ animation: true, @@ -67,4 +94,4 @@ angular.module('tag-controller', ['registry-services']) }); }; - }]); + }]); \ No newline at end of file diff --git a/app/tag/tag-list-directive.html b/app/tag/tag-list-directive.html index 905a636..b933706 100644 --- a/app/tag/tag-list-directive.html +++ b/app/tag/tag-list-directive.html @@ -7,21 +7,42 @@
- +
- + + + + + + + - + + + + + + +
Tag Tag Image ID Created Author Docker version
- + {{tag.name}}
+
diff --git a/start-apache.sh b/start-apache.sh index ba3c6a0..a5c5b9b 100644 --- a/start-apache.sh +++ b/start-apache.sh @@ -40,7 +40,8 @@ echo "{\"host\": \"$ENV_REGISTRY_PROXY_FQDN\", \"port\": $ENV_REGISTRY_PROXY_POR # Overwrite browse-only option for now since only browse-only is working right now ENV_MODE_BROWSE_ONLY=true [[ -z "$ENV_DEFAULT_REPOSITORIES_PER_PAGE" ]] && ENV_DEFAULT_REPOSITORIES_PER_PAGE=20 -echo "{\"browseOnly\":$ENV_MODE_BROWSE_ONLY, \"defaultRepositoriesPerPage\":$ENV_DEFAULT_REPOSITORIES_PER_PAGE}" > /var/www/html/app-mode.json +[[ -z "$ENV_DEFAULT_TAGS_PER_PAGE" ]] && ENV_DEFAULT_TAGS_PER_PAGE=10 +echo "{\"browseOnly\":$ENV_MODE_BROWSE_ONLY, \"defaultRepositoriesPerPage\":$ENV_DEFAULT_REPOSITORIES_PER_PAGE , \"defaultTagsPerPage\":$ENV_DEFAULT_TAGS_PER_PAGE }" > /var/www/html/app-mode.json if [ "$ENV_MODE_BROWSE_ONLY" == "true" ]; then echo "export APACHE_ARGUMENTS='-D FRONTEND_BROWSE_ONLY_MODE'" >> /etc/apache2/envvars fi