From 918e46be0e3491c1f14744f16539f5cd783cbdb3 Mon Sep 17 00:00:00 2001 From: LP B Date: Mon, 17 May 2021 17:43:22 +0200 Subject: [PATCH] feat(app): backport private registries frontend changes (#5056) * feat(app/docker): backport docker/components changes * feat(app/docker): backport docker/helpers changes * feat(app/docker): backport docker/views/container changes * feat(app/docker): backport docker/views/images changes * feat(app/docker): backport docker/views/registries changes * feat(app/docker): backport docker/views/services changes * feat(app/docker): backport docker changes * feat(app/kubernetes): backport kubernetes/components changes * feat(app/kubernetes): backport kubernetes/converters changes * feat(app/kubernetes): backport kubernetes/models changes * feat(app/kubernetes): backport kubernetes/registries changes * feat(app/kubernetes): backport kubernetes/services changes * feat(app/kubernetes): backport kubernetes/views/applications changes * feat(app/kubernetes): backport kubernetes/views/configurations changes * feat(app/kubernetes): backport kubernetes/views/configure changes * feat(app/kubernetes): backport kubernetes/views/resource-pools changes * feat(app/kubernetes): backport kubernetes/views changes * feat(app/portainer): backport portainer/components/accessManagement changes * feat(app/portainer): backport portainer/components/datatables changes * feat(app/portainer): backport portainer/components/forms changes * feat(app/portainer): backport portainer/components/registry-details changes * feat(app/portainer): backport portainer/models changes * feat(app/portainer): backport portainer/rest changes * feat(app/portainer): backport portainer/services changes * feat(app/portainer): backport portainer/views changes * feat(app/portainer): backport portainer changes * feat(app): backport app changes * config(project): gitignore + jsconfig changes gitignore all files under api/cmd/portainer but main.go and enable Code Editor autocomplete on import ... from '@/...' --- .gitignore | 3 +- app/constants.js | 1 - app/docker/__module.js | 22 + .../dockerSidebarContent.html | 21 +- .../por-image-registry.controller.js | 65 +- .../imageRegistry/por-image-registry.html | 3 +- .../imageRegistry/por-image-registry.js | 2 + app/docker/helpers/imageHelper.js | 140 +- .../create/createContainerController.js | 1841 ++++++++--------- .../containers/create/createcontainer.html | 3 +- .../views/containers/edit/container.html | 2 +- .../containers/edit/containerController.js | 3 +- app/docker/views/images/edit/image.html | 2 +- .../views/images/edit/imageController.js | 5 +- app/docker/views/images/images.html | 1 + .../registries/access/registryAccess.html | 16 + .../views/registries/access/registryAccess.js | 7 + .../access/registryAccessController.js | 48 + .../views/services/create/createservice.html | 1 + .../views/services/edit/includes/image.html | 1 + .../views/services/edit/serviceController.js | 6 +- app/kubernetes/__module.js | 26 +- .../kubernetesSidebarContent.html | 14 +- app/kubernetes/converters/application.js | 11 +- app/kubernetes/converters/daemonSet.js | 12 +- app/kubernetes/converters/deployment.js | 8 +- app/kubernetes/converters/resourcePool.js | 11 +- app/kubernetes/converters/statefulSet.js | 10 +- .../models/application/formValues.js | 55 +- app/kubernetes/models/daemon-set/models.js | 2 +- app/kubernetes/models/deployment/models.js | 2 +- .../models/resource-pool/formValues.js | 2 + app/kubernetes/models/stateful-set/models.js | 2 +- app/kubernetes/registries/index.js | 5 + .../kube-registry-access-view/index.js | 9 + .../kube-registry-access-view.controller.js | 71 + .../kube-registry-access-view.html | 72 + .../services/resourcePoolService.js | 24 +- app/kubernetes/services/statefulSetService.js | 1 + .../create/createApplication.html | 80 +- .../applications/create/createApplication.js | 1 - .../create/createApplicationController.js | 137 +- .../edit/configurationController.js | 17 +- .../views/configure/configureController.js | 13 +- .../create/createResourcePool.html | 122 +- .../create/createResourcePool.js | 9 +- .../create/createResourcePoolController.js | 196 +- .../resource-pools/edit/resourcePool.html | 44 + .../views/resource-pools/edit/resourcePool.js | 2 +- .../edit/resourcePoolController.js | 207 +- app/portainer/__module.js | 17 +- .../accessManagement/por-access-management.js | 1 + .../porAccessManagementController.js | 15 + .../registriesDatatable.html | 31 +- .../registriesDatatable.js | 5 +- .../registriesDatatableController.js | 85 + .../datatables/strings-datatable/index.js | 20 + .../strings-datatable/strings-datatable.html | 65 + .../registry-form-dockerhub.html | 64 + .../registry-form-dockerhub.js | 9 + .../forms/template-form/template-form.js | 13 - .../forms/template-form/templateForm.html | 580 ------ .../template-form/templateFormController.js | 55 - .../components/registry-details/index.js | 10 + .../registry-details/registry-details.html | 25 + app/portainer/models/dockerhub.js | 12 +- app/portainer/models/registry.js | 7 +- app/portainer/models/registryTypes.js | 2 + app/portainer/rest/dockerhub.js | 15 - app/portainer/rest/endpoint.js | 2 + app/portainer/rest/registry.js | 3 +- app/portainer/services/api/accessService.js | 15 +- .../services/api/dockerhubService.js | 51 - app/portainer/services/api/endpointService.js | 10 + app/portainer/services/api/registryService.js | 92 +- app/portainer/services/api/templateService.js | 152 +- .../views/endpoint-registries/registries.html | 21 + .../views/endpoint-registries/registries.js | 7 + .../registriesController.js | 51 + .../views/init/admin/initAdminController.js | 3 +- .../registries/access/registryAccess.html | 35 - .../access/registryAccessController.js | 34 - ...reateregistry.html => createRegistry.html} | 74 +- .../views/registries/create/createRegistry.js | 10 + .../create/createRegistryController.js | 196 +- .../registries/edit/registryController.js | 6 +- .../views/registries/registries.html | 74 - .../views/registries/registriesController.js | 34 +- app/portainer/views/sidebar/sidebar.html | 4 +- .../views/sidebar/sidebarController.js | 25 +- jsconfig.json | 3 +- 91 files changed, 2649 insertions(+), 2642 deletions(-) create mode 100644 app/docker/views/registries/access/registryAccess.html create mode 100644 app/docker/views/registries/access/registryAccess.js create mode 100644 app/docker/views/registries/access/registryAccessController.js create mode 100644 app/kubernetes/registries/index.js create mode 100644 app/kubernetes/registries/kube-registry-access-view/index.js create mode 100644 app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js create mode 100644 app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html create mode 100644 app/portainer/components/datatables/registries-datatable/registriesDatatableController.js create mode 100644 app/portainer/components/datatables/strings-datatable/index.js create mode 100644 app/portainer/components/datatables/strings-datatable/strings-datatable.html create mode 100644 app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html create mode 100644 app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js delete mode 100644 app/portainer/components/forms/template-form/template-form.js delete mode 100644 app/portainer/components/forms/template-form/templateForm.html delete mode 100644 app/portainer/components/forms/template-form/templateFormController.js create mode 100644 app/portainer/components/registry-details/index.js create mode 100644 app/portainer/components/registry-details/registry-details.html delete mode 100644 app/portainer/rest/dockerhub.js delete mode 100644 app/portainer/services/api/dockerhubService.js create mode 100644 app/portainer/views/endpoint-registries/registries.html create mode 100644 app/portainer/views/endpoint-registries/registries.js create mode 100644 app/portainer/views/endpoint-registries/registriesController.js delete mode 100644 app/portainer/views/registries/access/registryAccess.html delete mode 100644 app/portainer/views/registries/access/registryAccessController.js rename app/portainer/views/registries/create/{createregistry.html => createRegistry.html} (51%) create mode 100644 app/portainer/views/registries/create/createRegistry.js diff --git a/.gitignore b/.gitignore index 6f3dc2d33..7ffa4841b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ node_modules bower_components dist portainer-checksum.txt -api/cmd/portainer/portainer* +api/cmd/portainer/* +!api/cmd/portainer/main.go .tmp **/.vscode/settings.json **/.vscode/tasks.json diff --git a/app/constants.js b/app/constants.js index cb0e8f17f..a2003ed1c 100644 --- a/app/constants.js +++ b/app/constants.js @@ -1,7 +1,6 @@ angular .module('portainer') .constant('API_ENDPOINT_AUTH', 'api/auth') - .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') .constant('API_ENDPOINT_CUSTOM_TEMPLATES', 'api/custom_templates') .constant('API_ENDPOINT_EDGE_GROUPS', 'api/edge_groups') .constant('API_ENDPOINT_EDGE_JOBS', 'api/edge_jobs') diff --git a/app/docker/__module.js b/app/docker/__module.js index 516134d7d..83b0763a5 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -591,6 +591,26 @@ angular.module('portainer.docker', ['portainer.app']).config([ }, }; + const registries = { + name: 'docker.registries', + url: '/registries', + views: { + 'content@': { + component: 'endpointRegistriesView', + }, + }, + }; + + const registryAccess = { + name: 'docker.registries.access', + url: '/:id/access', + views: { + 'content@': { + component: 'dockerRegistryAccessView', + }, + }, + }; + $stateRegistryProvider.register(configs); $stateRegistryProvider.register(config); $stateRegistryProvider.register(configCreation); @@ -641,5 +661,7 @@ angular.module('portainer.docker', ['portainer.app']).config([ $stateRegistryProvider.register(volumeBrowse); $stateRegistryProvider.register(volumeCreation); $stateRegistryProvider.register(dockerFeaturesConfiguration); + $stateRegistryProvider.register(registries); + $stateRegistryProvider.register(registryAccess); }, ]); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 484451142..b8999f509 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -35,17 +35,20 @@ - - diff --git a/app/docker/components/imageRegistry/por-image-registry.controller.js b/app/docker/components/imageRegistry/por-image-registry.controller.js index edf570f8b..b5515228e 100644 --- a/app/docker/components/imageRegistry/por-image-registry.controller.js +++ b/app/docker/components/imageRegistry/por-image-registry.controller.js @@ -5,18 +5,21 @@ import { RegistryTypes } from '@/portainer/models/registryTypes'; class porImageRegistryController { /* @ngInject */ - constructor($async, $scope, ImageHelper, RegistryService, DockerHubService, ImageService, Notifications) { + constructor($async, $scope, ImageHelper, RegistryService, EndpointService, ImageService, Notifications) { this.$async = $async; this.$scope = $scope; this.ImageHelper = ImageHelper; this.RegistryService = RegistryService; - this.DockerHubService = DockerHubService; + this.EndpointService = EndpointService; this.ImageService = ImageService; this.Notifications = Notifications; - this.onInit = this.onInit.bind(this); this.onRegistryChange = this.onRegistryChange.bind(this); + this.registries = []; + this.images = []; + this.defaultRegistry = new DockerHubViewModel(); + this.$scope.$watch(() => this.model.Registry, this.onRegistryChange); } @@ -40,7 +43,7 @@ class porImageRegistryController { const registryImages = _.filter(this.images, (image) => _.includes(image, url)); images = _.map(registryImages, (image) => _.replace(image, new RegExp(url + '/?'), '')); } else { - const registries = _.filter(this.availableRegistries, (reg) => this.isKnownRegistry(reg)); + const registries = _.filter(this.registries, (reg) => this.isKnownRegistry(reg)); const registryImages = _.flatMap(registries, (registry) => _.filter(this.images, (image) => _.includes(image, registry.URL))); const imagesWithoutKnown = _.difference(this.images, registryImages); images = _.filter(imagesWithoutKnown, (image) => !this.ImageHelper.imageContainsURL(image)); @@ -63,29 +66,49 @@ class porImageRegistryController { return this.getRegistryURL(this.model.Registry) || 'docker.io'; } - async onInit() { - try { - const [registries, dockerhub, images] = await Promise.all([ - this.RegistryService.registries(), - this.DockerHubService.dockerhub(), - this.autoComplete ? this.ImageService.images() : [], - ]); - this.images = this.ImageService.getUniqueTagListFromImages(images); - this.availableRegistries = _.concat(dockerhub, registries); + async reloadRegistries() { + return this.$async(async () => { + try { + const registries = await this.EndpointService.registries(this.endpointId, this.namespace); + this.registries = _.concat(this.defaultRegistry, registries); - const id = this.model.Registry.Id; - if (!id) { - this.model.Registry = dockerhub; - } else { - this.model.Registry = _.find(this.availableRegistries, { Id: id }); + const id = this.model.Registry.Id; + const registry = _.find(this.registries, { Id: id }); + if (!registry) { + this.model.Registry = this.defaultRegistry; + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registries'); } - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve registries'); + }); + } + + async loadImages() { + return this.$async(async () => { + try { + if (!this.autoComplete) { + this.images = []; + return; + } + + const images = await this.ImageService.images(); + this.images = this.ImageService.getUniqueTagListFromImages(images); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve images'); + } + }); + } + + $onChanges({ namespace, endpointId }) { + if ((namespace || endpointId) && this.endpointId) { + this.reloadRegistries(); } } $onInit() { - return this.$async(this.onInit); + return this.$async(async () => { + await this.loadImages(); + }); } } diff --git a/app/docker/components/imageRegistry/por-image-registry.html b/app/docker/components/imageRegistry/por-image-registry.html index 77ca79c82..51b7cf397 100644 --- a/app/docker/components/imageRegistry/por-image-registry.html +++ b/app/docker/components/imageRegistry/por-image-registry.html @@ -6,10 +6,9 @@
diff --git a/app/docker/components/imageRegistry/por-image-registry.js b/app/docker/components/imageRegistry/por-image-registry.js index 52b001db0..721cd1fd7 100644 --- a/app/docker/components/imageRegistry/por-image-registry.js +++ b/app/docker/components/imageRegistry/por-image-registry.js @@ -12,6 +12,8 @@ angular.module('portainer.docker').component('porImageRegistry', { checkRateLimits: '<', onImageChange: '&', setValidity: '<', + endpointId: '<', + namespace: '<', }, require: { form: '^form', diff --git a/app/docker/helpers/imageHelper.js b/app/docker/helpers/imageHelper.js index af50cea4b..67529a10e 100644 --- a/app/docker/helpers/imageHelper.js +++ b/app/docker/helpers/imageHelper.js @@ -1,77 +1,85 @@ import _ from 'lodash-es'; -import { RegistryTypes } from '@/portainer/models/registryTypes'; +import { RegistryTypes } from 'Portainer/models/registryTypes'; -angular.module('portainer.docker').factory('ImageHelper', [ - function ImageHelperFactory() { - 'use strict'; +angular.module('portainer.docker').factory('ImageHelper', ImageHelperFactory); +function ImageHelperFactory() { + return { + isValidTag, + createImageConfigForContainer, + getImagesNamesForDownload, + removeDigestFromRepository, + imageContainsURL, + }; - var helper = {}; + function isValidTag(tag) { + return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g); + } - helper.isValidTag = isValidTag; - helper.createImageConfigForContainer = createImageConfigForContainer; - helper.getImagesNamesForDownload = getImagesNamesForDownload; - helper.removeDigestFromRepository = removeDigestFromRepository; - helper.imageContainsURL = imageContainsURL; + function getImagesNamesForDownload(images) { + var names = images.map(function (image) { + return image.RepoTags[0] !== ':' ? image.RepoTags[0] : image.Id; + }); + return { + names: names, + }; + } - function isValidTag(tag) { - return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g); + /** + * + * @param {PorImageRegistryModel} registry + */ + function createImageConfigForContainer(imageModel) { + return { + fromImage: buildImageFullURI(imageModel), + }; + } + + function imageContainsURL(image) { + const split = _.split(image, '/'); + const url = split[0]; + if (split.length > 1) { + return _.includes(url, '.') || _.includes(url, ':'); } + return false; + } - function getImagesNamesForDownload(images) { - var names = images.map(function (image) { - return image.RepoTags[0] !== ':' ? image.RepoTags[0] : image.Id; - }); - return { - names: names, - }; - } + function removeDigestFromRepository(repository) { + return repository.split('@sha')[0]; + } +} +/** + * builds the complete uri for an image based on its registry + * @param {PorImageRegistryModel} imageModel + */ +export function buildImageFullURI(imageModel) { + if (!imageModel.UseRegistry) { + return imageModel.Image; + } - /** - * - * @param {PorImageRegistryModel} registry - */ - function createImageConfigForContainer(registry) { - const data = { - fromImage: '', - }; - let fullImageName = ''; + let fullImageName = ''; - if (registry.UseRegistry) { - if (registry.Registry.Type === RegistryTypes.GITLAB) { - const slash = _.startsWith(registry.Image, ':') ? '' : '/'; - fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image; - } else if (registry.Registry.Type === RegistryTypes.QUAY) { - const name = registry.Registry.Quay.UseOrganisation ? registry.Registry.Quay.OrganisationName : registry.Registry.Username; - const url = registry.Registry.URL ? registry.Registry.URL + '/' : ''; - fullImageName = url + name + '/' + registry.Image; - } else { - const url = registry.Registry.URL ? registry.Registry.URL + '/' : ''; - fullImageName = url + registry.Image; - } - if (!_.includes(registry.Image, ':')) { - fullImageName += ':latest'; - } - } else { - fullImageName = registry.Image; - } + switch (imageModel.Registry.Type) { + case RegistryTypes.GITLAB: + fullImageName = imageModel.Registry.URL + '/' + imageModel.Registry.Gitlab.ProjectPath + (imageModel.Image.startsWith(':') ? '' : '/') + imageModel.Image; + break; + case RegistryTypes.ANONYMOUS: + fullImageName = imageModel.Image; + break; + case RegistryTypes.QUAY: + fullImageName = + (imageModel.Registry.URL ? imageModel.Registry.URL + '/' : '') + + (imageModel.Registry.Quay.UseOrganisation ? imageModel.Registry.Quay.OrganisationName : imageModel.Registry.Username) + + '/' + + imageModel.Image; + break; + default: + fullImageName = imageModel.Registry.URL + '/' + imageModel.Image; + break; + } - data.fromImage = fullImageName; - return data; - } + if (!imageModel.Image.includes(':')) { + fullImageName += ':latest'; + } - function imageContainsURL(image) { - const split = _.split(image, '/'); - const url = split[0]; - if (split.length > 1) { - return _.includes(url, '.') || _.includes(url, ':'); - } - return false; - } - - function removeDigestFromRepository(repository) { - return repository.split('@sha')[0]; - } - - return helper; - }, -]); + return fullImageName; +} diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 768d1e589..a2b082bef 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -4,983 +4,958 @@ import { ContainerCapabilities, ContainerCapability } from '../../../models/cont import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel'; import { ContainerDetailsViewModel } from '../../../models/container'; -angular.module('portainer.docker').controller('CreateContainerController', [ - '$q', - '$scope', - '$async', - '$state', - '$timeout', - '$transition$', - '$filter', - 'Container', - 'ContainerHelper', - 'Image', - 'ImageHelper', - 'Volume', - 'NetworkService', - 'ResourceControlService', - 'Authentication', - 'Notifications', - 'ContainerService', - 'ImageService', - 'FormValidator', - 'ModalService', - 'RegistryService', - 'SystemService', - 'PluginService', - 'HttpRequestHelper', - 'endpoint', - function ( - $q, - $scope, - $async, - $state, - $timeout, - $transition$, - $filter, - Container, - ContainerHelper, - Image, - ImageHelper, - Volume, - NetworkService, - ResourceControlService, - Authentication, - Notifications, - ContainerService, - ImageService, - FormValidator, - ModalService, - RegistryService, - SystemService, - PluginService, - HttpRequestHelper, - endpoint - ) { - $scope.create = create; - $scope.endpoint = endpoint; +angular.module('portainer.docker').controller('CreateContainerController', CreateContainerController); - $scope.formValues = { - alwaysPull: true, - Console: 'none', - Volumes: [], - NetworkContainer: null, - Labels: [], +/* @ngInject */ +function CreateContainerController( + $q, + $scope, + $async, + $state, + $timeout, + $transition$, + $filter, + Container, + ContainerHelper, + ImageHelper, + Volume, + NetworkService, + ResourceControlService, + Authentication, + Notifications, + ContainerService, + ImageService, + FormValidator, + ModalService, + RegistryService, + SystemService, + PluginService, + HttpRequestHelper, + endpoint +) { + $scope.create = create; + $scope.endpoint = endpoint; + + $scope.formValues = { + alwaysPull: true, + Console: 'none', + Volumes: [], + NetworkContainer: null, + Labels: [], + ExtraHosts: [], + MacAddress: '', + IPv4: '', + IPv6: '', + DnsPrimary: '', + DnsSecondary: '', + AccessControlData: new AccessControlFormData(), + CpuLimit: 0, + MemoryLimit: 0, + MemoryReservation: 0, + CmdMode: 'default', + EntrypointMode: 'default', + NodeName: null, + capabilities: [], + Sysctls: [], + LogDriverName: '', + LogDriverOpts: [], + RegistryModel: new PorImageRegistryModel(), + }; + + $scope.extraNetworks = {}; + + $scope.state = { + formValidationError: '', + actionInProgress: false, + mode: '', + pullImageValidity: true, + }; + + $scope.refreshSlider = function () { + $timeout(function () { + $scope.$broadcast('rzSliderForceRender'); + }); + }; + + $scope.onImageNameChange = function () { + $scope.formValues.CmdMode = 'default'; + $scope.formValues.EntrypointMode = 'default'; + }; + + $scope.setPullImageValidity = setPullImageValidity; + function setPullImageValidity(validity) { + if (!validity) { + $scope.formValues.alwaysPull = false; + } + $scope.state.pullImageValidity = validity; + } + + $scope.config = { + Image: '', + Env: [], + Cmd: '', + MacAddress: '', + ExposedPorts: {}, + Entrypoint: '', + HostConfig: { + RestartPolicy: { + Name: 'no', + }, + PortBindings: [], + PublishAllPorts: false, + Binds: [], + AutoRemove: false, + NetworkMode: 'bridge', + Privileged: false, + Init: false, + Runtime: null, ExtraHosts: [], - MacAddress: '', - IPv4: '', - IPv6: '', - DnsPrimary: '', - DnsSecondary: '', - AccessControlData: new AccessControlFormData(), - CpuLimit: 0, - MemoryLimit: 0, - MemoryReservation: 0, - CmdMode: 'default', - EntrypointMode: 'default', - NodeName: null, - capabilities: [], - Sysctls: [], - LogDriverName: '', - LogDriverOpts: [], - RegistryModel: new PorImageRegistryModel(), - }; + Devices: [], + CapAdd: [], + CapDrop: [], + Sysctls: {}, + }, + NetworkingConfig: { + EndpointsConfig: {}, + }, + Labels: {}, + }; - $scope.extraNetworks = {}; + $scope.addVolume = function () { + $scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, type: 'volume' }); + }; - $scope.state = { - formValidationError: '', - actionInProgress: false, - mode: '', - pullImageValidity: true, - }; + $scope.removeVolume = function (index) { + $scope.formValues.Volumes.splice(index, 1); + }; - $scope.refreshSlider = function () { - $timeout(function () { - $scope.$broadcast('rzSliderForceRender'); - }); - }; + $scope.addEnvironmentVariable = function () { + $scope.config.Env.push({ name: '', value: '' }); + }; - $scope.onImageNameChange = function () { - $scope.formValues.CmdMode = 'default'; - $scope.formValues.EntrypointMode = 'default'; - }; + $scope.removeEnvironmentVariable = function (index) { + $scope.config.Env.splice(index, 1); + }; - $scope.setPullImageValidity = setPullImageValidity; - function setPullImageValidity(validity) { - if (!validity) { - $scope.formValues.alwaysPull = false; + $scope.addPortBinding = function () { + $scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' }); + }; + + $scope.removePortBinding = function (index) { + $scope.config.HostConfig.PortBindings.splice(index, 1); + }; + + $scope.addLabel = function () { + $scope.formValues.Labels.push({ name: '', value: '' }); + }; + + $scope.removeLabel = function (index) { + $scope.formValues.Labels.splice(index, 1); + }; + + $scope.addExtraHost = function () { + $scope.formValues.ExtraHosts.push({ value: '' }); + }; + + $scope.removeExtraHost = function (index) { + $scope.formValues.ExtraHosts.splice(index, 1); + }; + + $scope.addDevice = function () { + $scope.config.HostConfig.Devices.push({ pathOnHost: '', pathInContainer: '' }); + }; + + $scope.removeDevice = function (index) { + $scope.config.HostConfig.Devices.splice(index, 1); + }; + + $scope.addSysctl = function () { + $scope.formValues.Sysctls.push({ name: '', value: '' }); + }; + + $scope.removeSysctl = function (index) { + $scope.formValues.Sysctls.splice(index, 1); + }; + + $scope.addLogDriverOpt = function () { + $scope.formValues.LogDriverOpts.push({ name: '', value: '' }); + }; + + $scope.removeLogDriverOpt = function (index) { + $scope.formValues.LogDriverOpts.splice(index, 1); + }; + + $scope.fromContainerMultipleNetworks = false; + + function prepareImageConfig(config) { + const imageConfig = ImageHelper.createImageConfigForContainer($scope.formValues.RegistryModel); + config.Image = imageConfig.fromImage; + } + + function preparePortBindings(config) { + const bindings = ContainerHelper.preparePortBindings(config.HostConfig.PortBindings); + config.ExposedPorts = {}; + _.forEach(bindings, (_, key) => (config.ExposedPorts[key] = {})); + config.HostConfig.PortBindings = bindings; + } + + function prepareConsole(config) { + var value = $scope.formValues.Console; + var openStdin = true; + var tty = true; + if (value === 'tty') { + openStdin = false; + } else if (value === 'interactive') { + tty = false; + } else if (value === 'none') { + openStdin = false; + tty = false; + } + config.OpenStdin = openStdin; + config.Tty = tty; + } + + function prepareCmd(config) { + if (_.isEmpty(config.Cmd) || $scope.formValues.CmdMode == 'default') { + delete config.Cmd; + } else { + config.Cmd = ContainerHelper.commandStringToArray(config.Cmd); + } + } + + function prepareEntrypoint(config) { + if ($scope.formValues.EntrypointMode == 'default' || (_.isEmpty(config.Cmd) && _.isEmpty(config.Entrypoint))) { + config.Entrypoint = null; + } + } + + function prepareEnvironmentVariables(config) { + var env = []; + config.Env.forEach(function (v) { + if (v.name && v.value) { + env.push(v.name + '=' + v.value); } - $scope.state.pullImageValidity = validity; - } + }); + config.Env = env; + } - $scope.config = { - Image: '', - Env: [], - Cmd: '', - MacAddress: '', - ExposedPorts: {}, - Entrypoint: '', - HostConfig: { - RestartPolicy: { - Name: 'no', - }, - PortBindings: [], - PublishAllPorts: false, - Binds: [], - AutoRemove: false, - NetworkMode: 'bridge', - Privileged: false, - Init: false, - Runtime: null, - ExtraHosts: [], - Devices: [], - CapAdd: [], - CapDrop: [], - Sysctls: {}, - }, - NetworkingConfig: { - EndpointsConfig: {}, - }, - Labels: {}, - }; + function prepareVolumes(config) { + var binds = []; + var volumes = {}; - $scope.addVolume = function () { - $scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, type: 'volume' }); - }; - - $scope.removeVolume = function (index) { - $scope.formValues.Volumes.splice(index, 1); - }; - - $scope.addEnvironmentVariable = function () { - $scope.config.Env.push({ name: '', value: '' }); - }; - - $scope.removeEnvironmentVariable = function (index) { - $scope.config.Env.splice(index, 1); - }; - - $scope.addPortBinding = function () { - $scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' }); - }; - - $scope.removePortBinding = function (index) { - $scope.config.HostConfig.PortBindings.splice(index, 1); - }; - - $scope.addLabel = function () { - $scope.formValues.Labels.push({ name: '', value: '' }); - }; - - $scope.removeLabel = function (index) { - $scope.formValues.Labels.splice(index, 1); - }; - - $scope.addExtraHost = function () { - $scope.formValues.ExtraHosts.push({ value: '' }); - }; - - $scope.removeExtraHost = function (index) { - $scope.formValues.ExtraHosts.splice(index, 1); - }; - - $scope.addDevice = function () { - $scope.config.HostConfig.Devices.push({ pathOnHost: '', pathInContainer: '' }); - }; - - $scope.removeDevice = function (index) { - $scope.config.HostConfig.Devices.splice(index, 1); - }; - - $scope.addSysctl = function () { - $scope.formValues.Sysctls.push({ name: '', value: '' }); - }; - - $scope.removeSysctl = function (index) { - $scope.formValues.Sysctls.splice(index, 1); - }; - - $scope.addLogDriverOpt = function () { - $scope.formValues.LogDriverOpts.push({ name: '', value: '' }); - }; - - $scope.removeLogDriverOpt = function (index) { - $scope.formValues.LogDriverOpts.splice(index, 1); - }; - - $scope.fromContainerMultipleNetworks = false; - - function prepareImageConfig(config) { - const imageConfig = ImageHelper.createImageConfigForContainer($scope.formValues.RegistryModel); - config.Image = imageConfig.fromImage; - } - - function preparePortBindings(config) { - const bindings = ContainerHelper.preparePortBindings(config.HostConfig.PortBindings); - config.ExposedPorts = {}; - _.forEach(bindings, (_, key) => (config.ExposedPorts[key] = {})); - config.HostConfig.PortBindings = bindings; - } - - function prepareConsole(config) { - var value = $scope.formValues.Console; - var openStdin = true; - var tty = true; - if (value === 'tty') { - openStdin = false; - } else if (value === 'interactive') { - tty = false; - } else if (value === 'none') { - openStdin = false; - tty = false; - } - config.OpenStdin = openStdin; - config.Tty = tty; - } - - function prepareCmd(config) { - if (_.isEmpty(config.Cmd) || $scope.formValues.CmdMode == 'default') { - delete config.Cmd; - } else { - config.Cmd = ContainerHelper.commandStringToArray(config.Cmd); - } - } - - function prepareEntrypoint(config) { - if ($scope.formValues.EntrypointMode == 'default' || (_.isEmpty(config.Cmd) && _.isEmpty(config.Entrypoint))) { - config.Entrypoint = null; - } - } - - function prepareEnvironmentVariables(config) { - var env = []; - config.Env.forEach(function (v) { - if (v.name && v.value) { - env.push(v.name + '=' + v.value); + $scope.formValues.Volumes.forEach(function (volume) { + var name = volume.name; + var containerPath = volume.containerPath; + if (name && containerPath) { + var bind = name + ':' + containerPath; + volumes[containerPath] = {}; + if (volume.readOnly) { + bind += ':ro'; } + binds.push(bind); + } + }); + config.HostConfig.Binds = binds; + config.Volumes = volumes; + } + + function prepareNetworkConfig(config) { + var mode = config.HostConfig.NetworkMode; + var container = $scope.formValues.NetworkContainer; + var containerName = container; + if (container && typeof container === 'object') { + containerName = $filter('trimcontainername')(container.Names[0]); + } + var networkMode = mode; + if (containerName) { + networkMode += ':' + containerName; + config.Hostname = ''; + } + config.HostConfig.NetworkMode = networkMode; + config.MacAddress = $scope.formValues.MacAddress; + + config.NetworkingConfig.EndpointsConfig[networkMode] = { + IPAMConfig: { + IPv4Address: $scope.formValues.IPv4, + IPv6Address: $scope.formValues.IPv6, + }, + }; + + if (networkMode && _.get($scope.config.NetworkingConfig.EndpointsConfig[networkMode], 'Aliases')) { + var aliases = $scope.config.NetworkingConfig.EndpointsConfig[networkMode].Aliases; + config.NetworkingConfig.EndpointsConfig[networkMode].Aliases = _.filter(aliases, (o) => { + return !_.startsWith($scope.fromContainer.Id, o); }); - config.Env = env; } - function prepareVolumes(config) { - var binds = []; - var volumes = {}; + var dnsServers = []; + if ($scope.formValues.DnsPrimary) { + dnsServers.push($scope.formValues.DnsPrimary); + } + if ($scope.formValues.DnsSecondary) { + dnsServers.push($scope.formValues.DnsSecondary); + } + config.HostConfig.Dns = dnsServers; - $scope.formValues.Volumes.forEach(function (volume) { - var name = volume.name; - var containerPath = volume.containerPath; - if (name && containerPath) { - var bind = name + ':' + containerPath; - volumes[containerPath] = {}; - if (volume.readOnly) { - bind += ':ro'; + config.HostConfig.ExtraHosts = _.map( + _.filter($scope.formValues.ExtraHosts, (v) => v.value), + 'value' + ); + } + + function prepareLabels(config) { + var labels = {}; + $scope.formValues.Labels.forEach(function (label) { + if (label.name) { + if (label.value) { + labels[label.name] = label.value; + } else { + labels[label.name] = ''; + } + } + }); + config.Labels = labels; + } + + function prepareDevices(config) { + var path = []; + config.HostConfig.Devices.forEach(function (p) { + if (p.pathOnHost) { + if (p.pathInContainer === '') { + p.pathInContainer = p.pathOnHost; + } + path.push({ PathOnHost: p.pathOnHost, PathInContainer: p.pathInContainer, CgroupPermissions: 'rwm' }); + } + }); + config.HostConfig.Devices = path; + } + + function prepareSysctls(config) { + var sysctls = {}; + $scope.formValues.Sysctls.forEach(function (sysctl) { + if (sysctl.name && sysctl.value) { + sysctls[sysctl.name] = sysctl.value; + } + }); + config.HostConfig.Sysctls = sysctls; + } + + function prepareResources(config) { + // Memory Limit - Round to 0.125 + if ($scope.formValues.MemoryLimit >= 0) { + var memoryLimit = (Math.round($scope.formValues.MemoryLimit * 8) / 8).toFixed(3); + memoryLimit *= 1024 * 1024; + config.HostConfig.Memory = memoryLimit; + } + + // Memory Resevation - Round to 0.125 + if ($scope.formValues.MemoryReservation >= 0) { + var memoryReservation = (Math.round($scope.formValues.MemoryReservation * 8) / 8).toFixed(3); + memoryReservation *= 1024 * 1024; + config.HostConfig.MemoryReservation = memoryReservation; + } + + // CPU Limit + if ($scope.formValues.CpuLimit >= 0) { + config.HostConfig.NanoCpus = $scope.formValues.CpuLimit * 1000000000; + } + } + + function prepareLogDriver(config) { + var logOpts = {}; + if ($scope.formValues.LogDriverName) { + config.HostConfig.LogConfig = { Type: $scope.formValues.LogDriverName }; + if ($scope.formValues.LogDriverName !== 'none') { + $scope.formValues.LogDriverOpts.forEach(function (opt) { + if (opt.name) { + logOpts[opt.name] = opt.value; } - binds.push(bind); - } - }); - config.HostConfig.Binds = binds; - config.Volumes = volumes; - } - - function prepareNetworkConfig(config) { - var mode = config.HostConfig.NetworkMode; - var container = $scope.formValues.NetworkContainer; - var containerName = container; - if (container && typeof container === 'object') { - containerName = $filter('trimcontainername')(container.Names[0]); - } - var networkMode = mode; - if (containerName) { - networkMode += ':' + containerName; - config.Hostname = ''; - } - config.HostConfig.NetworkMode = networkMode; - config.MacAddress = $scope.formValues.MacAddress; - - config.NetworkingConfig.EndpointsConfig[networkMode] = { - IPAMConfig: { - IPv4Address: $scope.formValues.IPv4, - IPv6Address: $scope.formValues.IPv6, - }, - }; - - if (networkMode && _.get($scope.config.NetworkingConfig.EndpointsConfig[networkMode], 'Aliases')) { - var aliases = $scope.config.NetworkingConfig.EndpointsConfig[networkMode].Aliases; - config.NetworkingConfig.EndpointsConfig[networkMode].Aliases = _.filter(aliases, (o) => { - return !_.startsWith($scope.fromContainer.Id, o); }); - } - - var dnsServers = []; - if ($scope.formValues.DnsPrimary) { - dnsServers.push($scope.formValues.DnsPrimary); - } - if ($scope.formValues.DnsSecondary) { - dnsServers.push($scope.formValues.DnsSecondary); - } - config.HostConfig.Dns = dnsServers; - - config.HostConfig.ExtraHosts = _.map( - _.filter($scope.formValues.ExtraHosts, (v) => v.value), - 'value' - ); - } - - function prepareLabels(config) { - var labels = {}; - $scope.formValues.Labels.forEach(function (label) { - if (label.name) { - if (label.value) { - labels[label.name] = label.value; - } else { - labels[label.name] = ''; - } - } - }); - config.Labels = labels; - } - - function prepareDevices(config) { - var path = []; - config.HostConfig.Devices.forEach(function (p) { - if (p.pathOnHost) { - if (p.pathInContainer === '') { - p.pathInContainer = p.pathOnHost; - } - path.push({ PathOnHost: p.pathOnHost, PathInContainer: p.pathInContainer, CgroupPermissions: 'rwm' }); - } - }); - config.HostConfig.Devices = path; - } - - function prepareSysctls(config) { - var sysctls = {}; - $scope.formValues.Sysctls.forEach(function (sysctl) { - if (sysctl.name && sysctl.value) { - sysctls[sysctl.name] = sysctl.value; - } - }); - config.HostConfig.Sysctls = sysctls; - } - - function prepareResources(config) { - // Memory Limit - Round to 0.125 - if ($scope.formValues.MemoryLimit >= 0) { - var memoryLimit = (Math.round($scope.formValues.MemoryLimit * 8) / 8).toFixed(3); - memoryLimit *= 1024 * 1024; - config.HostConfig.Memory = memoryLimit; - } - - // Memory Resevation - Round to 0.125 - if ($scope.formValues.MemoryReservation >= 0) { - var memoryReservation = (Math.round($scope.formValues.MemoryReservation * 8) / 8).toFixed(3); - memoryReservation *= 1024 * 1024; - config.HostConfig.MemoryReservation = memoryReservation; - } - - // CPU Limit - if ($scope.formValues.CpuLimit >= 0) { - config.HostConfig.NanoCpus = $scope.formValues.CpuLimit * 1000000000; - } - } - - function prepareLogDriver(config) { - var logOpts = {}; - if ($scope.formValues.LogDriverName) { - config.HostConfig.LogConfig = { Type: $scope.formValues.LogDriverName }; - if ($scope.formValues.LogDriverName !== 'none') { - $scope.formValues.LogDriverOpts.forEach(function (opt) { - if (opt.name) { - logOpts[opt.name] = opt.value; - } - }); - if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { - config.HostConfig.LogConfig.Config = logOpts; - } + if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { + config.HostConfig.LogConfig.Config = logOpts; } } } + } - function prepareCapabilities(config) { - var allowed = $scope.formValues.capabilities.filter(function (item) { - return item.allowed === true; - }); - var notAllowed = $scope.formValues.capabilities.filter(function (item) { - return item.allowed === false; - }); + function prepareCapabilities(config) { + var allowed = $scope.formValues.capabilities.filter(function (item) { + return item.allowed === true; + }); + var notAllowed = $scope.formValues.capabilities.filter(function (item) { + return item.allowed === false; + }); - var getCapName = function (item) { - return item.capability; - }; - config.HostConfig.CapAdd = allowed.map(getCapName); - config.HostConfig.CapDrop = notAllowed.map(getCapName); - } - - function prepareConfiguration() { - var config = angular.copy($scope.config); - prepareCmd(config); - prepareEntrypoint(config); - prepareNetworkConfig(config); - prepareImageConfig(config); - preparePortBindings(config); - prepareConsole(config); - prepareEnvironmentVariables(config); - prepareVolumes(config); - prepareLabels(config); - prepareDevices(config); - prepareResources(config); - prepareLogDriver(config); - prepareCapabilities(config); - prepareSysctls(config); - return config; - } - - function loadFromContainerCmd() { - if ($scope.config.Cmd) { - $scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd); - $scope.formValues.CmdMode = 'override'; - } - } - - function loadFromContainerEntrypoint() { - if (_.has($scope.config, 'Entrypoint')) { - if ($scope.config.Entrypoint == null) { - $scope.config.Entrypoint = ''; - } - $scope.formValues.EntrypointMode = 'override'; - } - } - - function loadFromContainerPortBindings() { - const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings); - $scope.config.HostConfig.PortBindings = bindings; - } - - function loadFromContainerVolumes(d) { - for (var v in d.Mounts) { - if ({}.hasOwnProperty.call(d.Mounts, v)) { - var mount = d.Mounts[v]; - var volume = { - type: mount.Type, - name: mount.Name || mount.Source, - containerPath: mount.Destination, - readOnly: mount.RW === false, - }; - $scope.formValues.Volumes.push(volume); - } - } - } - - $scope.resetNetworkConfig = function () { - $scope.config.NetworkingConfig = { - EndpointsConfig: {}, - }; + var getCapName = function (item) { + return item.capability; }; + config.HostConfig.CapAdd = allowed.map(getCapName); + config.HostConfig.CapDrop = notAllowed.map(getCapName); + } - function loadFromContainerNetworkConfig(d) { - $scope.config.NetworkingConfig = { - EndpointsConfig: {}, + function prepareConfiguration() { + var config = angular.copy($scope.config); + prepareCmd(config); + prepareEntrypoint(config); + prepareNetworkConfig(config); + prepareImageConfig(config); + preparePortBindings(config); + prepareConsole(config); + prepareEnvironmentVariables(config); + prepareVolumes(config); + prepareLabels(config); + prepareDevices(config); + prepareResources(config); + prepareLogDriver(config); + prepareCapabilities(config); + prepareSysctls(config); + return config; + } + + function loadFromContainerCmd() { + if ($scope.config.Cmd) { + $scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd); + $scope.formValues.CmdMode = 'override'; + } + } + + function loadFromContainerEntrypoint() { + if (_.has($scope.config, 'Entrypoint')) { + if ($scope.config.Entrypoint == null) { + $scope.config.Entrypoint = ''; + } + $scope.formValues.EntrypointMode = 'override'; + } + } + + function loadFromContainerPortBindings() { + const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings); + $scope.config.HostConfig.PortBindings = bindings; + } + + function loadFromContainerVolumes(d) { + for (var v in d.Mounts) { + if ({}.hasOwnProperty.call(d.Mounts, v)) { + var mount = d.Mounts[v]; + var volume = { + type: mount.Type, + name: mount.Name || mount.Source, + containerPath: mount.Destination, + readOnly: mount.RW === false, + }; + $scope.formValues.Volumes.push(volume); + } + } + } + + $scope.resetNetworkConfig = function () { + $scope.config.NetworkingConfig = { + EndpointsConfig: {}, + }; + }; + + function loadFromContainerNetworkConfig(d) { + $scope.config.NetworkingConfig = { + EndpointsConfig: {}, + }; + var networkMode = d.HostConfig.NetworkMode; + if (networkMode === 'default') { + $scope.config.HostConfig.NetworkMode = 'bridge'; + if (!_.find($scope.availableNetworks, { Name: 'bridge' })) { + $scope.config.HostConfig.NetworkMode = 'nat'; + } + } + if ($scope.config.HostConfig.NetworkMode.indexOf('container:') === 0) { + var netContainer = $scope.config.HostConfig.NetworkMode.split(/^container:/)[1]; + $scope.config.HostConfig.NetworkMode = 'container'; + for (var c in $scope.runningContainers) { + if ($scope.runningContainers[c].Id == netContainer) { + $scope.formValues.NetworkContainer = $scope.runningContainers[c]; + } + } + } + $scope.fromContainerMultipleNetworks = Object.keys(d.NetworkSettings.Networks).length >= 2; + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]) { + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig) { + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address) { + $scope.formValues.IPv4 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address; + } + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address) { + $scope.formValues.IPv6 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address; + } + } + } + $scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]; + if (Object.keys(d.NetworkSettings.Networks).length > 1) { + var firstNetwork = d.NetworkSettings.Networks[Object.keys(d.NetworkSettings.Networks)[0]]; + $scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = firstNetwork; + $scope.extraNetworks = angular.copy(d.NetworkSettings.Networks); + delete $scope.extraNetworks[Object.keys(d.NetworkSettings.Networks)[0]]; + } + $scope.formValues.MacAddress = d.Config.MacAddress; + + if (d.HostConfig.Dns && d.HostConfig.Dns[0]) { + $scope.formValues.DnsPrimary = d.HostConfig.Dns[0]; + if (d.HostConfig.Dns[1]) { + $scope.formValues.DnsSecondary = d.HostConfig.Dns[1]; + } + } + + // ExtraHosts + if ($scope.config.HostConfig.ExtraHosts) { + var extraHosts = $scope.config.HostConfig.ExtraHosts; + for (var i = 0; i < extraHosts.length; i++) { + var host = extraHosts[i]; + $scope.formValues.ExtraHosts.push({ value: host }); + } + $scope.config.HostConfig.ExtraHosts = []; + } + } + + function loadFromContainerEnvironmentVariables() { + var envArr = []; + for (var e in $scope.config.Env) { + if ({}.hasOwnProperty.call($scope.config.Env, e)) { + var arr = $scope.config.Env[e].split(/\=(.*)/); + envArr.push({ name: arr[0], value: arr[1] }); + } + } + $scope.config.Env = envArr; + } + + function loadFromContainerLabels() { + for (var l in $scope.config.Labels) { + if ({}.hasOwnProperty.call($scope.config.Labels, l)) { + $scope.formValues.Labels.push({ name: l, value: $scope.config.Labels[l] }); + } + } + } + + function loadFromContainerConsole() { + if ($scope.config.OpenStdin && $scope.config.Tty) { + $scope.formValues.Console = 'both'; + } else if (!$scope.config.OpenStdin && $scope.config.Tty) { + $scope.formValues.Console = 'tty'; + } else if ($scope.config.OpenStdin && !$scope.config.Tty) { + $scope.formValues.Console = 'interactive'; + } else if (!$scope.config.OpenStdin && !$scope.config.Tty) { + $scope.formValues.Console = 'none'; + } + } + + function loadFromContainerDevices() { + var path = []; + for (var dev in $scope.config.HostConfig.Devices) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) { + var device = $scope.config.HostConfig.Devices[dev]; + path.push({ pathOnHost: device.PathOnHost, pathInContainer: device.PathInContainer }); + } + } + $scope.config.HostConfig.Devices = path; + } + + function loadFromContainerSysctls() { + for (var s in $scope.config.HostConfig.Sysctls) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) { + $scope.formValues.Sysctls.push({ name: s, value: $scope.config.HostConfig.Sysctls[s] }); + } + } + } + + function loadFromContainerImageConfig() { + RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image) + .then((model) => { + $scope.formValues.RegistryModel = model; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrive registry'); + }); + } + + function loadFromContainerResources(d) { + if (d.HostConfig.NanoCpus) { + $scope.formValues.CpuLimit = d.HostConfig.NanoCpus / 1000000000; + } + if (d.HostConfig.Memory) { + $scope.formValues.MemoryLimit = d.HostConfig.Memory / 1024 / 1024; + } + if (d.HostConfig.MemoryReservation) { + $scope.formValues.MemoryReservation = d.HostConfig.MemoryReservation / 1024 / 1024; + } + } + + function loadFromContainerCapabilities(d) { + if (d.HostConfig.CapAdd) { + d.HostConfig.CapAdd.forEach(function (cap) { + $scope.formValues.capabilities.push(new ContainerCapability(cap, true)); + }); + } + if (d.HostConfig.CapDrop) { + d.HostConfig.CapDrop.forEach(function (cap) { + $scope.formValues.capabilities.push(new ContainerCapability(cap, false)); + }); + } + + function hasCapability(item) { + return item.capability === cap.capability; + } + + var capabilities = new ContainerCapabilities(); + for (var i = 0; i < capabilities.length; i++) { + var cap = capabilities[i]; + if (!_.find($scope.formValues.capabilities, hasCapability)) { + $scope.formValues.capabilities.push(cap); + } + } + + $scope.formValues.capabilities.sort(function (a, b) { + return a.capability < b.capability ? -1 : 1; + }); + } + + function loadFromContainerSpec() { + // Get container + Container.get({ id: $transition$.params().from }) + .$promise.then(function success(d) { + var fromContainer = new ContainerDetailsViewModel(d); + if (fromContainer.ResourceControl && fromContainer.ResourceControl.Public) { + $scope.formValues.AccessControlData.AccessControlEnabled = false; + } + $scope.fromContainer = fromContainer; + $scope.state.mode = 'duplicate'; + $scope.config = ContainerHelper.configFromContainer(fromContainer.Model); + loadFromContainerCmd(d); + loadFromContainerEntrypoint(d); + loadFromContainerLogging(d); + loadFromContainerPortBindings(d); + loadFromContainerVolumes(d); + loadFromContainerNetworkConfig(d); + loadFromContainerEnvironmentVariables(d); + loadFromContainerLabels(d); + loadFromContainerConsole(d); + loadFromContainerDevices(d); + loadFromContainerImageConfig(d); + loadFromContainerResources(d); + loadFromContainerCapabilities(d); + loadFromContainerSysctls(d); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve container'); + }); + } + + function loadFromContainerLogging(config) { + var logConfig = config.HostConfig.LogConfig; + $scope.formValues.LogDriverName = logConfig.Type; + $scope.formValues.LogDriverOpts = _.map(logConfig.Config, function (value, name) { + return { + name: name, + value: value, }; - var networkMode = d.HostConfig.NetworkMode; - if (networkMode === 'default') { - $scope.config.HostConfig.NetworkMode = 'bridge'; - if (!_.find($scope.availableNetworks, { Name: 'bridge' })) { + }); + } + + async function initView() { + var nodeName = $transition$.params().nodeName; + $scope.formValues.NodeName = nodeName; + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + + $scope.isAdmin = Authentication.isAdmin(); + $scope.showDeviceMapping = await shouldShowDevices(); + $scope.showSysctls = await shouldShowSysctls(); + $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled(); + $scope.isAdminOrEndpointAdmin = Authentication.isAdmin(); + + Volume.query( + {}, + function (d) { + $scope.availableVolumes = d.Volumes.sort((vol1, vol2) => { + return vol1.Name.localeCompare(vol2.Name); + }); + }, + function (e) { + Notifications.error('Failure', e, 'Unable to retrieve volumes'); + } + ); + + var provider = $scope.applicationState.endpoint.mode.provider; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25) + .then(function success(networks) { + networks.push({ Name: 'container' }); + $scope.availableNetworks = networks.sort((a, b) => a.Name.localeCompare(b.Name)); + + if (_.find(networks, { Name: 'nat' })) { $scope.config.HostConfig.NetworkMode = 'nat'; } - } - if ($scope.config.HostConfig.NetworkMode.indexOf('container:') === 0) { - var netContainer = $scope.config.HostConfig.NetworkMode.split(/^container:/)[1]; - $scope.config.HostConfig.NetworkMode = 'container'; - for (var c in $scope.runningContainers) { - if ($scope.runningContainers[c].Id == netContainer) { - $scope.formValues.NetworkContainer = $scope.runningContainers[c]; - } - } - } - $scope.fromContainerMultipleNetworks = Object.keys(d.NetworkSettings.Networks).length >= 2; - if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]) { - if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig) { - if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address) { - $scope.formValues.IPv4 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address; - } - if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address) { - $scope.formValues.IPv6 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address; - } - } - } - $scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]; - if (Object.keys(d.NetworkSettings.Networks).length > 1) { - var firstNetwork = d.NetworkSettings.Networks[Object.keys(d.NetworkSettings.Networks)[0]]; - $scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = firstNetwork; - $scope.extraNetworks = angular.copy(d.NetworkSettings.Networks); - delete $scope.extraNetworks[Object.keys(d.NetworkSettings.Networks)[0]]; - } - $scope.formValues.MacAddress = d.Config.MacAddress; - - if (d.HostConfig.Dns && d.HostConfig.Dns[0]) { - $scope.formValues.DnsPrimary = d.HostConfig.Dns[0]; - if (d.HostConfig.Dns[1]) { - $scope.formValues.DnsSecondary = d.HostConfig.Dns[1]; - } - } - - // ExtraHosts - if ($scope.config.HostConfig.ExtraHosts) { - var extraHosts = $scope.config.HostConfig.ExtraHosts; - for (var i = 0; i < extraHosts.length; i++) { - var host = extraHosts[i]; - $scope.formValues.ExtraHosts.push({ value: host }); - } - $scope.config.HostConfig.ExtraHosts = []; - } - } - - function loadFromContainerEnvironmentVariables() { - var envArr = []; - for (var e in $scope.config.Env) { - if ({}.hasOwnProperty.call($scope.config.Env, e)) { - var arr = $scope.config.Env[e].split(/\=(.*)/); - envArr.push({ name: arr[0], value: arr[1] }); - } - } - $scope.config.Env = envArr; - } - - function loadFromContainerLabels() { - for (var l in $scope.config.Labels) { - if ({}.hasOwnProperty.call($scope.config.Labels, l)) { - $scope.formValues.Labels.push({ name: l, value: $scope.config.Labels[l] }); - } - } - } - - function loadFromContainerConsole() { - if ($scope.config.OpenStdin && $scope.config.Tty) { - $scope.formValues.Console = 'both'; - } else if (!$scope.config.OpenStdin && $scope.config.Tty) { - $scope.formValues.Console = 'tty'; - } else if ($scope.config.OpenStdin && !$scope.config.Tty) { - $scope.formValues.Console = 'interactive'; - } else if (!$scope.config.OpenStdin && !$scope.config.Tty) { - $scope.formValues.Console = 'none'; - } - } - - function loadFromContainerDevices() { - var path = []; - for (var dev in $scope.config.HostConfig.Devices) { - if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) { - var device = $scope.config.HostConfig.Devices[dev]; - path.push({ pathOnHost: device.PathOnHost, pathInContainer: device.PathInContainer }); - } - } - $scope.config.HostConfig.Devices = path; - } - - function loadFromContainerSysctls() { - for (var s in $scope.config.HostConfig.Sysctls) { - if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) { - $scope.formValues.Sysctls.push({ name: s, value: $scope.config.HostConfig.Sysctls[s] }); - } - } - } - - function loadFromContainerImageConfig() { - RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image) - .then((model) => { - $scope.formValues.RegistryModel = model; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrive registry'); - }); - } - - function loadFromContainerResources(d) { - if (d.HostConfig.NanoCpus) { - $scope.formValues.CpuLimit = d.HostConfig.NanoCpus / 1000000000; - } - if (d.HostConfig.Memory) { - $scope.formValues.MemoryLimit = d.HostConfig.Memory / 1024 / 1024; - } - if (d.HostConfig.MemoryReservation) { - $scope.formValues.MemoryReservation = d.HostConfig.MemoryReservation / 1024 / 1024; - } - } - - function loadFromContainerCapabilities(d) { - if (d.HostConfig.CapAdd) { - d.HostConfig.CapAdd.forEach(function (cap) { - $scope.formValues.capabilities.push(new ContainerCapability(cap, true)); - }); - } - if (d.HostConfig.CapDrop) { - d.HostConfig.CapDrop.forEach(function (cap) { - $scope.formValues.capabilities.push(new ContainerCapability(cap, false)); - }); - } - - function hasCapability(item) { - return item.capability === cap.capability; - } - - var capabilities = new ContainerCapabilities(); - for (var i = 0; i < capabilities.length; i++) { - var cap = capabilities[i]; - if (!_.find($scope.formValues.capabilities, hasCapability)) { - $scope.formValues.capabilities.push(cap); - } - } - - $scope.formValues.capabilities.sort(function (a, b) { - return a.capability < b.capability ? -1 : 1; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve networks'); }); - } - function loadFromContainerSpec() { - // Get container - Container.get({ id: $transition$.params().from }) - .$promise.then(function success(d) { - var fromContainer = new ContainerDetailsViewModel(d); - if (fromContainer.ResourceControl && fromContainer.ResourceControl.Public) { - $scope.formValues.AccessControlData.AccessControlEnabled = false; - } - $scope.fromContainer = fromContainer; - $scope.state.mode = 'duplicate'; - $scope.config = ContainerHelper.configFromContainer(fromContainer.Model); - loadFromContainerCmd(d); - loadFromContainerEntrypoint(d); - loadFromContainerLogging(d); - loadFromContainerPortBindings(d); - loadFromContainerVolumes(d); - loadFromContainerNetworkConfig(d); - loadFromContainerEnvironmentVariables(d); - loadFromContainerLabels(d); - loadFromContainerConsole(d); - loadFromContainerDevices(d); - loadFromContainerImageConfig(d); - loadFromContainerResources(d); - loadFromContainerCapabilities(d); - loadFromContainerSysctls(d); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve container'); - }); - } + Container.query( + {}, + function (d) { + var containers = d; + $scope.runningContainers = containers; + if ($transition$.params().from) { + loadFromContainerSpec(); + } else { + $scope.fromContainer = {}; + $scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : []; + } + }, + function (e) { + Notifications.error('Failure', e, 'Unable to retrieve running containers'); + } + ); - function loadFromContainerLogging(config) { - var logConfig = config.HostConfig.LogConfig; - $scope.formValues.LogDriverName = logConfig.Type; - $scope.formValues.LogDriverOpts = _.map(logConfig.Config, function (value, name) { - return { - name: name, - value: value, - }; + SystemService.info() + .then(function success(data) { + $scope.availableRuntimes = data.Runtimes ? Object.keys(data.Runtimes) : []; + $scope.state.sliderMaxCpu = 32; + if (data.NCPU) { + $scope.state.sliderMaxCpu = data.NCPU; + } + $scope.state.sliderMaxMemory = 32768; + if (data.MemTotal) { + $scope.state.sliderMaxMemory = Math.floor(data.MemTotal / 1000 / 1000); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve engine details'); }); + + $scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers; + $scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers; + + PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) { + $scope.availableLoggingDrivers = loggingDrivers; + }); + } + + function validateForm(accessControlData, isAdmin) { + $scope.state.formValidationError = ''; + var error = ''; + error = FormValidator.validateAccessControl(accessControlData, isAdmin); + + if (error) { + $scope.state.formValidationError = error; + return false; + } + return true; + } + + function create() { + var oldContainer = null; + HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName); + return findCurrentContainer().then(setOldContainer).then(confirmCreateContainer).then(startCreationProcess).catch(notifyOnError).finally(final); + + function final() { + $scope.state.actionInProgress = false; } - async function initView() { - var nodeName = $transition$.params().nodeName; - $scope.formValues.NodeName = nodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + function setOldContainer(container) { + oldContainer = container; + return container; + } - $scope.isAdmin = Authentication.isAdmin(); - $scope.showDeviceMapping = await shouldShowDevices(); - $scope.showSysctls = await shouldShowSysctls(); - $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled(); - $scope.isAdminOrEndpointAdmin = Authentication.isAdmin(); - - Volume.query( - {}, - function (d) { - $scope.availableVolumes = d.Volumes.sort((vol1, vol2) => { - return vol1.Name.localeCompare(vol2.Name); - }); - }, - function (e) { - Notifications.error('Failure', e, 'Unable to retrieve volumes'); - } - ); - - var provider = $scope.applicationState.endpoint.mode.provider; - var apiVersion = $scope.applicationState.endpoint.apiVersion; - NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25) - .then(function success(networks) { - networks.push({ Name: 'container' }); - $scope.availableNetworks = networks.sort((a, b) => a.Name.localeCompare(b.Name)); - - if (_.find(networks, { Name: 'nat' })) { - $scope.config.HostConfig.NetworkMode = 'nat'; + function findCurrentContainer() { + return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } }) + .$promise.then(function onQuerySuccess(containers) { + if (!containers.length) { + return; } + return containers[0]; }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve networks'); - }); - - Container.query( - {}, - function (d) { - var containers = d; - $scope.runningContainers = containers; - if ($transition$.params().from) { - loadFromContainerSpec(); - } else { - $scope.fromContainer = {}; - $scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : []; - } - }, - function (e) { - Notifications.error('Failure', e, 'Unable to retrieve running containers'); - } - ); - - SystemService.info() - .then(function success(data) { - $scope.availableRuntimes = data.Runtimes ? Object.keys(data.Runtimes) : []; - $scope.state.sliderMaxCpu = 32; - if (data.NCPU) { - $scope.state.sliderMaxCpu = data.NCPU; - } - $scope.state.sliderMaxMemory = 32768; - if (data.MemTotal) { - $scope.state.sliderMaxMemory = Math.floor(data.MemTotal / 1000 / 1000); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve engine details'); - }); - - $scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers; - $scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers; - - PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) { - $scope.availableLoggingDrivers = loggingDrivers; - }); - } - - function validateForm(accessControlData, isAdmin) { - $scope.state.formValidationError = ''; - var error = ''; - error = FormValidator.validateAccessControl(accessControlData, isAdmin); - - if (error) { - $scope.state.formValidationError = error; - return false; - } - return true; - } - - function create() { - var oldContainer = null; - HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName); - return findCurrentContainer().then(setOldContainer).then(confirmCreateContainer).then(startCreationProcess).catch(notifyOnError).finally(final); - - function final() { - $scope.state.actionInProgress = false; - } - - function setOldContainer(container) { - oldContainer = container; - return container; - } - - function findCurrentContainer() { - return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } }) - .$promise.then(function onQuerySuccess(containers) { - if (!containers.length) { - return; - } - return containers[0]; - }) - .catch(notifyOnError); - - function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to retrieve containers'); - } - } - - function startCreationProcess(confirmed) { - if (!confirmed) { - return $q.when(); - } - if (!validateAccessControl()) { - return $q.when(); - } - $scope.state.actionInProgress = true; - return pullImageIfNeeded() - .then(stopAndRenameContainer) - .then(createNewContainer) - .then(applyResourceControl) - .then(connectToExtraNetworks) - .then(removeOldContainer) - .then(onSuccess) - .catch(onCreationProcessFail); - } - - function onCreationProcessFail(error) { - var deferred = $q.defer(); - removeNewContainer() - .then(restoreOldContainerName) - .then(function () { - deferred.reject(error); - }) - .catch(function (restoreError) { - deferred.reject(restoreError); - }); - return deferred.promise; - } - - function removeNewContainer() { - return findCurrentContainer().then(function onContainerLoaded(container) { - if (container && (!oldContainer || container.Id !== oldContainer.Id)) { - return ContainerService.remove(container, true); - } - }); - } - - function restoreOldContainerName() { - if (!oldContainer) { - return; - } - return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1)); - } - - function confirmCreateContainer(container) { - if (!container) { - return $q.when(true); - } - - return showConfirmationModal(); - - function showConfirmationModal() { - var deferred = $q.defer(); - - ModalService.confirm({ - title: 'Are you sure ?', - message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?', - buttons: { - confirm: { - label: 'Replace', - className: 'btn-danger', - }, - }, - callback: function onConfirm(confirmed) { - deferred.resolve(confirmed); - }, - }); - - return deferred.promise; - } - } - - function stopAndRenameContainer() { - if (!oldContainer) { - return $q.when(); - } - return stopContainerIfNeeded(oldContainer).then(renameContainer); - } - - function stopContainerIfNeeded(oldContainer) { - if (oldContainer.State !== 'running') { - return $q.when(); - } - return ContainerService.stopContainer(oldContainer.Id); - } - - function renameContainer() { - return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1) + '-old'); - } - - function pullImageIfNeeded() { - return $q.when($scope.formValues.alwaysPull && ImageService.pullImage($scope.formValues.RegistryModel, true)); - } - - function createNewContainer() { - return $async(async () => { - const config = prepareConfiguration(); - return await ContainerService.createAndStartContainer(config); - }); - } - - function applyResourceControl(newContainer) { - const userId = Authentication.getUserDetails().ID; - const resourceControl = newContainer.Portainer.ResourceControl; - const containerId = newContainer.Id; - const accessControlData = $scope.formValues.AccessControlData; - - return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl).then(function onApplyResourceControlSuccess() { - return containerId; - }); - } - - function connectToExtraNetworks(newContainerId) { - if (!$scope.extraNetworks) { - return $q.when(); - } - - var connectionPromises = _.forOwn($scope.extraNetworks, function (network, networkName) { - if (_.has(network, 'Aliases')) { - var aliases = _.filter(network.Aliases, (o) => { - return !_.startsWith($scope.fromContainer.Id, o); - }); - } - return NetworkService.connectContainer(networkName, newContainerId, aliases); - }); - - return $q.all(connectionPromises); - } - - function removeOldContainer() { - var deferred = $q.defer(); - - if (!oldContainer) { - deferred.resolve(); - return; - } - - ContainerService.remove(oldContainer, true).then(notifyOnRemoval).catch(notifyOnRemoveError); - - return deferred.promise; - - function notifyOnRemoval() { - Notifications.success('Container Removed', oldContainer.Id); - deferred.resolve(); - } - - function notifyOnRemoveError(err) { - deferred.reject({ msg: 'Unable to remove container', err: err }); - } - } + .catch(notifyOnError); function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to create container'); - } - - function validateAccessControl() { - var accessControlData = $scope.formValues.AccessControlData; - return validateForm(accessControlData, $scope.isAdmin); - } - - function onSuccess() { - Notifications.success('Container successfully created'); - $state.go('docker.containers', {}, { reload: true }); + Notifications.error('Failure', err, 'Unable to retrieve containers'); } } - async function shouldShowDevices() { - return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin(); + function startCreationProcess(confirmed) { + if (!confirmed) { + return $q.when(); + } + if (!validateAccessControl()) { + return $q.when(); + } + $scope.state.actionInProgress = true; + return pullImageIfNeeded() + .then(stopAndRenameContainer) + .then(createNewContainer) + .then(applyResourceControl) + .then(connectToExtraNetworks) + .then(removeOldContainer) + .then(onSuccess) + .catch(onCreationProcessFail); } - async function shouldShowSysctls() { - const { allowSysctlSettingForRegularUsers } = $scope.applicationState.application; - - return allowSysctlSettingForRegularUsers || Authentication.isAdmin(); + function onCreationProcessFail(error) { + var deferred = $q.defer(); + removeNewContainer() + .then(restoreOldContainerName) + .then(function () { + deferred.reject(error); + }) + .catch(function (restoreError) { + deferred.reject(restoreError); + }); + return deferred.promise; } - async function checkIfContainerCapabilitiesEnabled() { - return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin(); + function removeNewContainer() { + return findCurrentContainer().then(function onContainerLoaded(container) { + if (container && (!oldContainer || container.Id !== oldContainer.Id)) { + return ContainerService.remove(container, true); + } + }); } - initView(); - }, -]); + function restoreOldContainerName() { + if (!oldContainer) { + return; + } + return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1)); + } + + function confirmCreateContainer(container) { + if (!container) { + return $q.when(true); + } + + return showConfirmationModal(); + + function showConfirmationModal() { + var deferred = $q.defer(); + + ModalService.confirm({ + title: 'Are you sure ?', + message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?', + buttons: { + confirm: { + label: 'Replace', + className: 'btn-danger', + }, + }, + callback: function onConfirm(confirmed) { + deferred.resolve(confirmed); + }, + }); + + return deferred.promise; + } + } + + function stopAndRenameContainer() { + if (!oldContainer) { + return $q.when(); + } + return stopContainerIfNeeded(oldContainer).then(renameContainer); + } + + function stopContainerIfNeeded(oldContainer) { + if (oldContainer.State !== 'running') { + return $q.when(); + } + return ContainerService.stopContainer(oldContainer.Id); + } + + function renameContainer() { + return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1) + '-old'); + } + + function pullImageIfNeeded() { + return $q.when($scope.formValues.alwaysPull && ImageService.pullImage($scope.formValues.RegistryModel, true)); + } + + function createNewContainer() { + return $async(async () => { + const config = prepareConfiguration(); + return await ContainerService.createAndStartContainer(config); + }); + } + + function applyResourceControl(newContainer) { + const userId = Authentication.getUserDetails().ID; + const resourceControl = newContainer.Portainer.ResourceControl; + const containerId = newContainer.Id; + const accessControlData = $scope.formValues.AccessControlData; + + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl).then(function onApplyResourceControlSuccess() { + return containerId; + }); + } + + function connectToExtraNetworks(newContainerId) { + if (!$scope.extraNetworks) { + return $q.when(); + } + + var connectionPromises = _.forOwn($scope.extraNetworks, function (network, networkName) { + if (_.has(network, 'Aliases')) { + var aliases = _.filter(network.Aliases, (o) => { + return !_.startsWith($scope.fromContainer.Id, o); + }); + } + return NetworkService.connectContainer(networkName, newContainerId, aliases); + }); + + return $q.all(connectionPromises); + } + + function removeOldContainer() { + var deferred = $q.defer(); + + if (!oldContainer) { + deferred.resolve(); + return; + } + + ContainerService.remove(oldContainer, true).then(notifyOnRemoval).catch(notifyOnRemoveError); + + return deferred.promise; + + function notifyOnRemoval() { + Notifications.success('Container Removed', oldContainer.Id); + deferred.resolve(); + } + + function notifyOnRemoveError(err) { + deferred.reject({ msg: 'Unable to remove container', err: err }); + } + } + + function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to create container'); + } + + function validateAccessControl() { + var accessControlData = $scope.formValues.AccessControlData; + return validateForm(accessControlData, $scope.isAdmin); + } + + function onSuccess() { + Notifications.success('Container successfully created'); + $state.go('docker.containers', {}, { reload: true }); + } + } + + async function shouldShowDevices() { + return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin(); + } + + async function shouldShowSysctls() { + const { allowSysctlSettingForRegularUsers } = $scope.applicationState.application; + + return allowSysctlSettingForRegularUsers || Authentication.isAdmin(); + } + + async function checkIfContainerCapabilitiesEnabled() { + return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin(); + } + + initView(); +} diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index de43d1fa2..3c9a42bf3 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -45,10 +45,11 @@ auto-complete="true" label-class="col-sm-1" input-class="col-sm-11" + endpoint-id="endpoint.Id" + on-image-change="onImageNameChange()" endpoint="endpoint" is-admin="isAdmin" check-rate-limits="formValues.alwaysPull" - on-image-change="onImageNameChange()" set-validity="setPullImageValidity" > diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index 097162ce0..40cd16358 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -190,7 +190,7 @@ - +
diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 83db4e944..345d56718 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -21,7 +21,6 @@ angular.module('portainer.docker').controller('ContainerController', [ 'ImageService', 'HttpRequestHelper', 'Authentication', - 'StateManager', 'endpoint', function ( $q, @@ -42,9 +41,9 @@ angular.module('portainer.docker').controller('ContainerController', [ ImageService, HttpRequestHelper, Authentication, - StateManager, endpoint ) { + $scope.endpoint = endpoint; $scope.activityTime = 0; $scope.portBindings = []; $scope.displayRecreateButton = false; diff --git a/app/docker/views/images/edit/image.html b/app/docker/views/images/edit/image.html index b8a686edb..84d25b817 100644 --- a/app/docker/views/images/edit/image.html +++ b/app/docker/views/images/edit/image.html @@ -63,7 +63,7 @@
- +
diff --git a/app/docker/views/images/edit/imageController.js b/app/docker/views/images/edit/imageController.js index 597cdb7a0..f110f603a 100644 --- a/app/docker/views/images/edit/imageController.js +++ b/app/docker/views/images/edit/imageController.js @@ -6,7 +6,7 @@ angular.module('portainer.docker').controller('ImageController', [ '$scope', '$transition$', '$state', - '$timeout', + 'endpoint', 'ImageService', 'ImageHelper', 'RegistryService', @@ -15,7 +15,8 @@ angular.module('portainer.docker').controller('ImageController', [ 'ModalService', 'FileSaver', 'Blob', - function ($q, $scope, $transition$, $state, $timeout, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) { + function ($q, $scope, $transition$, $state, endpoint, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) { + $scope.endpoint = endpoint; $scope.formValues = { RegistryModel: new PorImageRegistryModel(), }; diff --git a/app/docker/views/images/images.html b/app/docker/views/images/images.html index a2f3034c3..83897cf0c 100644 --- a/app/docker/views/images/images.html +++ b/app/docker/views/images/images.html @@ -20,6 +20,7 @@ pull-warning="true" label-class="col-sm-1" input-class="col-sm-11" + endpoint-id="endpoint.Id" endpoint="endpoint" is-admin="isAdmin" set-validity="setPullImageValidity" diff --git a/app/docker/views/registries/access/registryAccess.html b/app/docker/views/registries/access/registryAccess.html new file mode 100644 index 000000000..2f07251d4 --- /dev/null +++ b/app/docker/views/registries/access/registryAccess.html @@ -0,0 +1,16 @@ + + + Registries > {{ $ctrl.registry.Name }} > Access management + + + + + + diff --git a/app/docker/views/registries/access/registryAccess.js b/app/docker/views/registries/access/registryAccess.js new file mode 100644 index 000000000..7ee0814dd --- /dev/null +++ b/app/docker/views/registries/access/registryAccess.js @@ -0,0 +1,7 @@ +angular.module('portainer.docker').component('dockerRegistryAccessView', { + templateUrl: './registryAccess.html', + controller: 'DockerRegistryAccessController', + bindings: { + endpoint: '<', + }, +}); diff --git a/app/docker/views/registries/access/registryAccessController.js b/app/docker/views/registries/access/registryAccessController.js new file mode 100644 index 000000000..9565e540b --- /dev/null +++ b/app/docker/views/registries/access/registryAccessController.js @@ -0,0 +1,48 @@ +class DockerRegistryAccessController { + /* @ngInject */ + constructor($async, $state, Notifications, RegistryService, EndpointService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.EndpointService = EndpointService; + this.RegistryService = RegistryService; + + this.updateAccess = this.updateAccess.bind(this); + } + + updateAccess() { + return this.$async(async () => { + this.state.actionInProgress = true; + try { + await this.EndpointService.updateRegistryAccess(this.state.endpointId, this.state.registryId, this.registryEndpointAccesses); + this.Notifications.success('Access successfully updated'); + this.$state.reload(); + } catch (err) { + this.state.actionInProgress = false; + this.Notifications.error('Failure', err, 'Unable to update accesses'); + } + }); + } + + $onInit() { + return this.$async(async () => { + try { + this.state = { + viewReady: false, + actionInProgress: false, + endpointId: this.$state.params.endpointId, + registryId: this.$state.params.id, + }; + this.registry = await this.RegistryService.registry(this.state.registryId, this.state.endpointId); + this.registryEndpointAccesses = this.registry.RegistryAccesses[this.state.endpointId] || {}; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registry details'); + } finally { + this.state.viewReady = true; + } + }); + } +} + +export default DockerRegistryAccessController; +angular.module('portainer.docker').controller('DockerRegistryAccessController', DockerRegistryAccessController); diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index 3e2be2cba..1d6034a3a 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -25,6 +25,7 @@ auto-complete="true" label-class="col-sm-1" input-class="col-sm-11" + endpoint-id="endpoint.Id" endpoint="endpoint" is-admin="isAdmin" check-rate-limits="true" diff --git a/app/docker/views/services/edit/includes/image.html b/app/docker/views/services/edit/includes/image.html index 2a3fae3a2..1ce46156d 100644 --- a/app/docker/views/services/edit/includes/image.html +++ b/app/docker/views/services/edit/includes/image.html @@ -8,6 +8,7 @@ auto-complete="true" label-class="col-sm-1" input-class="col-sm-11" + endpoint-id="endpoint.Id" endpoint="endpoint" is-admin="isAdmin" check-rate-limits="true" diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js index cb41d94d8..ae33cb2bd 100644 --- a/app/docker/views/services/edit/serviceController.js +++ b/app/docker/views/services/edit/serviceController.js @@ -47,7 +47,6 @@ angular.module('portainer.docker').controller('ServiceController', [ 'VolumeService', 'ImageHelper', 'WebhookService', - 'EndpointProvider', 'clipboard', 'WebhookHelper', 'NetworkService', @@ -79,7 +78,6 @@ angular.module('portainer.docker').controller('ServiceController', [ VolumeService, ImageHelper, WebhookService, - EndpointProvider, clipboard, WebhookHelper, NetworkService, @@ -330,7 +328,7 @@ angular.module('portainer.docker').controller('ServiceController', [ Notifications.error('Failure', err, 'Unable to delete webhook'); }); } else { - WebhookService.createServiceWebhook(service.Id, EndpointProvider.endpointID()) + WebhookService.createServiceWebhook(service.Id, endpoint.Id) .then(function success(data) { $scope.WebhookExists = true; $scope.webhookID = data.Id; @@ -678,7 +676,7 @@ angular.module('portainer.docker').controller('ServiceController', [ availableImages: ImageService.images(), availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25), availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25), - webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()), + webhooks: WebhookService.webhooks(service.Id, endpoint.Id), }); }) .then(async function success(data) { diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index c48095d81..60ef7b5e6 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -1,4 +1,6 @@ -angular.module('portainer.kubernetes', ['portainer.app']).config([ +import registriesModule from './registries'; + +angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).config([ '$stateRegistryProvider', function ($stateRegistryProvider) { 'use strict'; @@ -262,6 +264,26 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([ }, }; + const registries = { + name: 'kubernetes.registries', + url: '/registries', + views: { + 'content@': { + component: 'endpointRegistriesView', + }, + }, + }; + + const registriesAccess = { + name: 'kubernetes.registries.access', + url: '/:id/access', + views: { + 'content@': { + component: 'kubernetesRegistryAccessView', + }, + }, + }; + $stateRegistryProvider.register(kubernetes); $stateRegistryProvider.register(applications); $stateRegistryProvider.register(applicationCreation); @@ -286,5 +308,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([ $stateRegistryProvider.register(resourcePoolAccess); $stateRegistryProvider.register(volumes); $stateRegistryProvider.register(volume); + $stateRegistryProvider.register(registries); + $stateRegistryProvider.register(registriesAccess); }, ]); diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html index 90785a22c..18265ba47 100644 --- a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html +++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html @@ -15,7 +15,17 @@ diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 97f257f95..4ec84f802 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -62,6 +62,9 @@ class KubernetesApplicationConverter { if (containers.length) { res.Image = containers[0].image; } + if (data.spec.template && data.spec.template.spec && data.spec.template.spec.imagePullSecrets && data.spec.template.spec.imagePullSecrets.length) { + res.RegistryId = parseInt(data.spec.template.spec.imagePullSecrets[0].name.replace('registry-', ''), 10); + } res.CreationDate = data.metadata.creationTimestamp; res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined); res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data]; @@ -268,7 +271,8 @@ class KubernetesApplicationConverter { res.Name = app.Name; res.StackName = app.StackName; res.ApplicationOwner = app.ApplicationOwner; - res.Image = app.Image; + res.ImageModel.Image = app.Image; + res.ImageModel.Registry.Id = app.RegistryId; res.ReplicaCount = app.TotalPodsCount; res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory); res.CpuLimit = app.Limits.Cpu; @@ -292,7 +296,10 @@ class KubernetesApplicationConverter { res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL; } - KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels); + if (app.Pods && app.Pods.length) { + KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels); + } + return res; } diff --git a/app/kubernetes/converters/daemonSet.js b/app/kubernetes/converters/daemonSet.js index 063005593..8f95eba1c 100644 --- a/app/kubernetes/converters/daemonSet.js +++ b/app/kubernetes/converters/daemonSet.js @@ -10,10 +10,11 @@ import { import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; +import { buildImageFullURI } from 'Docker/helpers/imageHelper'; class KubernetesDaemonSetConverter { /** - * Generate KubernetesDaemonSet from KubenetesApplicationFormValues + * Generate KubernetesDaemonSet from KubernetesApplicationFormValues * @param {KubernetesApplicationFormValues} formValues */ static applicationFormValuesToDaemonSet(formValues, volumeClaims) { @@ -23,7 +24,7 @@ class KubernetesDaemonSetConverter { res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; res.ApplicationOwner = formValues.ApplicationOwner; res.ApplicationName = formValues.Name; - res.Image = formValues.Image; + res.ImageModel = formValues.ImageModel; res.CpuLimit = formValues.CpuLimit; res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); @@ -35,7 +36,7 @@ class KubernetesDaemonSetConverter { /** * Generate CREATE payload from DaemonSet - * @param {KubernetesDaemonSetPayload} model DaemonSet to genereate payload from + * @param {KubernetesDaemonSetPayload} model DaemonSet to generate payload from */ static createPayload(daemonSet) { const payload = new KubernetesDaemonSetCreatePayload(); @@ -50,7 +51,10 @@ class KubernetesDaemonSetConverter { payload.spec.template.metadata.labels.app = daemonSet.Name; payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName; payload.spec.template.spec.containers[0].name = daemonSet.Name; - payload.spec.template.spec.containers[0].image = daemonSet.Image; + payload.spec.template.spec.containers[0].image = buildImageFullURI(daemonSet.ImageModel); + if (daemonSet.ImageModel.Registry && daemonSet.ImageModel.Registry.Authentication) { + payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${daemonSet.ImageModel.Registry.Id}` }]; + } payload.spec.template.spec.affinity = daemonSet.Affinity; KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env); KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts); diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js index 471faa179..080644641 100644 --- a/app/kubernetes/converters/deployment.js +++ b/app/kubernetes/converters/deployment.js @@ -11,6 +11,7 @@ import { import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; +import { buildImageFullURI } from 'Docker/helpers/imageHelper'; class KubernetesDeploymentConverter { /** @@ -25,7 +26,7 @@ class KubernetesDeploymentConverter { res.ApplicationOwner = formValues.ApplicationOwner; res.ApplicationName = formValues.Name; res.ReplicaCount = formValues.ReplicaCount; - res.Image = formValues.Image; + res.ImageModel = formValues.ImageModel; res.CpuLimit = formValues.CpuLimit; res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); @@ -53,7 +54,10 @@ class KubernetesDeploymentConverter { payload.spec.template.metadata.labels.app = deployment.Name; payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName; payload.spec.template.spec.containers[0].name = deployment.Name; - payload.spec.template.spec.containers[0].image = deployment.Image; + payload.spec.template.spec.containers[0].image = buildImageFullURI(deployment.ImageModel); + if (deployment.ImageModel.Registry && deployment.ImageModel.Registry.Authentication) { + payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${deployment.ImageModel.Registry.Id}` }]; + } payload.spec.template.spec.affinity = deployment.Affinity; KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env); KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts); diff --git a/app/kubernetes/converters/resourcePool.js b/app/kubernetes/converters/resourcePool.js index 234d13d27..eb20ce5e4 100644 --- a/app/kubernetes/converters/resourcePool.js +++ b/app/kubernetes/converters/resourcePool.js @@ -28,7 +28,16 @@ class KubernetesResourcePoolConverter { } }); const ingresses = _.without(ingMap, undefined); - return [namespace, quota, ingresses]; + const registries = _.map(formValues.Registries, (r) => { + if (!r.RegistryAccesses[formValues.EndpointId]) { + r.RegistryAccesses[formValues.EndpointId] = { Namespaces: [] }; + } + if (!_.includes(r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name)) { + r.RegistryAccesses[formValues.EndpointId].Namespaces = [...r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name]; + } + return r; + }); + return [namespace, quota, ingresses, registries]; } } diff --git a/app/kubernetes/converters/statefulSet.js b/app/kubernetes/converters/statefulSet.js index 26cffd4ac..a7693513b 100644 --- a/app/kubernetes/converters/statefulSet.js +++ b/app/kubernetes/converters/statefulSet.js @@ -12,6 +12,7 @@ import { import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; +import { buildImageFullURI } from 'Docker/helpers/imageHelper'; import KubernetesPersistentVolumeClaimConverter from './persistentVolumeClaim'; class KubernetesStatefulSetConverter { @@ -27,7 +28,7 @@ class KubernetesStatefulSetConverter { res.ApplicationOwner = formValues.ApplicationOwner; res.ApplicationName = formValues.Name; res.ReplicaCount = formValues.ReplicaCount; - res.Image = formValues.Image; + res.ImageModel = formValues.ImageModel; res.CpuLimit = formValues.CpuLimit; res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); @@ -56,7 +57,12 @@ class KubernetesStatefulSetConverter { payload.spec.template.metadata.labels.app = statefulSet.Name; payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName; payload.spec.template.spec.containers[0].name = statefulSet.Name; - payload.spec.template.spec.containers[0].image = statefulSet.Image; + if (statefulSet.ImageModel.Image) { + payload.spec.template.spec.containers[0].image = buildImageFullURI(statefulSet.ImageModel); + if (statefulSet.ImageModel.Registry && statefulSet.ImageModel.Registry.Authentication) { + payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${statefulSet.ImageModel.Registry.Id}` }]; + } + } payload.spec.template.spec.affinity = statefulSet.Affinity; KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', statefulSet.Env); KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts); diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index a5ed94391..47482ac65 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -1,37 +1,34 @@ +import { PorImageRegistryModel } from '@/docker/models/porImageRegistry'; import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationPlacementTypes } from './models'; /** * KubernetesApplicationFormValues Model */ -const _KubernetesApplicationFormValues = Object.freeze({ - ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation) - ResourcePool: {}, - Name: '', - StackName: '', - ApplicationOwner: '', - Image: '', - Note: '', - MemoryLimit: 0, - CpuLimit: 0, - DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED, - ReplicaCount: 1, - AutoScaler: {}, - Containers: [], - EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list - DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, - PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list - Configurations: [], // KubernetesApplicationConfigurationFormValue list - PublishingType: KubernetesApplicationPublishingTypes.INTERNAL, - PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list - PlacementType: KubernetesApplicationPlacementTypes.PREFERRED, - Placements: [], // KubernetesApplicationPlacementFormValue list - OriginalIngresses: undefined, -}); - -export class KubernetesApplicationFormValues { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationFormValues))); - } +export function KubernetesApplicationFormValues() { + return { + ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation) + ResourcePool: {}, + Name: '', + StackName: '', + ApplicationOwner: '', + ImageModel: new PorImageRegistryModel(), + Note: '', + MemoryLimit: 0, + CpuLimit: 0, + DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED, + ReplicaCount: 1, + AutoScaler: {}, + Containers: [], + EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list + DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, + PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list + Configurations: [], // KubernetesApplicationConfigurationFormValue list + PublishingType: KubernetesApplicationPublishingTypes.INTERNAL, + PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list + PlacementType: KubernetesApplicationPlacementTypes.PREFERRED, + Placements: [], // KubernetesApplicationPlacementFormValue list + OriginalIngresses: undefined, + }; } export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({ diff --git a/app/kubernetes/models/daemon-set/models.js b/app/kubernetes/models/daemon-set/models.js index a4c4fc411..4f2f82ed1 100644 --- a/app/kubernetes/models/daemon-set/models.js +++ b/app/kubernetes/models/daemon-set/models.js @@ -5,7 +5,7 @@ const _KubernetesDaemonSet = Object.freeze({ Namespace: '', Name: '', StackName: '', - Image: '', + ImageModel: null, Env: [], CpuLimit: 0, MemoryLimit: 0, diff --git a/app/kubernetes/models/deployment/models.js b/app/kubernetes/models/deployment/models.js index df8a8d79c..05a6963df 100644 --- a/app/kubernetes/models/deployment/models.js +++ b/app/kubernetes/models/deployment/models.js @@ -6,7 +6,7 @@ const _KubernetesDeployment = Object.freeze({ Name: '', StackName: '', ReplicaCount: 0, - Image: '', + ImageModel: null, Env: [], CpuLimit: 0, MemoryLimit: 0, diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js index bb3f598c0..fca07da3b 100644 --- a/app/kubernetes/models/resource-pool/formValues.js +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -1,10 +1,12 @@ export function KubernetesResourcePoolFormValues(defaults) { return { + EndpointId: 0, Name: '', MemoryLimit: defaults.MemoryLimit, CpuLimit: defaults.CpuLimit, HasQuota: false, IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue + Registries: [], // RegistryViewModel }; } diff --git a/app/kubernetes/models/stateful-set/models.js b/app/kubernetes/models/stateful-set/models.js index 28609416c..fa445799f 100644 --- a/app/kubernetes/models/stateful-set/models.js +++ b/app/kubernetes/models/stateful-set/models.js @@ -6,7 +6,7 @@ const _KubernetesStatefulSet = Object.freeze({ Name: '', StackName: '', ReplicaCount: 0, - Image: '', + ImageModel: null, Env: [], CpuLimit: '', MemoryLimit: '', diff --git a/app/kubernetes/registries/index.js b/app/kubernetes/registries/index.js new file mode 100644 index 000000000..54373821c --- /dev/null +++ b/app/kubernetes/registries/index.js @@ -0,0 +1,5 @@ +import angular from 'angular'; + +import { kubernetesRegistryAccessView } from './kube-registry-access-view'; + +export default angular.module('portainer.kubernetes.registries', []).component('kubernetesRegistryAccessView', kubernetesRegistryAccessView).name; diff --git a/app/kubernetes/registries/kube-registry-access-view/index.js b/app/kubernetes/registries/kube-registry-access-view/index.js new file mode 100644 index 000000000..bf51890e0 --- /dev/null +++ b/app/kubernetes/registries/kube-registry-access-view/index.js @@ -0,0 +1,9 @@ +import controller from './kube-registry-access-view.controller'; + +export const kubernetesRegistryAccessView = { + templateUrl: './kube-registry-access-view.html', + controller, + bindings: { + endpoint: '<', + }, +}; diff --git a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js new file mode 100644 index 000000000..f9c86154f --- /dev/null +++ b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js @@ -0,0 +1,71 @@ +export default class KubernetesRegistryAccessController { + /* @ngInject */ + constructor($async, $state, EndpointService, Notifications, RegistryService, KubernetesResourcePoolService, KubernetesNamespaceHelper) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.RegistryService = RegistryService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.EndpointService = EndpointService; + + this.state = { + actionInProgress: false, + }; + + this.selectedResourcePools = []; + this.resourcePools = []; + this.savedResourcePools = []; + + this.handleRemove = this.handleRemove.bind(this); + } + + async submit() { + return this.updateNamespaces([...this.savedResourcePools.map(({ value }) => value), ...this.selectedResourcePools.map((pool) => pool.name)]); + } + + handleRemove(namespaces) { + const removeNamespaces = namespaces.map(({ value }) => value); + + return this.updateNamespaces(this.savedResourcePools.map(({ value }) => value).filter((value) => !removeNamespaces.includes(value))); + } + + updateNamespaces(namespaces) { + return this.$async(async () => { + try { + await this.EndpointService.updateRegistryAccess(this.endpoint.Id, this.registry.Id, { + namespaces, + }); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Failed saving registry access'); + } + }); + } + + $onInit() { + return this.$async(async () => { + try { + this.state = { + registryId: this.$state.params.id, + }; + this.registry = await this.RegistryService.registry(this.state.registryId, this.endpoint.Id); + if (this.registry.RegistryAccesses && this.registry.RegistryAccesses[this.endpoint.Id]) { + this.savedResourcePools = this.registry.RegistryAccesses[this.endpoint.Id].Namespaces.map((value) => ({ value })); + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registry details'); + } + + try { + const resourcePools = await this.KubernetesResourcePoolService.get(); + + this.resourcePools = resourcePools + .filter((pool) => !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name) && !this.savedResourcePools.find(({ value }) => value === pool.Namespace.Name)) + .map((pool) => ({ name: pool.Namespace.Name, id: pool.Namespace.Id })); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve resource pools'); + } + }); + } +} diff --git a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html new file mode 100644 index 000000000..bd8129b73 --- /dev/null +++ b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html @@ -0,0 +1,72 @@ + + + Registries > {{ $ctrl.registry.Name }} > Access management + + + + +
+
+ + + + +
+ +
+ + No resource pools available. + + + +
+
+ + +
+
+ +
+
+ + +
+
+
+
+
+
+ + +
+
diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index f492c6ef4..49198b6f0 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -5,7 +5,7 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool' import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; /* @ngInject */ -export function KubernetesResourcePoolService($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) { +export function KubernetesResourcePoolService($async, EndpointService, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) { return { get, create, @@ -59,7 +59,7 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService function create(formValues) { return $async(async () => { try { - const [namespace, quota, ingresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues); + const [namespace, quota, ingresses, registries] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues); await KubernetesNamespaceService.create(namespace); if (quota) { @@ -67,6 +67,10 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService } const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i)); await Promise.all(ingressPromises); + + const endpointId = formValues.EndpointId; + const registriesPromises = _.map(registries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId])); + await Promise.all(registriesPromises); } catch (err) { throw err; } @@ -76,8 +80,8 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService function patch(oldFormValues, newFormValues) { return $async(async () => { try { - const [oldNamespace, oldQuota, oldIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues); - const [newNamespace, newQuota, newIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues); + const [oldNamespace, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues); + const [newNamespace, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues); void oldNamespace, newNamespace; if (oldQuota && newQuota) { @@ -103,6 +107,18 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService const promises = _.flatten([createPromises, delPromises, patchPromises]); await Promise.all(promises); + + const endpointId = newFormValues.EndpointId; + const keptRegistries = _.intersectionBy(oldRegistries, newRegistries, 'Id'); + const removedRegistries = _.without(oldRegistries, ...keptRegistries); + + const newRegistriesPromises = _.map(newRegistries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId])); + const removedRegistriesPromises = _.map(removedRegistries, (r) => { + _.pull(r.RegistryAccesses[endpointId].Namespaces, newFormValues.Name); + return EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]); + }); + + await Promise.all(_.concat(newRegistriesPromises, removedRegistriesPromises)); } catch (err) { throw err; } diff --git a/app/kubernetes/services/statefulSetService.js b/app/kubernetes/services/statefulSetService.js index 60cdea44c..5a14c4190 100644 --- a/app/kubernetes/services/statefulSetService.js +++ b/app/kubernetes/services/statefulSetService.js @@ -93,6 +93,7 @@ class KubernetesStatefulSetService { if (!payload.length) { return; } + const data = await this.KubernetesStatefulSets(namespace).patch(params, payload).$promise; return data; } catch (err) { diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 106b2454d..a1f30a874 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -16,6 +16,40 @@
+
+ Resource pool +
+ +
+ +
+ +
+
+
+
+ + This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the + resource pool. +
+
+
+
+ + You do not have access to any resource pool. Contact your administrator to get access to a resource pool. +
+
+ +
+ Application +
@@ -53,17 +87,15 @@
- -
- +
+
@@ -83,32 +115,6 @@ -
- Resource pool -
- -
- -
- -
-
-
-
- - This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the - resource pool. -
-
- -
Stack
diff --git a/app/kubernetes/views/applications/create/createApplication.js b/app/kubernetes/views/applications/create/createApplication.js index 9feaabc76..c5c8df16b 100644 --- a/app/kubernetes/views/applications/create/createApplication.js +++ b/app/kubernetes/views/applications/create/createApplication.js @@ -3,7 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesCreateApplicationVie controller: 'KubernetesCreateApplicationController', controllerAs: 'ctrl', bindings: { - $transition$: '<', endpoint: '<', }, }); diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 0ae3a9c88..2394bc0d5 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -38,7 +38,6 @@ class KubernetesCreateApplicationController { $async, $state, Notifications, - EndpointProvider, Authentication, DockerHubService, ModalService, @@ -50,12 +49,12 @@ class KubernetesCreateApplicationController { KubernetesIngressService, KubernetesPersistentVolumeClaimService, KubernetesNamespaceHelper, - KubernetesVolumeService + KubernetesVolumeService, + RegistryService ) { this.$async = $async; this.$state = $state; this.Notifications = Notifications; - this.EndpointProvider = EndpointProvider; this.Authentication = Authentication; this.DockerHubService = DockerHubService; this.ModalService = ModalService; @@ -68,6 +67,7 @@ class KubernetesCreateApplicationController { this.KubernetesIngressService = KubernetesIngressService; this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.RegistryService = RegistryService; this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; @@ -77,6 +77,56 @@ class KubernetesCreateApplicationController { this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes; this.ServiceTypes = KubernetesServiceTypes; + this.state = { + actionInProgress: false, + useLoadBalancer: false, + useServerMetrics: false, + sliders: { + cpu: { + min: 0, + max: 0, + }, + memory: { + min: 0, + max: 0, + }, + }, + nodes: { + memory: 0, + cpu: 0, + }, + resourcePoolHasQuota: false, + viewReady: false, + availableSizeUnits: ['MB', 'GB', 'TB'], + alreadyExists: false, + duplicates: { + environmentVariables: new KubernetesFormValidationReferences(), + persistedFolders: new KubernetesFormValidationReferences(), + configurationPaths: new KubernetesFormValidationReferences(), + existingVolumes: new KubernetesFormValidationReferences(), + publishedPorts: { + containerPorts: new KubernetesFormValidationReferences(), + nodePorts: new KubernetesFormValidationReferences(), + ingressRoutes: new KubernetesFormValidationReferences(), + loadBalancerPorts: new KubernetesFormValidationReferences(), + }, + placements: new KubernetesFormValidationReferences(), + }, + isEdit: this.$state.params.namespace && this.$state.params.name, + persistedFoldersUseExistingVolumes: false, + pullImageValidity: false, + }; + + this.isAdmin = this.Authentication.isAdmin(); + + this.editChanges = []; + + this.storageClasses = []; + this.state.useLoadBalancer = false; + this.state.useServerMetrics = false; + + this.formValues = new KubernetesApplicationFormValues(); + this.updateApplicationAsync = this.updateApplicationAsync.bind(this); this.deployApplicationAsync = this.deployApplicationAsync.bind(this); this.setPullImageValidity = this.setPullImageValidity.bind(this); @@ -867,9 +917,9 @@ class KubernetesCreateApplicationController { getApplication() { return this.$async(async () => { try { - const namespace = this.state.params.namespace; + const namespace = this.$state.params.namespace; [this.application, this.persistentVolumeClaims] = await Promise.all([ - this.KubernetesApplicationService.get(namespace, this.state.params.name), + this.KubernetesApplicationService.get(namespace, this.$state.params.name), this.KubernetesPersistentVolumeClaimService.get(namespace), ]); } catch (err) { @@ -877,71 +927,26 @@ class KubernetesCreateApplicationController { } }); } + + async parseImageConfiguration(imageModel) { + return this.$async(async () => { + try { + return await this.RegistryService.retrievePorRegistryModelFromRepository(imageModel.Image, this.endpoint.Id, imageModel.Registry.Id); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registry'); + return imageModel; + } + }); + } /* #endregion */ /* #region ON INIT */ $onInit() { return this.$async(async () => { try { - this.state = { - actionInProgress: false, - useLoadBalancer: false, - useServerMetrics: false, - sliders: { - cpu: { - min: 0, - max: 0, - }, - memory: { - min: 0, - max: 0, - }, - }, - nodes: { - memory: 0, - cpu: 0, - }, - resourcePoolHasQuota: false, - viewReady: false, - availableSizeUnits: ['MB', 'GB', 'TB'], - alreadyExists: false, - duplicates: { - environmentVariables: new KubernetesFormValidationReferences(), - persistedFolders: new KubernetesFormValidationReferences(), - configurationPaths: new KubernetesFormValidationReferences(), - existingVolumes: new KubernetesFormValidationReferences(), - publishedPorts: { - containerPorts: new KubernetesFormValidationReferences(), - nodePorts: new KubernetesFormValidationReferences(), - ingressRoutes: new KubernetesFormValidationReferences(), - loadBalancerPorts: new KubernetesFormValidationReferences(), - }, - placements: new KubernetesFormValidationReferences(), - }, - isEdit: false, - params: { - namespace: this.$transition$.params().namespace, - name: this.$transition$.params().name, - }, - persistedFoldersUseExistingVolumes: false, - pullImageValidity: false, - }; - - this.isAdmin = this.Authentication.isAdmin(); - - this.editChanges = []; - - if (this.state.params.namespace && this.state.params.name) { - this.state.isEdit = true; - } - - const endpoint = this.EndpointProvider.currentEndpoint(); - this.endpoint = endpoint; - this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses; - this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer; - this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics; - - this.formValues = new KubernetesApplicationFormValues(); + this.storageClasses = this.endpoint.Kubernetes.Configuration.StorageClasses; + this.state.useLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; + this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; const [resourcePools, nodes, ingresses] = await Promise.all([ this.KubernetesResourcePoolService.get(), @@ -962,7 +967,7 @@ class KubernetesCreateApplicationController { }); this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes); - const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name; + const namespace = this.state.isEdit ? this.$state.params.namespace : this.formValues.ResourcePool.Namespace.Name; await this.refreshNamespaceData(namespace); if (this.state.isEdit) { @@ -975,6 +980,7 @@ class KubernetesCreateApplicationController { this.nodesLabels ); this.formValues.OriginalIngresses = this.filteredIngresses; + this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel); this.savedFormValues = angular.copy(this.formValues); delete this.formValues.ApplicationType; @@ -992,7 +998,6 @@ class KubernetesCreateApplicationController { this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount); this.formValues.OriginalIngressClasses = angular.copy(this.ingresses); } - this.updateSliders(); const dockerHub = await this.DockerHubService.dockerhub(); diff --git a/app/kubernetes/views/configurations/edit/configurationController.js b/app/kubernetes/views/configurations/edit/configurationController.js index 57928dbb1..a84fd0b85 100644 --- a/app/kubernetes/views/configurations/edit/configurationController.js +++ b/app/kubernetes/views/configurations/edit/configurationController.js @@ -134,6 +134,10 @@ class KubernetesConfigurationController { const name = this.$transition$.params().name; const namespace = this.$transition$.params().namespace; const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]); + if (secret.status === 'rejected' && secret.reason.err.status === 403) { + this.$state.go('kubernetes.configurations'); + throw new Error('Not authorized to edit secret'); + } if (secret.status === 'fulfilled') { this.configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value); this.formValues.Data = secret.value.Data; @@ -146,6 +150,8 @@ class KubernetesConfigurationController { this.formValues.Name = this.configuration.Name; this.formValues.Type = this.configuration.Type; this.oldDataYaml = this.formValues.DataYaml; + + return this.configuration; } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve configuration'); } finally { @@ -251,11 +257,12 @@ class KubernetesConfigurationController { this.formValues = new KubernetesConfigurationFormValues(); this.resourcePools = await this.KubernetesResourcePoolService.get(); - await this.getConfiguration(); - await this.getApplications(this.configuration.Namespace); - await this.getEvents(this.configuration.Namespace); - await this.getConfigurations(); - + const configuration = await this.getConfiguration(); + if (configuration) { + await this.getApplications(this.configuration.Namespace); + await this.getEvents(this.configuration.Namespace); + await this.getConfigurations(); + } this.tagUsedDataKeys(); } catch (err) { this.Notifications.error('Failure', err, 'Unable to load view data'); diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 6cef0781d..70b201ef7 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -9,18 +9,10 @@ import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; class KubernetesConfigureController { /* #region CONSTRUCTOR */ - // TODO: technical debt - // $transition$ cannot be injected as bindings: { $transition$: '<' } inside app/portainer/__module.js - // because this view is not using a component (https://ui-router.github.io/guide/ng1/route-to-component#accessing-transition) - // and will cause - // >> Error: Cannot combine: component|bindings|componentProvider - // >> with: templateProvider|templateUrl|template|notify|async|controller|controllerProvider|controllerAs|resolveAs - // >> in stateview: 'content@@portainer.endpoints.endpoint.kubernetesConfig' /* @ngInject */ constructor( $async, $state, - $transition$, Notifications, KubernetesStorageService, EndpointService, @@ -33,7 +25,6 @@ class KubernetesConfigureController { ) { this.$async = $async; this.$state = $state; - this.$transition$ = $transition$; this.Notifications = Notifications; this.KubernetesStorageService = KubernetesStorageService; this.EndpointService = EndpointService; @@ -163,7 +154,7 @@ class KubernetesConfigureController { } }); } - + enableMetricsServer() { if (this.formValues.UseServerMetrics) { this.state.metrics.userClick = true; @@ -241,7 +232,7 @@ class KubernetesConfigureController { actionInProgress: false, displayConfigureClassPanel: {}, viewReady: false, - endpointId: this.$transition$.params().id, + endpointId: this.$state.params.id, duplicates: { ingressClasses: new KubernetesFormValidationReferences(), }, diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html index 95d7bb94e..9bba866ac 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePool.html +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -1,10 +1,10 @@ - + Resource pools > Create a resource pool - + -
+
@@ -18,16 +18,16 @@ type="text" class="form-control" name="pool_name" - ng-model="ctrl.formValues.Name" + ng-model="$ctrl.formValues.Name" ng-pattern="/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/" - ng-change="ctrl.onChangeName()" + ng-change="$ctrl.onChangeName()" placeholder="my-project" required auto-focus />
-
+

This field is required.

@@ -36,7 +36,7 @@ with an alphanumeric character.

-

A resource pool with the same name already exists.

+

A resource pool with the same name already exists.

@@ -58,16 +58,16 @@ - +
-
+

At least a single limit must be set for the quota to be valid.

-
+
Resource limits
@@ -78,17 +78,23 @@ Memory
- +
@@ -103,7 +109,7 @@

Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }} + > Value must be between {{ $ctrl.defaults.MemoryLimit }} and {{ $ctrl.state.sliderMaxMemory }}

@@ -115,7 +121,14 @@ CPU
- +
@@ -186,20 +199,20 @@
-
+
Ingresses
-
+
The ingress feature must be enabled in the - endpoint configuration view to be able to register ingresses inside this + endpoint configuration view to be able to register ingresses inside this resource pool.
-
+

@@ -208,7 +221,7 @@

-
+
{{ ic.IngressClass.Name }} @@ -234,7 +247,7 @@ > - + add hostname
@@ -248,13 +261,13 @@ class="form-control" name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}" ng-model="item.Host" - ng-change="ctrl.onChangeIngressHostname()" + ng-change="$ctrl.onChangeIngressHostname()" placeholder="foo" required />
-
@@ -264,20 +277,20 @@ style="margin-top: 5px;" ng-show=" resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid || - ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined + $ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined " >

Hostname is required.

-

+

This hostname is already used.

-
+
-
@@ -338,6 +351,47 @@
+ +
+ Registries +
+
+
+

+ + Define which registry can be used by users who have access to this resource pool. +

+
+
+ +
+ +
+ + No registries available. Head over registry view to define container registry. + + + No registries available. Contact your administrator to create a container registry. + + + +
+
+ +
Actions
@@ -347,12 +401,12 @@
diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.js b/app/kubernetes/views/resource-pools/create/createResourcePool.js index daf67bd9c..a192da090 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePool.js +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.js @@ -1,5 +1,10 @@ +import angular from 'angular'; +import KubernetesCreateResourcePoolController from './createResourcePoolController'; + angular.module('portainer.kubernetes').component('kubernetesCreateResourcePoolView', { templateUrl: './createResourcePool.html', - controller: 'KubernetesCreateResourcePoolController', - controllerAs: 'ctrl', + controller: KubernetesCreateResourcePoolController, + bindings: { + endpoint: '<', + }, }); diff --git a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js index 4a823dcd2..a36033e86 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js +++ b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js @@ -1,4 +1,3 @@ -import angular from 'angular'; import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; @@ -16,23 +15,20 @@ import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; class KubernetesCreateResourcePoolController { /* #region CONSTRUCTOR */ /* @ngInject */ - constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointProvider) { - this.$async = $async; - this.$state = $state; - this.Notifications = Notifications; - this.Authentication = Authentication; - this.EndpointProvider = EndpointProvider; - - this.KubernetesNodeService = KubernetesNodeService; - this.KubernetesResourcePoolService = KubernetesResourcePoolService; - this.KubernetesIngressService = KubernetesIngressService; + constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointProvider, RegistryService) { + Object.assign(this, { + $async, + $state, + Notifications, + KubernetesNodeService, + KubernetesResourcePoolService, + KubernetesIngressService, + Authentication, + EndpointProvider, + RegistryService, + }); this.IngressClassTypes = KubernetesIngressClassTypes; - - this.onInit = this.onInit.bind(this); - this.createResourcePoolAsync = this.createResourcePoolAsync.bind(this); - this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this); - this.getIngressesAsync = this.getIngressesAsync.bind(this); } /* #endregion */ @@ -116,105 +112,111 @@ class KubernetesCreateResourcePoolController { } /* #region CREATE RESOURCE POOL */ - async createResourcePoolAsync() { - this.state.actionInProgress = true; - try { - this.checkDefaults(); - const owner = this.Authentication.getUserDetails().username; - this.formValues.Owner = owner; - await this.KubernetesResourcePoolService.create(this.formValues); - this.Notifications.success('Resource pool successfully created', this.formValues.Name); - this.$state.go('kubernetes.resourcePools'); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to create resource pool'); - } finally { - this.state.actionInProgress = false; - } - } - createResourcePool() { - return this.$async(this.createResourcePoolAsync); + return this.$async(async () => { + this.state.actionInProgress = true; + try { + this.checkDefaults(); + this.formValues.Owner = this.Authentication.getUserDetails().username; + await this.KubernetesResourcePoolService.create(this.formValues); + this.Notifications.success('Resource pool successfully created', this.formValues.Name); + this.$state.go('kubernetes.resourcePools'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create resource pool'); + } finally { + this.state.actionInProgress = false; + } + }); } /* #endregion */ /* #region GET INGRESSES */ - async getIngressesAsync() { - try { - this.allIngresses = await this.KubernetesIngressService.get(); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.'); - } - } - getIngresses() { - return this.$async(this.getIngressesAsync); + return this.$async(async () => { + try { + this.allIngresses = await this.KubernetesIngressService.get(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.'); + } + }); } /* #endregion */ /* #region GET RESOURCE POOLS */ - async getResourcePoolsAsync() { - try { - this.resourcePools = await this.KubernetesResourcePoolService.get(); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve resource pools'); - } - } - getResourcePools() { - return this.$async(this.getResourcePoolsAsync); + return this.$async(async () => { + try { + this.resourcePools = await this.KubernetesResourcePoolService.get(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve resource pools'); + } + }); + } + /* #endregion */ + + /* #region GET REGISTRIES */ + getRegistries() { + return this.$async(async () => { + try { + this.registries = await this.RegistryService.registries(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registries'); + } + }); } /* #endregion */ /* #region ON INIT */ - async onInit() { - try { - const endpoint = this.EndpointProvider.currentEndpoint(); - this.endpoint = endpoint; - this.defaults = KubernetesResourceQuotaDefaults; - this.formValues = new KubernetesResourcePoolFormValues(this.defaults); - - this.state = { - actionInProgress: false, - sliderMaxMemory: 0, - sliderMaxCpu: 0, - viewReady: false, - isAlreadyExist: false, - canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length, - duplicates: { - ingressHosts: new KubernetesFormValidationReferences(), - }, - }; - - const nodes = await this.KubernetesNodeService.get(); - - _.forEach(nodes, (item) => { - this.state.sliderMaxMemory += filesizeParser(item.Memory); - this.state.sliderMaxCpu += item.CPU; - }); - this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory); - await this.getResourcePools(); - if (this.state.canUseIngress) { - await this.getIngresses(); - const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses; - this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses); - } - _.forEach(this.formValues.IngressClasses, (ic) => { - if (ic.Hosts.length === 0) { - ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue()); - } - }); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to load view data'); - } finally { - this.state.viewReady = true; - } - } - $onInit() { - return this.$async(this.onInit); + return this.$async(async () => { + try { + const endpoint = this.EndpointProvider.currentEndpoint(); + this.endpoint = endpoint; + this.defaults = KubernetesResourceQuotaDefaults; + this.formValues = new KubernetesResourcePoolFormValues(this.defaults); + this.formValues.EndpointId = this.endpoint.Id; + + this.state = { + actionInProgress: false, + sliderMaxMemory: 0, + sliderMaxCpu: 0, + viewReady: false, + isAlreadyExist: false, + canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length, + duplicates: { + ingressHosts: new KubernetesFormValidationReferences(), + }, + isAdmin: this.Authentication.isAdmin(), + }; + + const nodes = await this.KubernetesNodeService.get(); + + _.forEach(nodes, (item) => { + this.state.sliderMaxMemory += filesizeParser(item.Memory); + this.state.sliderMaxCpu += item.CPU; + }); + this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory); + await this.getResourcePools(); + if (this.state.canUseIngress) { + await this.getIngresses(); + const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses; + this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses); + } + _.forEach(this.formValues.IngressClasses, (ic) => { + if (ic.Hosts.length === 0) { + ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue()); + } + }); + + await this.getRegistries(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + }); } /* #endregion */ } export default KubernetesCreateResourcePoolController; -angular.module('portainer.kubernetes').controller('KubernetesCreateResourcePoolController', KubernetesCreateResourcePoolController); diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html index ec1337013..595d2996a 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.html +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -298,6 +298,50 @@
+ + +
+
+ Registries +
+
+
+

+ + Define which registry can be used by users who have access to this resource pool. +

+
+
+ +
+ +
+ + No registries available. Head over registry view to define container registry. + + + No registries available. Contact your administrator to create a container registry. + + + +
+
+
+ +
Storages diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.js b/app/kubernetes/views/resource-pools/edit/resourcePool.js index 3b6012a2d..d612e9f6a 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.js @@ -3,6 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolView', { controller: 'KubernetesResourcePoolController', controllerAs: 'ctrl', bindings: { - $transition$: '<', + endpoint: '<', }, }); diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index 051dc1fc9..e996cf3b9 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -1,7 +1,7 @@ import angular from 'angular'; import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; -import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; +import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import { @@ -26,6 +26,7 @@ class KubernetesResourcePoolController { LocalStorage, EndpointProvider, ModalService, + RegistryService, KubernetesNodeService, KubernetesResourceQuotaService, KubernetesResourcePoolService, @@ -36,33 +37,31 @@ class KubernetesResourcePoolController { KubernetesIngressService, KubernetesVolumeService ) { - this.$async = $async; - this.$state = $state; - this.Notifications = Notifications; - this.Authentication = Authentication; - this.LocalStorage = LocalStorage; - this.EndpointProvider = EndpointProvider; - this.ModalService = ModalService; - - this.KubernetesNodeService = KubernetesNodeService; - this.KubernetesResourceQuotaService = KubernetesResourceQuotaService; - this.KubernetesResourcePoolService = KubernetesResourcePoolService; - this.KubernetesEventService = KubernetesEventService; - this.KubernetesPodService = KubernetesPodService; - this.KubernetesApplicationService = KubernetesApplicationService; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; - this.KubernetesIngressService = KubernetesIngressService; - this.KubernetesVolumeService = KubernetesVolumeService; + Object.assign(this, { + $async, + $state, + Authentication, + Notifications, + LocalStorage, + EndpointProvider, + ModalService, + RegistryService, + KubernetesNodeService, + KubernetesResourceQuotaService, + KubernetesResourcePoolService, + KubernetesEventService, + KubernetesPodService, + KubernetesApplicationService, + KubernetesNamespaceHelper, + KubernetesIngressService, + KubernetesVolumeService, + }); this.IngressClassTypes = KubernetesIngressClassTypes; this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults; - this.onInit = this.onInit.bind(this); - this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this); this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this); this.getEvents = this.getEvents.bind(this); - this.getApplications = this.getApplications.bind(this); - this.getIngresses = this.getIngresses.bind(this); } /* #endregion */ @@ -159,15 +158,6 @@ class KubernetesResourcePoolController { this.selectTab(2); } - async createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit) { - const quota = new KubernetesResourceQuota(namespace); - quota.CpuLimit = cpuLimit; - quota.MemoryLimit = memoryLimit; - quota.ResourcePoolName = namespace; - quota.ResourcePoolOwner = owner; - await this.KubernetesResourceQuotaService.create(quota); - } - hasResourceQuotaBeenReduced() { if (this.formValues.HasQuota && this.oldQuota) { const cpuLimit = this.formValues.CpuLimit; @@ -285,88 +275,111 @@ class KubernetesResourcePoolController { } /* #endregion */ + /* #region GET REGISTRIES */ + getRegistries() { + return this.$async(async () => { + try { + this.registries = await this.RegistryService.registries(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registries'); + } + }); + } + /* #endregion */ + /* #region ON INIT */ - async onInit() { - try { - const endpoint = this.EndpointProvider.currentEndpoint(); - this.endpoint = endpoint; - this.isAdmin = this.Authentication.isAdmin(); + $onInit() { + return this.$async(async () => { + try { + const endpoint = this.EndpointProvider.currentEndpoint(); + this.endpoint = endpoint; + this.isAdmin = this.Authentication.isAdmin(); - this.state = { - actionInProgress: false, - sliderMaxMemory: 0, - sliderMaxCpu: 0, - cpuUsage: 0, - cpuUsed: 0, - memoryUsage: 0, - memoryUsed: 0, - activeTab: 0, - currentName: this.$state.$current.name, - showEditorTab: false, - eventsLoading: true, - applicationsLoading: true, - ingressesLoading: true, - viewReady: false, - eventWarningCount: 0, - canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length, - duplicates: { - ingressHosts: new KubernetesFormValidationReferences(), - }, - }; + this.state = { + actionInProgress: false, + sliderMaxMemory: 0, + sliderMaxCpu: 0, + cpuUsage: 0, + cpuUsed: 0, + memoryUsage: 0, + memoryUsed: 0, + activeTab: 0, + currentName: this.$state.$current.name, + showEditorTab: false, + eventsLoading: true, + applicationsLoading: true, + ingressesLoading: true, + viewReady: false, + eventWarningCount: 0, + canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length, + duplicates: { + ingressHosts: new KubernetesFormValidationReferences(), + }, + }; - this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool'); + this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool'); - const name = this.$transition$.params().id; + const name = this.$state.params.id; - const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]); + const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]); - this.pool = _.find(pools, { Namespace: { Name: name } }); - this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults); - this.formValues.Name = this.pool.Namespace.Name; + this.pool = _.find(pools, { Namespace: { Name: name } }); + this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults); + this.formValues.Name = this.pool.Namespace.Name; + this.formValues.EndpointId = this.endpoint.Id; - _.forEach(nodes, (item) => { - this.state.sliderMaxMemory += filesizeParser(item.Memory); - this.state.sliderMaxCpu += item.CPU; - }); - this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory); + _.forEach(nodes, (item) => { + this.state.sliderMaxMemory += filesizeParser(item.Memory); + this.state.sliderMaxCpu += item.CPU; + }); + this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory); - const quota = this.pool.Quota; - if (quota) { - this.oldQuota = angular.copy(quota); - this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota); - this.state.cpuUsed = quota.CpuLimitUsed; - this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed); - } + const quota = this.pool.Quota; + if (quota) { + this.oldQuota = angular.copy(quota); + this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota); + this.formValues.EndpointId = this.endpoint.Id; - this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name); - if (this.pool.Namespace.Name === 'default') { - this.isEditable = false; - } + this.state.cpuUsed = quota.CpuLimitUsed; + this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed); + } - await this.getEvents(); - await this.getApplications(); + this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name); + if (this.pool.Namespace.Name === 'default') { + this.isEditable = false; + } - if (this.state.canUseIngress) { - await this.getIngresses(); - const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses; - this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses); - _.forEach(this.formValues.IngressClasses, (ic) => { - if (ic.Hosts.length === 0) { - ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue()); + await this.getEvents(); + await this.getApplications(); + + if (this.state.canUseIngress) { + await this.getIngresses(); + const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses; + this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses); + _.forEach(this.formValues.IngressClasses, (ic) => { + if (ic.Hosts.length === 0) { + ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue()); + } + }); + } + + await this.getRegistries(); + _.forEach(this.registries, (reg) => { + if (reg.RegistryAccesses && reg.RegistryAccesses[this.endpoint.Id] && reg.RegistryAccesses[this.endpoint.Id].Namespaces.includes(name)) { + reg.Checked = true; + this.formValues.Registries.push(reg); } }); + + this.savedFormValues = angular.copy(this.formValues); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; } - this.savedFormValues = angular.copy(this.formValues); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to load view data'); - } finally { - this.state.viewReady = true; - } + }); } - $onInit() { - return this.$async(this.onInit); - } /* #endregion */ $onDestroy() { diff --git a/app/portainer/__module.js b/app/portainer/__module.js index b8f01310c..a6f501325 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -293,24 +293,12 @@ angular.module('portainer.app', ['portainer.oauth']).config([ }, }; - var registryCreation = { + const registryCreation = { name: 'portainer.registries.new', url: '/new', views: { 'content@': { - templateUrl: './views/registries/create/createregistry.html', - controller: 'CreateRegistryController', - }, - }, - }; - - var registryAccess = { - name: 'portainer.registries.registry.access', - url: '/access', - views: { - 'content@': { - templateUrl: './views/registries/access/registryAccess.html', - controller: 'RegistryAccessController', + component: 'createRegistry', }, }, }; @@ -423,7 +411,6 @@ angular.module('portainer.app', ['portainer.oauth']).config([ $stateRegistryProvider.register(initAdmin); $stateRegistryProvider.register(registries); $stateRegistryProvider.register(registry); - $stateRegistryProvider.register(registryAccess); $stateRegistryProvider.register(registryCreation); $stateRegistryProvider.register(settings); $stateRegistryProvider.register(settingsAuthentication); diff --git a/app/portainer/components/accessManagement/por-access-management.js b/app/portainer/components/accessManagement/por-access-management.js index 491f795c0..c2df140ff 100644 --- a/app/portainer/components/accessManagement/por-access-management.js +++ b/app/portainer/components/accessManagement/por-access-management.js @@ -3,6 +3,7 @@ angular.module('portainer.app').component('porAccessManagement', { controller: 'porAccessManagementController', controllerAs: 'ctrl', bindings: { + endpoint: '<', accessControlledEntity: '<', inheritFrom: '<', entityType: '@', diff --git a/app/portainer/components/accessManagement/porAccessManagementController.js b/app/portainer/components/accessManagement/porAccessManagementController.js index 460c302d9..93e8ed5a6 100644 --- a/app/portainer/components/accessManagement/porAccessManagementController.js +++ b/app/portainer/components/accessManagement/porAccessManagementController.js @@ -1,6 +1,7 @@ import _ from 'lodash-es'; import angular from 'angular'; +import { TeamAccessViewModel, UserAccessViewModel } from 'Portainer/models/access'; class PorAccessManagementController { /* @ngInject */ @@ -55,6 +56,20 @@ class PorAccessManagementController { const parent = this.inheritFrom; const data = await this.AccessService.accesses(entity, parent, this.roles); + if (this.entityType === 'registry' && this.endpoint) { + const endpointUsers = this.endpoint.UserAccessPolicies; + const endpointTeams = this.endpoint.TeamAccessPolicies; + data.availableUsersAndTeams = _.filter(data.availableUsersAndTeams, (userOrTeam) => { + const userRole = userOrTeam instanceof UserAccessViewModel && endpointUsers[userOrTeam.Id]; + const teamRole = userOrTeam instanceof TeamAccessViewModel && endpointTeams[userOrTeam.Id]; + if (!userRole && !teamRole) { + return false; + } + const roleId = (userRole && userRole.RoleId) || (teamRole && teamRole.RoleId); + const role = _.find(this.roles, { Id: roleId }); + return role && (role.Authorizations['DockerImageCreate'] || role.Authorizations['DockerImagePush']) && !role.Authorizations['EndpointResourcesAccess']; + }); + } this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc'); this.authorizedUsersAndTeams = data.authorizedUsersAndTeams; } catch (err) { diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html index 697e7fd47..ce7ff1503 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html @@ -4,8 +4,14 @@
{{ $ctrl.titleText }}
-
- @@ -27,7 +33,7 @@ - + @@ -53,22 +59,29 @@ ng-class="{ active: item.Checked }" > - - + + - {{ item.Name }} - {{ item.Name }} + {{ item.Name }} + {{ item.Name }} authentication-enabled {{ item.URL }} - Manage access - + Manage access + Browse + - diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js index f4df3c5a8..e5acb6907 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js @@ -1,6 +1,6 @@ angular.module('portainer.app').component('registriesDatatable', { templateUrl: './registriesDatatable.html', - controller: 'GenericDatatableController', + controller: 'RegistriesDatatableController', bindings: { titleText: '@', titleIcon: '@', @@ -8,8 +8,9 @@ angular.module('portainer.app').component('registriesDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - accessManagement: '<', removeAction: '<', canBrowse: '<', + endpointType: '<', + canManageAccess: '<', }, }); diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js b/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js new file mode 100644 index 000000000..1b2f86e35 --- /dev/null +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js @@ -0,0 +1,85 @@ +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; + +angular.module('portainer.docker').controller('RegistriesDatatableController', RegistriesDatatableController); + +/* @ngInject */ +function RegistriesDatatableController($scope, $controller, $state, Authentication, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + this.allowSelection = function (item) { + return item.Id; + }; + + this.enableGoToLink = (item) => { + return this.isAdmin && item.Id && !this.endpointType; + }; + + this.goToRegistry = function (item) { + if ( + this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment || + this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment || + this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { + $state.go('kubernetes.registries.registry', { id: item.Id }); + } else if ( + this.endpointType === PortainerEndpointTypes.DockerEnvironment || + this.endpointType === PortainerEndpointTypes.AgentOnDockerEnvironment || + this.endpointType === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment + ) { + $state.go('docker.registries.registry', { id: item.Id }); + } else { + $state.go('portainer.registries.registry', { id: item.Id }); + } + }; + + this.redirectToManageAccess = function (item) { + if ( + this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment || + this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment || + this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { + $state.go('kubernetes.registries.access', { id: item.Id }); + } else { + $state.go('docker.registries.access', { id: item.Id }); + } + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + + var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey); + if (storedColumnVisibility !== null) { + this.columnVisibility = storedColumnVisibility; + } + }; +} diff --git a/app/portainer/components/datatables/strings-datatable/index.js b/app/portainer/components/datatables/strings-datatable/index.js new file mode 100644 index 000000000..dda9fa5fd --- /dev/null +++ b/app/portainer/components/datatables/strings-datatable/index.js @@ -0,0 +1,20 @@ +import angular from 'angular'; +// import controller from './strings-datatable.controller.js'; + +export const stringsDatatable = { + templateUrl: './strings-datatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + emptyDatasetMessage: '@', + + columnHeader: '@', + tableKey: '@', + + onRemove: '<', + }, +}; + +angular.module('portainer.app').component('stringsDatatable', stringsDatatable); diff --git a/app/portainer/components/datatables/strings-datatable/strings-datatable.html b/app/portainer/components/datatables/strings-datatable/strings-datatable.html new file mode 100644 index 000000000..0b6faf0ec --- /dev/null +++ b/app/portainer/components/datatables/strings-datatable/strings-datatable.html @@ -0,0 +1,65 @@ +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + + +
+ + + + + + {{ $ctrl.columnHeader }} + + + +
+ + + + + {{ item.value }} +
{{ $ctrl.emptyDatasetMessage }}
+
+
+
+
diff --git a/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html new file mode 100644 index 000000000..ddf26592e --- /dev/null +++ b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html @@ -0,0 +1,64 @@ + +
+ DockerHub account details +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + + +
+ Actions +
+
+
+ +
+
+ + diff --git a/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js new file mode 100644 index 000000000..adf60fcd5 --- /dev/null +++ b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js @@ -0,0 +1,9 @@ +angular.module('portainer.app').component('registryFormDockerhub', { + templateUrl: './registry-form-dockerhub.html', + bindings: { + model: '=', + formAction: '<', + formActionLabel: '@', + actionInProgress: '<', + }, +}); diff --git a/app/portainer/components/forms/template-form/template-form.js b/app/portainer/components/forms/template-form/template-form.js deleted file mode 100644 index 8aacd5839..000000000 --- a/app/portainer/components/forms/template-form/template-form.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('portainer.app').component('templateForm', { - templateUrl: './templateForm.html', - controller: 'TemplateFormController', - bindings: { - model: '=', - categories: '<', - networks: '<', - formAction: '<', - formActionLabel: '@', - actionInProgress: '<', - showTypeSelector: '<', - }, -}); diff --git a/app/portainer/components/forms/template-form/templateForm.html b/app/portainer/components/forms/template-form/templateForm.html deleted file mode 100644 index 12bff3e7b..000000000 --- a/app/portainer/components/forms/template-form/templateForm.html +++ /dev/null @@ -1,580 +0,0 @@ -
- -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- - -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- -
- Template - - expand - collapse - -
- -
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- - {{ $item }} - - {{ category }} - - -
-
- - -
-
- - -
-
- -
- -
-
- Stack - - expand - collapse - -
- -
- -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- - -
- -
- -
-
- -
- -
-
-
- Container - - expand - collapse - -
- -
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
-
- - - map additional port - -
-
- Portainer will automatically assign a port if you leave the host port empty. -
- -
-
-
- -
- host - -
- - - - - -
- container - -
- - -
-
- - -
- -
- -
-
-
- -
- - -
-
- - - map additional volume - -
-
- Portainer will automatically create and map a local volume when using the auto option. -
-
-
- -
- -
- container - -
- - -
-
- - -
- -
- -
- - -
- - -
- host - -
- - -
-
- - -
-
- -
- -
-
-
- - -
-
- - - add label - -
- -
-
-
-
- name - -
-
- value - -
- -
-
-
- -
- - -
- -
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -
-
- Environment - - expand - collapse - -
- -
- -
-
- - - add variable - -
- -
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- -
- -
-
- -
-
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
- -
-
-
-
- - - add allowed value - -
- -
-
-
-
- name - -
-
- value - -
-
- - -
-
-
-
- -
-
-
-
-
-
- -
- -
- - -
- Actions -
-
-
- -
-
- -
diff --git a/app/portainer/components/forms/template-form/templateFormController.js b/app/portainer/components/forms/template-form/templateFormController.js deleted file mode 100644 index c03c11b60..000000000 --- a/app/portainer/components/forms/template-form/templateFormController.js +++ /dev/null @@ -1,55 +0,0 @@ -angular.module('portainer.app').controller('TemplateFormController', [ - function () { - this.state = { - collapseTemplate: false, - collapseContainer: false, - collapseStack: false, - collapseEnv: false, - }; - - this.addPortBinding = function () { - this.model.Ports.push({ containerPort: '', protocol: 'tcp' }); - }; - - this.removePortBinding = function (index) { - this.model.Ports.splice(index, 1); - }; - - this.addVolume = function () { - this.model.Volumes.push({ container: '', bind: '', readonly: false, type: 'auto' }); - }; - - this.removeVolume = function (index) { - this.model.Volumes.splice(index, 1); - }; - - this.addLabel = function () { - this.model.Labels.push({ name: '', value: '' }); - }; - - this.removeLabel = function (index) { - this.model.Labels.splice(index, 1); - }; - - this.addEnvVar = function () { - this.model.Env.push({ type: 1, name: '', label: '', description: '', default: '', preset: true, select: [] }); - }; - - this.removeEnvVar = function (index) { - this.model.Env.splice(index, 1); - }; - - this.addEnvVarValue = function (env) { - env.select = env.select || []; - env.select.push({ name: '', value: '' }); - }; - - this.removeEnvVarValue = function (env, index) { - env.select.splice(index, 1); - }; - - this.changeEnvVarType = function (env) { - env.preset = env.type === 1; - }; - }, -]); diff --git a/app/portainer/components/registry-details/index.js b/app/portainer/components/registry-details/index.js new file mode 100644 index 000000000..418c48781 --- /dev/null +++ b/app/portainer/components/registry-details/index.js @@ -0,0 +1,10 @@ +import angular from 'angular'; + +export const registryDetails = { + templateUrl: './registry-details.html', + bindings: { + registry: '<', + }, +}; + +angular.module('portainer.app').component('registryDetails', registryDetails); diff --git a/app/portainer/components/registry-details/registry-details.html b/app/portainer/components/registry-details/registry-details.html new file mode 100644 index 000000000..b67e69b03 --- /dev/null +++ b/app/portainer/components/registry-details/registry-details.html @@ -0,0 +1,25 @@ +
+
+ + + + + + + + + + + + + + +
Name + {{ $ctrl.registry.Name }} +
URL + {{ $ctrl.registry.URL }} +
+
+
+
+
diff --git a/app/portainer/models/dockerhub.js b/app/portainer/models/dockerhub.js index 880a89f5b..9e77e0b8d 100644 --- a/app/portainer/models/dockerhub.js +++ b/app/portainer/models/dockerhub.js @@ -1,7 +1,7 @@ -export function DockerHubViewModel(data) { - this.Name = 'DockerHub'; - this.URL = ''; - this.Authentication = data.Authentication; - this.Username = data.Username; - this.Password = data.Password; +import { RegistryTypes } from './registryTypes'; + +export function DockerHubViewModel() { + this.Type = RegistryTypes.ANONYMOUS; + this.Name = 'DockerHub (anonymous)'; + this.URL = 'docker.io'; } diff --git a/app/portainer/models/registry.js b/app/portainer/models/registry.js index 72808e3b9..f9956bb8f 100644 --- a/app/portainer/models/registry.js +++ b/app/portainer/models/registry.js @@ -9,10 +9,7 @@ export function RegistryViewModel(data) { this.Authentication = data.Authentication; this.Username = data.Username; this.Password = data.Password; - this.AuthorizedUsers = data.AuthorizedUsers; - this.AuthorizedTeams = data.AuthorizedTeams; - this.UserAccessPolicies = data.UserAccessPolicies; - this.TeamAccessPolicies = data.TeamAccessPolicies; + this.RegistryAccesses = data.RegistryAccesses; // map[EndpointID]{UserAccessPolicies, TeamAccessPolicies, NamespaceAccessPolicies} this.Checked = false; this.Gitlab = data.Gitlab; this.Quay = data.Quay; @@ -39,7 +36,7 @@ export function RegistryManagementConfigurationDefaultModel(registry) { } } -export function RegistryDefaultModel() { +export function RegistryCreateFormValues() { this.Type = RegistryTypes.CUSTOM; this.URL = ''; this.Name = ''; diff --git a/app/portainer/models/registryTypes.js b/app/portainer/models/registryTypes.js index 7b0dc229b..b63dc47e2 100644 --- a/app/portainer/models/registryTypes.js +++ b/app/portainer/models/registryTypes.js @@ -1,6 +1,8 @@ export const RegistryTypes = Object.freeze({ + ANONYMOUS: 0, // not backend replicated, only for frontend QUAY: 1, AZURE: 2, CUSTOM: 3, GITLAB: 4, + DOCKERHUB: 5, }); diff --git a/app/portainer/rest/dockerhub.js b/app/portainer/rest/dockerhub.js deleted file mode 100644 index 35b891076..000000000 --- a/app/portainer/rest/dockerhub.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module('portainer.app').factory('DockerHub', [ - '$resource', - 'API_ENDPOINT_DOCKERHUB', - function DockerHubFactory($resource, API_ENDPOINT_DOCKERHUB) { - 'use strict'; - return $resource( - API_ENDPOINT_DOCKERHUB, - {}, - { - get: { method: 'GET' }, - update: { method: 'PUT' }, - } - ); - }, -]); diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index d88e4e243..23e23fed1 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -23,6 +23,8 @@ angular.module('portainer.app').factory('Endpoints', [ status: { method: 'GET', params: { id: '@id', action: 'status' } }, updateSecuritySettings: { method: 'PUT', params: { id: '@id', action: 'settings' } }, dockerhubLimits: { method: 'GET', params: { id: '@id', action: 'dockerhub' } }, + registries: { url: `${API_ENDPOINT_ENDPOINTS}/:id/registries`, method: 'GET', params: { id: '@id', namespace: '@namespace' }, isArray: true }, + updateRegistryAccess: { url: `${API_ENDPOINT_ENDPOINTS}/:id/registries/:registryId`, method: 'PUT', params: { id: '@id', registryId: '@registryId' } }, } ); }, diff --git a/app/portainer/rest/registry.js b/app/portainer/rest/registry.js index 345ee6d5b..706c56ae1 100644 --- a/app/portainer/rest/registry.js +++ b/app/portainer/rest/registry.js @@ -9,9 +9,8 @@ angular.module('portainer.app').factory('Registries', [ { create: { method: 'POST', ignoreLoadingBar: true }, query: { method: 'GET', isArray: true }, - get: { method: 'GET', params: { id: '@id' } }, + get: { method: 'GET', params: { id: '@id', action: '', endpointId: '@endpointId' } }, update: { method: 'PUT', params: { id: '@id' } }, - updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, remove: { method: 'DELETE', params: { id: '@id' } }, configure: { method: 'POST', params: { id: '@id', action: 'configure' } }, } diff --git a/app/portainer/services/api/accessService.js b/app/portainer/services/api/accessService.js index d09f4bdfe..40018afd3 100644 --- a/app/portainer/services/api/accessService.js +++ b/app/portainer/services/api/accessService.js @@ -9,7 +9,10 @@ angular.module('portainer.app').factory('AccessService', [ 'TeamService', function AccessServiceFactory($q, $async, UserService, TeamService) { 'use strict'; - var service = {}; + return { + accesses, + generateAccessPolicies, + }; function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies) { var availableAccesses = []; @@ -76,7 +79,7 @@ angular.module('portainer.app').factory('AccessService', [ async function accessesAsync(entity, parent) { try { if (!entity) { - throw { msg: 'Unable to retrieve accesses' }; + throw new Error('Unable to retrieve accesses'); } if (!entity.UserAccessPolicies) { entity.UserAccessPolicies = {}; @@ -100,9 +103,7 @@ angular.module('portainer.app').factory('AccessService', [ return $async(accessesAsync, entity, parent); } - service.accesses = accesses; - - service.generateAccessPolicies = function (userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) { + function generateAccessPolicies(userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) { const newUserPolicies = _.clone(userAccessPolicies); const newTeamPolicies = _.clone(teamAccessPolicies); @@ -115,8 +116,6 @@ angular.module('portainer.app').factory('AccessService', [ }; return accessPolicies; - }; - - return service; + } }, ]); diff --git a/app/portainer/services/api/dockerhubService.js b/app/portainer/services/api/dockerhubService.js deleted file mode 100644 index aa079ae40..000000000 --- a/app/portainer/services/api/dockerhubService.js +++ /dev/null @@ -1,51 +0,0 @@ -import { DockerHubViewModel } from '../../models/dockerhub'; - -angular.module('portainer.app').factory('DockerHubService', [ - '$q', - 'DockerHub', - 'Endpoints', - 'AgentDockerhub', - 'EndpointHelper', - function DockerHubServiceFactory($q, DockerHub, Endpoints, AgentDockerhub, EndpointHelper) { - 'use strict'; - var service = {}; - - service.dockerhub = function () { - var deferred = $q.defer(); - - DockerHub.get() - .$promise.then(function success(data) { - var dockerhub = new DockerHubViewModel(data); - deferred.resolve(dockerhub); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve DockerHub details', err: err }); - }); - - return deferred.promise; - }; - - service.update = function (dockerhub) { - return DockerHub.update({}, dockerhub).$promise; - }; - - service.checkRateLimits = checkRateLimits; - function checkRateLimits(endpoint) { - if (EndpointHelper.isLocalEndpoint(endpoint)) { - return Endpoints.dockerhubLimits({ id: endpoint.Id }).$promise; - } - - switch (endpoint.Type) { - case 2: //AgentOnDockerEnvironment - case 4: //EdgeAgentOnDockerEnvironment - return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'docker' }).$promise; - - case 6: //AgentOnKubernetesEnvironment - case 7: //EdgeAgentOnKubernetesEnvironment - return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'kubernetes' }).$promise; - } - } - - return service; - }, -]); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 870075156..de47e6f60 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -8,6 +8,8 @@ angular.module('portainer.app').factory('EndpointService', [ 'use strict'; var service = { updateSecuritySettings, + registries, + updateRegistryAccess, }; service.endpoint = function (endpointID) { @@ -157,6 +159,14 @@ angular.module('portainer.app').factory('EndpointService', [ return deferred.promise; }; + function updateRegistryAccess(id, registryId, endpointAccesses) { + return Endpoints.updateRegistryAccess({ registryId, id }, endpointAccesses).$promise; + } + + function registries(id, namespace) { + return Endpoints.registries({ namespace, id }).$promise; + } + return service; function updateSecuritySettings(id, securitySettings) { diff --git a/app/portainer/services/api/registryService.js b/app/portainer/services/api/registryService.js index 36c46ad77..abf832cac 100644 --- a/app/portainer/services/api/registryService.js +++ b/app/portainer/services/api/registryService.js @@ -1,20 +1,30 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -import { RegistryTypes } from '@/portainer/models/registryTypes'; -import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry'; +import { RegistryTypes } from 'Portainer/models/registryTypes'; +import { RegistryCreateRequest, RegistryViewModel } from 'Portainer/models/registry'; +import { DockerHubViewModel } from 'Portainer/models/dockerhub'; angular.module('portainer.app').factory('RegistryService', [ '$q', '$async', 'Registries', - 'DockerHubService', 'ImageHelper', 'FileUploadService', - function RegistryServiceFactory($q, $async, Registries, DockerHubService, ImageHelper, FileUploadService) { - 'use strict'; - var service = {}; + function RegistryServiceFactory($q, $async, Registries, ImageHelper, FileUploadService) { + return { + registries, + registry, + encodedCredentials, + deleteRegistry, + updateRegistry, + configureRegistry, + createRegistry, + createGitlabRegistries, + retrievePorRegistryModelFromRepository, + retrievePorRegistryModelFromRepositoryWithRegistries, + }; - service.registries = function () { + function registries() { var deferred = $q.defer(); Registries.query() @@ -29,9 +39,9 @@ angular.module('portainer.app').factory('RegistryService', [ }); return deferred.promise; - }; + } - service.registry = function (id) { + function registry(id) { var deferred = $q.defer(); Registries.get({ id: id }) @@ -44,39 +54,35 @@ angular.module('portainer.app').factory('RegistryService', [ }); return deferred.promise; - }; + } - service.encodedCredentials = function (registry) { + function encodedCredentials(registry) { var credentials = { serveraddress: registry.URL, }; return btoa(JSON.stringify(credentials)); - }; + } - service.updateAccess = function (id, userAccessPolicies, teamAccessPolicies) { - return Registries.updateAccess({ id: id }, { UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies }).$promise; - }; - - service.deleteRegistry = function (id) { + function deleteRegistry(id) { return Registries.remove({ id: id }).$promise; - }; + } - service.updateRegistry = function (registry) { + function updateRegistry(registry) { registry.URL = _.replace(registry.URL, /^https?\:\/\//i, ''); registry.URL = _.replace(registry.URL, /\/$/, ''); return Registries.update({ id: registry.Id }, registry).$promise; - }; + } - service.configureRegistry = function (id, registryManagementConfigurationModel) { + function configureRegistry(id, registryManagementConfigurationModel) { return FileUploadService.configureRegistry(id, registryManagementConfigurationModel); - }; + } - service.createRegistry = function (model) { + function createRegistry(model) { var payload = new RegistryCreateRequest(model); return Registries.create(payload).$promise; - }; + } - service.createGitlabRegistries = function (model, projects) { + function createGitlabRegistries(model, projects) { const promises = []; _.forEach(projects, (p) => { const m = model; @@ -88,9 +94,7 @@ angular.module('portainer.app').factory('RegistryService', [ promises.push(Registries.create(payload).$promise); }); return $q.all(promises); - }; - - service.retrievePorRegistryModelFromRepositoryWithRegistries = retrievePorRegistryModelFromRepositoryWithRegistries; + } function getURL(reg) { let url = reg.URL; @@ -103,14 +107,23 @@ angular.module('portainer.app').factory('RegistryService', [ return url; } - function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, dockerhub) { + function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId, dockerhub) { const model = new PorImageRegistryModel(); - const registry = _.find(registries, (reg) => _.includes(repository, getURL(reg))); + const registry = registries.find((reg) => { + if (registryId) { + return reg.Id === registryId; + } + if (reg.Type === RegistryTypes.DOCKERHUB) { + return _.includes(repository, reg.Username); + } + return _.includes(repository, getURL(reg)); + }); if (registry) { const url = getURL(registry); - const lastIndex = repository.lastIndexOf(url) + url.length; + let lastIndex = repository.lastIndexOf(url); + lastIndex = lastIndex === -1 ? 0 : lastIndex + url.length; let image = repository.substring(lastIndex); - if (!_.startsWith(image, ':')) { + if (_.startsWith(image, '/')) { image = image.substring(1); } model.Registry = registry; @@ -125,19 +138,18 @@ angular.module('portainer.app').factory('RegistryService', [ return model; } - async function retrievePorRegistryModelFromRepositoryAsync(repository) { + async function retrievePorRegistryModelFromRepositoryAsync(repository, endpointId, registryId) { try { - let [registries, dockerhub] = await Promise.all([service.registries(), DockerHubService.dockerhub()]); - return retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, dockerhub); + const regs = await Promise.all([registries(endpointId)]); + const dockerhub = new DockerHubViewModel(); + return retrievePorRegistryModelFromRepositoryWithRegistries(repository, regs, registryId, dockerhub); } catch (err) { throw { msg: 'Unable to retrieve the registry associated to the repository', err: err }; } } - service.retrievePorRegistryModelFromRepository = function (repository) { - return $async(retrievePorRegistryModelFromRepositoryAsync, repository); - }; - - return service; + function retrievePorRegistryModelFromRepository(repository, endpointId, registryId) { + return $async(retrievePorRegistryModelFromRepositoryAsync, repository, endpointId, registryId); + } }, ]); diff --git a/app/portainer/services/api/templateService.js b/app/portainer/services/api/templateService.js index 82fb486e9..04265c85b 100644 --- a/app/portainer/services/api/templateService.js +++ b/app/portainer/services/api/templateService.js @@ -1,91 +1,85 @@ -import _ from 'lodash-es'; +import { DockerHubViewModel } from 'Portainer/models/dockerhub'; import { TemplateViewModel } from '../../models/template'; -angular.module('portainer.app').factory('TemplateService', [ - '$q', - 'Templates', - 'TemplateHelper', - 'RegistryService', - 'DockerHubService', - 'ImageHelper', - 'ContainerHelper', - function TemplateServiceFactory($q, Templates, TemplateHelper, RegistryService, DockerHubService, ImageHelper, ContainerHelper) { - 'use strict'; - var service = {}; +angular.module('portainer.app').factory('TemplateService', TemplateServiceFactory); - service.templates = function () { - const deferred = $q.defer(); +/* @ngInject */ +function TemplateServiceFactory($q, Templates, TemplateHelper, EndpointProvider, ImageHelper, ContainerHelper, EndpointService) { + var service = {}; - $q.all({ - templates: Templates.query().$promise, - registries: RegistryService.registries(), - dockerhub: DockerHubService.dockerhub(), - }) - .then(function success(data) { - const version = data.templates.version; - const templates = _.map(data.templates.templates, (item) => { + service.templates = function () { + const deferred = $q.defer(); + const endpointId = EndpointProvider.currentEndpoint().Id; + + $q.all({ + templates: Templates.query().$promise, + registries: EndpointService.registries(endpointId), + }) + .then(function success({ templates, registries }) { + const version = templates.version; + deferred.resolve( + templates.templates.map((item) => { try { const template = new TemplateViewModel(item, version); - const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(template.RegistryModel.Registry.URL, data.registries, data.dockerhub); - registry.Image = template.RegistryModel.Image; - template.RegistryModel = registry; + const registryURL = template.RegistryModel.Registry.URL; + const registry = registryURL ? registries.find((reg) => reg.URL === registryURL) : new DockerHubViewModel(); + template.RegistryModel.Registry = registry; return template; } catch (err) { deferred.reject({ msg: 'Unable to retrieve templates', err: err }); } - }); - deferred.resolve(templates); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve templates', err: err }); - }); - - return deferred.promise; - }; - - service.templateFile = templateFile; - function templateFile(repositoryUrl, composeFilePathInRepository) { - return Templates.file({ repositoryUrl, composeFilePathInRepository }).$promise; - } - - service.createTemplateConfiguration = function (template, containerName, network) { - var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel); - var containerConfiguration = createContainerConfiguration(template, containerName, network); - containerConfiguration.Image = imageConfiguration.fromImage; - return containerConfiguration; - }; - - function createContainerConfiguration(template, containerName, network) { - var configuration = TemplateHelper.getDefaultContainerConfiguration(); - configuration.HostConfig.NetworkMode = network.Name; - configuration.HostConfig.Privileged = template.Privileged; - configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy }; - configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : []; - configuration.name = containerName; - configuration.Hostname = template.Hostname; - configuration.Env = TemplateHelper.EnvToStringArray(template.Env); - configuration.Cmd = ContainerHelper.commandStringToArray(template.Command); - var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports); - configuration.HostConfig.PortBindings = portConfiguration.bindings; - configuration.ExposedPorts = portConfiguration.exposedPorts; - var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive); - configuration.OpenStdin = consoleConfiguration.openStdin; - configuration.Tty = consoleConfiguration.tty; - configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels); - return configuration; - } - - service.updateContainerConfigurationWithVolumes = function (configuration, template, generatedVolumesPile) { - var volumes = template.Volumes; - TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile); - volumes.forEach(function (volume) { - if (volume.binding) { - configuration.Volumes[volume.container] = {}; - configuration.HostConfig.Binds.push(volume.binding); - } + }) + ); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve templates', err: err }); }); - }; - return service; - }, -]); + return deferred.promise; + }; + + service.templateFile = templateFile; + function templateFile(repositoryUrl, composeFilePathInRepository) { + return Templates.file({ repositoryUrl, composeFilePathInRepository }).$promise; + } + + service.createTemplateConfiguration = function (template, containerName, network) { + var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel); + var containerConfiguration = createContainerConfiguration(template, containerName, network); + containerConfiguration.Image = imageConfiguration.fromImage; + return containerConfiguration; + }; + + function createContainerConfiguration(template, containerName, network) { + var configuration = TemplateHelper.getDefaultContainerConfiguration(); + configuration.HostConfig.NetworkMode = network.Name; + configuration.HostConfig.Privileged = template.Privileged; + configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy }; + configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : []; + configuration.name = containerName; + configuration.Hostname = template.Hostname; + configuration.Env = TemplateHelper.EnvToStringArray(template.Env); + configuration.Cmd = ContainerHelper.commandStringToArray(template.Command); + var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports); + configuration.HostConfig.PortBindings = portConfiguration.bindings; + configuration.ExposedPorts = portConfiguration.exposedPorts; + var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive); + configuration.OpenStdin = consoleConfiguration.openStdin; + configuration.Tty = consoleConfiguration.tty; + configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels); + return configuration; + } + + service.updateContainerConfigurationWithVolumes = function (configuration, template, generatedVolumesPile) { + var volumes = template.Volumes; + TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile); + volumes.forEach(function (volume) { + if (volume.binding) { + configuration.Volumes[volume.container] = {}; + configuration.HostConfig.Binds.push(volume.binding); + } + }); + }; + + return service; +} diff --git a/app/portainer/views/endpoint-registries/registries.html b/app/portainer/views/endpoint-registries/registries.html new file mode 100644 index 000000000..5ed4f74b3 --- /dev/null +++ b/app/portainer/views/endpoint-registries/registries.html @@ -0,0 +1,21 @@ + + + + + + + Manage registry access inside this environment + +
+
+ +
+
diff --git a/app/portainer/views/endpoint-registries/registries.js b/app/portainer/views/endpoint-registries/registries.js new file mode 100644 index 000000000..61dfc3b42 --- /dev/null +++ b/app/portainer/views/endpoint-registries/registries.js @@ -0,0 +1,7 @@ +angular.module('portainer.app').component('endpointRegistriesView', { + templateUrl: './registries.html', + controller: 'EndpointRegistriesController', + bindings: { + endpoint: '<', + }, +}); diff --git a/app/portainer/views/endpoint-registries/registriesController.js b/app/portainer/views/endpoint-registries/registriesController.js new file mode 100644 index 000000000..32482dbc9 --- /dev/null +++ b/app/portainer/views/endpoint-registries/registriesController.js @@ -0,0 +1,51 @@ +import _ from 'lodash-es'; +import { DockerHubViewModel } from 'Portainer/models/dockerhub'; +import { RegistryTypes } from 'Portainer/models/registryTypes'; + +class EndpointRegistriesController { + /* @ngInject */ + constructor($async, Notifications, RegistryService) { + this.$async = $async; + this.Notifications = Notifications; + this.RegistryService = RegistryService; + + this.canManageAccess = this.canManageAccess.bind(this); + } + + canManageAccess(item) { + return item.Type !== RegistryTypes.ANONYMOUS; + } + + getRegistries() { + return this.$async(async () => { + try { + const dockerhub = new DockerHubViewModel(); + const registries = await this.RegistryService.registries(); + this.registries = _.concat(registries, dockerhub); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registries'); + } + }); + } + + $onInit() { + return this.$async(async () => { + this.state = { + viewReady: false, + }; + + try { + this.endpointType = this.endpoint.Type; + this.endpointId = this.endpoint.Id; + await this.getRegistries(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve registries'); + } finally { + this.state.viewReady = true; + } + }); + } +} + +export default EndpointRegistriesController; +angular.module('portainer.app').controller('EndpointRegistriesController', EndpointRegistriesController); diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index 8872bcfe1..e08eca529 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -1,5 +1,4 @@ angular.module('portainer.app').controller('InitAdminController', [ - '$async', '$scope', '$state', 'Notifications', @@ -10,7 +9,7 @@ angular.module('portainer.app').controller('InitAdminController', [ 'EndpointService', 'BackupService', 'StatusService', - function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) { + function ($scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) { $scope.logo = StateManager.getState().application.logo; $scope.formValues = { diff --git a/app/portainer/views/registries/access/registryAccess.html b/app/portainer/views/registries/access/registryAccess.html deleted file mode 100644 index bab5d7391..000000000 --- a/app/portainer/views/registries/access/registryAccess.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - Registries > {{ registry.Name }} > Access management - - - -
-
- - - - - - - - - - - - - - -
Name - {{ registry.Name }} -
URL - {{ registry.URL }} -
-
-
-
-
- - - diff --git a/app/portainer/views/registries/access/registryAccessController.js b/app/portainer/views/registries/access/registryAccessController.js deleted file mode 100644 index a31372368..000000000 --- a/app/portainer/views/registries/access/registryAccessController.js +++ /dev/null @@ -1,34 +0,0 @@ -angular.module('portainer.app').controller('RegistryAccessController', [ - '$scope', - '$state', - '$transition$', - 'RegistryService', - 'Notifications', - function ($scope, $state, $transition$, RegistryService, Notifications) { - $scope.updateAccess = function () { - $scope.state.actionInProgress = true; - RegistryService.updateRegistry($scope.registry) - .then(() => { - Notifications.success('Access successfully updated'); - $state.reload(); - }) - .catch((err) => { - $scope.state.actionInProgress = false; - Notifications.error('Failure', err, 'Unable to update accesses'); - }); - }; - - function initView() { - $scope.state = { actionInProgress: false }; - RegistryService.registry($transition$.params().id) - .then(function success(data) { - $scope.registry = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve registry details'); - }); - } - - initView(); - }, -]); diff --git a/app/portainer/views/registries/create/createregistry.html b/app/portainer/views/registries/create/createRegistry.html similarity index 51% rename from app/portainer/views/registries/create/createregistry.html rename to app/portainer/views/registries/create/createRegistry.html index 16350efb7..064c6adf2 100644 --- a/app/portainer/views/registries/create/createregistry.html +++ b/app/portainer/views/registries/create/createRegistry.html @@ -17,8 +17,18 @@
- -
+
+ +