From 9c9e16b2b2d1dca560b527d8be4743e37d650845 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 14 Dec 2017 10:31:16 +0100 Subject: [PATCH 01/30] fix(containers): fix the ability to stop/pause a healthy container (#1507) --- .../containersDatatableController.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/directives/ui/datatables/containers-datatable/containersDatatableController.js b/app/directives/ui/datatables/containers-datatable/containersDatatableController.js index ccd65507a..e42dbcc58 100644 --- a/app/directives/ui/datatables/containers-datatable/containersDatatableController.js +++ b/app/directives/ui/datatables/containers-datatable/containersDatatableController.js @@ -70,16 +70,22 @@ function (PaginationService, DatatableService) { for (var i = 0; i < this.dataset.length; i++) { var item = this.dataset[i]; - if (item.Checked && item.Status === 'paused') { - this.state.noPausedItemsSelected = false; - } else if (item.Checked && (item.Status === 'stopped' || item.Status === 'created')) { - this.state.noStoppedItemsSelected = false; - } else if (item.Checked && item.Status === 'running') { - this.state.noRunningItemsSelected = false; + if (item.Checked) { + this.updateSelectionStateBasedOnItemStatus(item); } } }; + this.updateSelectionStateBasedOnItemStatus = function(item) { + if (item.Status === 'paused') { + this.state.noPausedItemsSelected = false; + } else if (['stopped', 'created'].indexOf(item.Status) !== -1) { + this.state.noStoppedItemsSelected = false; + } else if (['running', 'healthy', 'unhealthy', 'starting'].indexOf(item.Status) !== -1) { + this.state.noRunningItemsSelected = false; + } + }; + this.changePaginationLimit = function() { PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); }; From c9e060d5748757ca9939e77abb6ce5f19295671f Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 18 Dec 2017 21:24:51 +0100 Subject: [PATCH 02/30] fix(container-logs): add missing dependency to Notifications (#1514) --- app/components/containerLogs/containerLogsController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/containerLogs/containerLogsController.js b/app/components/containerLogs/containerLogsController.js index 941dbcc51..6e48bc47e 100644 --- a/app/components/containerLogs/containerLogsController.js +++ b/app/components/containerLogs/containerLogsController.js @@ -1,6 +1,6 @@ angular.module('containerLogs', []) -.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', -function ($scope, $transition$, $anchorScroll, ContainerLogs, Container) { +.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', 'Notifications', +function ($scope, $transition$, $anchorScroll, ContainerLogs, Container, Notifications) { $scope.state = {}; $scope.state.displayTimestampsOut = false; $scope.state.displayTimestampsErr = false; From 8e40eb1844eaa5fa0cac0f1e4cbecd397148bfb7 Mon Sep 17 00:00:00 2001 From: "Miguel A. C" <30386061+doncicuto@users.noreply.github.com> Date: Thu, 21 Dec 2017 09:53:34 +0100 Subject: [PATCH 03/30] feat(service): add hosts file entries in service create/update (#1511) --- .../createService/createServiceController.js | 26 +++++++++ .../createService/createservice.html | 23 ++++++++ app/components/service/includes/hosts.html | 57 +++++++++++++++++++ app/components/service/service.html | 1 + app/components/service/serviceController.js | 20 ++++++- app/helpers/serviceHelper.js | 26 +++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 app/components/service/includes/hosts.html diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index ab85f14ed..369c219cf 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -20,6 +20,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C Volumes: [], Network: '', ExtraNetworks: [], + HostsEntries: [], Ports: [], Parallelism: 1, PlacementConstraints: [], @@ -69,6 +70,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C $scope.formValues.ExtraNetworks.splice(index, 1); }; + $scope.addHostsEntry = function() { + $scope.formValues.HostsEntries.push({}); + }; + + $scope.removeHostsEntry = function(index) { + $scope.formValues.HostsEntries.splice(index, 1); + }; + $scope.addVolume = function() { $scope.formValues.Volumes.push({ Source: '', Target: '', ReadOnly: false, Type: 'volume' }); }; @@ -244,6 +253,22 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C config.Networks = _.uniqWith(networks, _.isEqual); } + function prepareHostsEntries(config, input) { + var hostsEntries = []; + if (input.HostsEntries) { + input.HostsEntries.forEach(function (host_ip) { + if (host_ip.value && host_ip.value.indexOf(':') && host_ip.value.split(':').length === 2) { + var keyVal = host_ip.value.split(':'); + // Hosts file format, IP_address canonical_hostname + hostsEntries.push(keyVal[1] + ' ' + keyVal[0]); + } + }); + if (hostsEntries.length > 0) { + config.TaskTemplate.ContainerSpec.Hosts = hostsEntries; + } + } + } + function prepareUpdateConfig(config, input) { config.UpdateConfig = { Parallelism: input.Parallelism || 0, @@ -355,6 +380,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C prepareLabelsConfig(config, input); prepareVolumes(config, input); prepareNetworks(config, input); + prepareHostsEntries(config, input); prepareUpdateConfig(config, input); prepareConfigConfig(config, input); prepareSecretConfig(config, input); diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 9a86ee73c..3f96b372c 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -308,6 +308,29 @@ + +
+
+ + + add additional entry + +
+ +
+
+
+ value + +
+ +
+
+ +
+ diff --git a/app/components/service/includes/hosts.html b/app/components/service/includes/hosts.html new file mode 100644 index 000000000..7d9ab78c7 --- /dev/null +++ b/app/components/service/includes/hosts.html @@ -0,0 +1,57 @@ +
+ + + + + +

The Hosts file has no extra entries.

+
+ + + + + + + + + + + + + + +
HostnameIP
+
+ +
+
+
+ + + + +
+
+
+ + + +
+
diff --git a/app/components/service/service.html b/app/components/service/service.html index 4715f5323..b889131d8 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -152,6 +152,7 @@

Networks & ports

+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 45518e02b..05d6ee618 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -168,6 +168,22 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, } }; + $scope.addHostsEntry = function (service) { + if (!service.Hosts) { + service.Hosts = []; + } + service.Hosts.push({ hostname: '', ip: '' }); + }; + $scope.removeHostsEntry = function(service, index) { + var removedElement = service.Hosts.splice(index, 1); + if (removedElement !== null) { + updateServiceArray(service, 'Hosts', service.Hosts); + } + }; + $scope.updateHostsEntry = function(service, entry) { + updateServiceArray(service, 'Hosts', service.Hosts); + }; + $scope.cancelChanges = function cancelChanges(service, keys) { if (keys) { // clean out the keys only from the list of modified keys keys.forEach(function(key) { @@ -203,7 +219,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, config.TaskTemplate.ContainerSpec.Image = service.Image; config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : []; config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : []; - + config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : []; + if (service.Mode === 'replicated') { config.Mode.Replicated.Replicas = service.Replicas; } @@ -300,6 +317,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, service.ServiceMounts = angular.copy(service.Mounts); service.ServiceConstraints = ServiceHelper.translateConstraintsToKeyValue(service.Constraints); service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences); + service.Hosts = ServiceHelper.translateHostsEntriesToHostnameIP(service.Hosts); } function transformResources(service) { diff --git a/app/helpers/serviceHelper.js b/app/helpers/serviceHelper.js index b25def767..36e66bd3e 100644 --- a/app/helpers/serviceHelper.js +++ b/app/helpers/serviceHelper.js @@ -191,5 +191,31 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe return humanDuration; }; + helper.translateHostsEntriesToHostnameIP = function(entries) { + var ipHostEntries = []; + if (entries) { + entries.forEach(function(entry) { + if (entry.indexOf(' ') && entry.split(' ').length === 2) { + var keyValue = entry.split(' '); + ipHostEntries.push({ hostname: keyValue[1], ip: keyValue[0]}); + } + }); + } + return ipHostEntries; + }; + + + helper.translateHostnameIPToHostsEntries = function(entries) { + var ipHostEntries = []; + if (entries) { + entries.forEach(function(entry) { + if (entry.ip && entry.hostname) { + ipHostEntries.push(entry.ip + ' ' + entry.hostname); + } + }); + } + return ipHostEntries; + }; + return helper; }]); From e0b09f20b000513be2f0ab9510b4c5b5acfc8d8b Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 21 Dec 2017 19:49:39 +0100 Subject: [PATCH 04/30] fix(cache): add a cache validity mechanism (#1527) --- app/constants.js | 3 +- app/services/stateManager.js | 76 +++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/app/constants.js b/app/constants.js index d62fd0a96..4062f9dd3 100644 --- a/app/constants.js +++ b/app/constants.js @@ -11,4 +11,5 @@ angular.module('portainer') .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships') .constant('API_ENDPOINT_TEMPLATES', 'api/templates') .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') -.constant('PAGINATION_MAX_ITEMS', 10); +.constant('PAGINATION_MAX_ITEMS', 10) +.constant('APPLICATION_CACHE_VALIDITY', 3600); diff --git a/app/services/stateManager.js b/app/services/stateManager.js index bd5e70d9e..c9deb5aee 100644 --- a/app/services/stateManager.js +++ b/app/services/stateManager.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService) { +.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'APPLICATION_CACHE_VALIDITY', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY) { 'use strict'; var manager = {}; @@ -34,6 +34,42 @@ angular.module('portainer.services') LocalStorage.storeApplicationState(state.application); }; + + function assignStateFromStatusAndSettings(status, settings) { + state.application.authentication = status.Authentication; + state.application.analytics = status.Analytics; + state.application.endpointManagement = status.EndpointManagement; + state.application.version = status.Version; + state.application.logo = settings.LogoURL; + state.application.displayDonationHeader = settings.DisplayDonationHeader; + state.application.displayExternalContributors = settings.DisplayExternalContributors; + state.application.validity = moment().unix(); + } + + function loadApplicationState() { + var deferred = $q.defer(); + + $q.all({ + settings: SettingsService.publicSettings(), + status: StatusService.status() + }) + .then(function success(data) { + var status = data.status; + var settings = data.settings; + assignStateFromStatusAndSettings(status, settings); + LocalStorage.storeApplicationState(state.application); + deferred.resolve(state); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve server settings and status', err: err}); + }) + .finally(function final() { + state.loading = false; + }); + + return deferred.promise; + } + manager.initialize = function () { var deferred = $q.defer(); @@ -44,32 +80,28 @@ angular.module('portainer.services') var applicationState = LocalStorage.getApplicationState(); if (applicationState) { - state.application = applicationState; - state.loading = false; - deferred.resolve(state); + var now = moment().unix(); + var cacheValidity = now - applicationState.validity; + if (cacheValidity > APPLICATION_CACHE_VALIDITY) { + loadApplicationState() + .then(function success(data) { + deferred.resolve(state); + }) + .catch(function error(err) { + deferred.reject(err); + }); + } else { + state.application = applicationState; + state.loading = false; + deferred.resolve(state); + } } else { - $q.all({ - settings: SettingsService.publicSettings(), - status: StatusService.status() - }) + loadApplicationState() .then(function success(data) { - var status = data.status; - var settings = data.settings; - state.application.authentication = status.Authentication; - state.application.analytics = status.Analytics; - state.application.endpointManagement = status.EndpointManagement; - state.application.version = status.Version; - state.application.logo = settings.LogoURL; - state.application.displayDonationHeader = settings.DisplayDonationHeader; - state.application.displayExternalContributors = settings.DisplayExternalContributors; - LocalStorage.storeApplicationState(state.application); deferred.resolve(state); }) .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve server settings and status', err: err}); - }) - .finally(function final() { - state.loading = false; + deferred.reject(err); }); } From eec10541b3947cb2a09fc335bc3130b2daad3f4f Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 21 Dec 2017 19:56:54 +0100 Subject: [PATCH 05/30] fix(users): fix invalid Authentication value (#1528) --- app/components/users/users.html | 1 + .../ui/datatables/users-datatable/usersDatatable.html | 4 ++-- .../ui/datatables/users-datatable/usersDatatable.js | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/components/users/users.html b/app/components/users/users.html index 9f6535c4d..ad69ff9b7 100644 --- a/app/components/users/users.html +++ b/app/components/users/users.html @@ -118,6 +118,7 @@ title="Users" title-icon="fa-user" dataset="users" table-key="users" order-by="Username" show-text-filter="true" + authentication-method="AuthenticationMethod" remove-action="removeAction" > diff --git a/app/directives/ui/datatables/users-datatable/usersDatatable.html b/app/directives/ui/datatables/users-datatable/usersDatatable.html index 833c1d3e9..9e5d5c40f 100644 --- a/app/directives/ui/datatables/users-datatable/usersDatatable.html +++ b/app/directives/ui/datatables/users-datatable/usersDatatable.html @@ -70,8 +70,8 @@ - Internal - LDAP + Internal + LDAP diff --git a/app/directives/ui/datatables/users-datatable/usersDatatable.js b/app/directives/ui/datatables/users-datatable/usersDatatable.js index f59ca6ae9..565cf3da5 100644 --- a/app/directives/ui/datatables/users-datatable/usersDatatable.js +++ b/app/directives/ui/datatables/users-datatable/usersDatatable.js @@ -9,6 +9,7 @@ angular.module('ui').component('usersDatatable', { orderBy: '@', reverseOrder: '<', showTextFilter: '<', - removeAction: '<' + removeAction: '<', + authenticationMethod: '<' } }); From 8bf3f669d0c185523901fb233528b3f1241dcb6e Mon Sep 17 00:00:00 2001 From: "Miguel A. C" <30386061+doncicuto@users.noreply.github.com> Date: Fri, 22 Dec 2017 10:05:31 +0100 Subject: [PATCH 06/30] feat(service): add logging driver config in service create/update (#1516) --- .../createService/createServiceController.js | 46 ++++++++++--- .../createService/createservice.html | 58 ++++++++++++++++- app/components/service/includes/logging.html | 65 +++++++++++++++++++ app/components/service/service.html | 2 + app/components/service/serviceController.js | 42 ++++++++++-- app/helpers/serviceHelper.js | 35 ++++++++-- app/models/docker/service.js | 9 +++ app/services/docker/pluginService.js | 4 ++ 8 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 app/components/service/includes/logging.html diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index 369c219cf..6a74afc70 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createService', []) -.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', -function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService, SettingsService) { +.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', +function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService) { $scope.formValues = { Name: '', @@ -40,7 +40,9 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C RestartCondition: 'any', RestartDelay: '5s', RestartMaxAttempts: 0, - RestartWindow: '0s' + RestartWindow: '0s', + LogDriverName: '', + LogDriverOpts: [] }; $scope.state = { @@ -142,6 +144,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C $scope.formValues.ContainerLabels.splice(index, 1); }; + $scope.addLogDriverOpt = function(value) { + $scope.formValues.LogDriverOpts.push({ name: '', value: ''}); + }; + + $scope.removeLogDriverOpt = function(index) { + $scope.formValues.LogDriverOpts.splice(index, 1); + }; + function prepareImageConfig(config, input) { var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry.URL); config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag; @@ -355,6 +365,23 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C } } + function prepareLogDriverConfig(config, input) { + var logOpts = {}; + if (input.LogDriverName) { + config.TaskTemplate.LogDriver = { Name: input.LogDriverName }; + if (input.LogDriverName !== 'none') { + input.LogDriverOpts.forEach(function (opt) { + if (opt.name) { + logOpts[opt.name] = opt.value; + } + }); + if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { + config.TaskTemplate.LogDriver.Options = logOpts; + } + } + } + } + function prepareConfiguration() { var input = $scope.formValues; var config = { @@ -388,6 +415,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C prepareResourcesCpuConfig(config, input); prepareResourcesMemoryConfig(config, input); prepareRestartPolicy(config, input); + prepareLogDriverConfig(config, input); return config; } @@ -474,17 +502,17 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], configs: apiVersion >= 1.30 ? ConfigService.configs() : [], nodes: NodeService.nodes(), - settings: SettingsService.publicSettings() + settings: SettingsService.publicSettings(), + availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25) }) .then(function success(data) { $scope.availableVolumes = data.volumes; $scope.availableNetworks = data.networks; $scope.availableSecrets = data.secrets; - $scope.availableConfigs = data.configs; - var nodes = data.nodes; - initSlidersMaxValuesBasedOnNodeData(nodes); - var settings = data.settings; - $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers; + $scope.availableConfigs = data.configs; + $scope.availableLoggingDrivers = data.availableLoggingDrivers; + initSlidersMaxValuesBasedOnNodeData(data.nodes); + $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers; var userDetails = Authentication.getUserDetails(); $scope.isAdmin = userDetails.role === 1; }) diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 3f96b372c..36870fabc 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -126,7 +126,7 @@