diff --git a/app/docker/__module.js b/app/docker/__module.js index c372684de..32253b43a 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -184,6 +184,17 @@ angular.module('portainer.docker', ['portainer.app']) } }; + var imageImport = { + name: 'docker.images.import', + url: '/import', + views: { + 'content@': { + templateUrl: 'app/docker/views/images/import/importimage.html', + controller: 'ImportImageController' + } + } + }; + var networks = { name: 'docker.networks', url: '/networks', @@ -422,6 +433,7 @@ angular.module('portainer.docker', ['portainer.app']) $stateRegistryProvider.register(images); $stateRegistryProvider.register(image); $stateRegistryProvider.register(imageBuild); + $stateRegistryProvider.register(imageImport); $stateRegistryProvider.register(networks); $stateRegistryProvider.register(network); $stateRegistryProvider.register(networkCreation); diff --git a/app/docker/components/datatables/images-datatable/imagesDatatable.html b/app/docker/components/datatables/images-datatable/imagesDatatable.html index fe556201b..87cba5507 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatable.html +++ b/app/docker/components/datatables/images-datatable/imagesDatatable.html @@ -23,6 +23,17 @@ +
+ + +
- + {{ formValues.UploadFile.name }} diff --git a/app/docker/views/images/edit/image.html b/app/docker/views/images/edit/image.html index 3c8aada79..ac0a13239 100644 --- a/app/docker/views/images/edit/image.html +++ b/app/docker/views/images/edit/image.html @@ -99,6 +99,11 @@ {{ image.Id }} + diff --git a/app/docker/views/images/edit/imageController.js b/app/docker/views/images/edit/imageController.js index 236f1633f..d89cd4dc3 100644 --- a/app/docker/views/images/edit/imageController.js +++ b/app/docker/views/images/edit/imageController.js @@ -1,11 +1,15 @@ angular.module('portainer.docker') -.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', 'HttpRequestHelper', -function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryService, Notifications, HttpRequestHelper) { +.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', 'HttpRequestHelper', 'ModalService', 'FileSaver', 'Blob', +function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) { $scope.formValues = { Image: '', Registry: '' }; + $scope.state = { + exportInProgress: false + }; + $scope.sortType = 'Order'; $scope.sortReverse = false; @@ -97,6 +101,35 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ }); }; + function exportImage(image) { + HttpRequestHelper.setPortainerAgentTargetHeader(image.NodeName); + $scope.state.exportInProgress = true; + ImageService.downloadImages([image]) + .then(function success(data) { + var downloadData = new Blob([data.file], { type: 'application/x-tar' }); + FileSaver.saveAs(downloadData, 'images.tar'); + Notifications.success('Image successfully downloaded'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to download image'); + }) + .finally(function final() { + $scope.state.exportInProgress = false; + }); + } + + $scope.exportImage = function (image) { + if (image.RepoTags.length === 0 || _.includes(image.RepoTags, '')) { + Notifications.warning('', 'Cannot download a untagged image'); + return; + } + + ModalService.confirmImageExport(function (confirmed) { + if(!confirmed) { return; } + exportImage(image); + }); + }; + function initView() { HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); var endpointProvider = $scope.applicationState.endpoint.mode.provider; diff --git a/app/docker/views/images/images.html b/app/docker/views/images/images.html index 4a7c2bc9c..3c8df020a 100644 --- a/app/docker/views/images/images.html +++ b/app/docker/views/images/images.html @@ -59,8 +59,10 @@ dataset="images" table-key="images" order-by="RepoTags" show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" + download-action="downloadAction" remove-action="removeAction" force-remove-action="confirmRemovalAction" + export-in-progress="state.exportInProgress" >
diff --git a/app/docker/views/images/imagesController.js b/app/docker/views/images/imagesController.js index 8968e5fe7..cb364edad 100644 --- a/app/docker/views/images/imagesController.js +++ b/app/docker/views/images/imagesController.js @@ -1,8 +1,9 @@ angular.module('portainer.docker') -.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', -function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper) { +.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', 'FileSaver', 'Blob', +function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob) { $scope.state = { - actionInProgress: false + actionInProgress: false, + exportInProgress: false }; $scope.formValues = { @@ -39,6 +40,57 @@ function ($scope, $state, ImageService, Notifications, ModalService, HttpRequest }); }; + function isAuthorizedToDownload(selectedItems) { + + for (var i = 0; i < selectedItems.length; i++) { + var image = selectedItems[i]; + + var untagged = _.find(image.RepoTags, function (item) { + return item.indexOf('') > -1; + }); + + if (untagged) { + Notifications.warning('', 'Cannot download a untagged image'); + return false; + } + } + + if (_.uniqBy(selectedItems, 'NodeName').length > 1) { + Notifications.warning('', 'Cannot download images from different nodes at the same time'); + return false; + } + + return true; + } + + function exportImages(images) { + HttpRequestHelper.setPortainerAgentTargetHeader(images[0].NodeName); + $scope.state.exportInProgress = true; + ImageService.downloadImages(images) + .then(function success(data) { + var downloadData = new Blob([data.file], { type: 'application/x-tar' }); + FileSaver.saveAs(downloadData, 'images.tar'); + Notifications.success('Image(s) successfully downloaded'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to download image(s)'); + }) + .finally(function final() { + $scope.state.exportInProgress = false; + }); + } + + $scope.downloadAction = function (selectedItems) { + if (!isAuthorizedToDownload(selectedItems)) { + return; + } + + ModalService.confirmImageExport(function (confirmed) { + if(!confirmed) { return; } + exportImages(selectedItems); + }); + }; + $scope.removeAction = function (selectedItems, force) { var actionCount = selectedItems.length; angular.forEach(selectedItems, function (image) { diff --git a/app/docker/views/images/import/importImageController.js b/app/docker/views/images/import/importImageController.js new file mode 100644 index 000000000..b993a614f --- /dev/null +++ b/app/docker/views/images/import/importImageController.js @@ -0,0 +1,31 @@ +angular.module('portainer.docker') +.controller('ImportImageController', ['$scope', '$state', 'ImageService', 'Notifications', 'HttpRequestHelper', +function ($scope, $state, ImageService, Notifications, HttpRequestHelper) { + + $scope.state = { + actionInProgress: false + }; + + $scope.formValues = { + UploadFile: null, + NodeName: null + }; + + $scope.uploadImage = function() { + $scope.state.actionInProgress = true; + + var nodeName = $scope.formValues.NodeName; + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + var file = $scope.formValues.UploadFile; + ImageService.uploadImage(file) + .then(function success() { + Notifications.success('Images successfully uploaded'); + }) + .catch(function error(err) { + Notifications.error('Failure', err.message, 'Unable to upload image'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; +}]); diff --git a/app/docker/views/images/import/importimage.html b/app/docker/views/images/import/importimage.html new file mode 100644 index 000000000..7615080f6 --- /dev/null +++ b/app/docker/views/images/import/importimage.html @@ -0,0 +1,60 @@ + + + + Images > Import image + + + +
+
+ + +
+ +
+ Upload +
+
+ + You can upload a tar archive containing your images. + +
+
+
+ + + {{ formValues.UploadFile.name }} + + +
+
+ +
+
+ Deployment +
+ + + + +
+ +
+ Actions +
+
+
+ + {{ state.formValidationError }} +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 3cfb71b39..d74cb8f8b 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -10,7 +10,6 @@ angular.module('portainer.app') service.buildImage = function(names, file, path) { var endpointID = EndpointProvider.endpointID(); - Upload.setDefaults({ ngfMinSize: 10 }); return Upload.http({ url: 'api/endpoints/' + endpointID + '/docker/build', headers : { @@ -28,6 +27,18 @@ angular.module('portainer.app') }); }; + service.loadImages = function(file) { + var endpointID = EndpointProvider.endpointID(); + return Upload.http({ + url: 'api/endpoints/' + endpointID + '/docker/images/load', + headers : { + 'Content-Type': file.type + }, + data: file, + ignoreLoadingBar: true + }); + }; + service.createSwarmStack = function(stackName, swarmId, file, env, endpointId) { return Upload.upload({ url: 'api/stacks?method=file&type=1&endpointId=' + endpointId, diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 74260e1fc..1a16b8e55 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -156,6 +156,20 @@ angular.module('portainer.app') }); }; + service.confirmImageExport = function(callback) { + service.confirm({ + title: 'Caution', + message: 'The export may take several minutes, do not navigate away whilst the export is in progress.', + buttons: { + confirm: { + label: 'Continue', + className: 'btn-primary' + } + }, + callback: callback + }); + }; + service.confirmServiceForceUpdate = function(message, callback) { service.customPrompt({ title: 'Are you sure ?',