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] 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 @@ <rd-widget> <rd-widget-body> <ul class="nav nav-pills nav-justified"> - <li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li> + <li class="active interactive"><a data-target="#command" data-toggle="tab">Command & Logging</a></li> <li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li> <li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li> <li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li> @@ -140,6 +140,9 @@ <!-- tab-command --> <div class="tab-pane active" id="command"> <form class="form-horizontal" style="margin-top: 15px;"> + <div class="col-sm-12 form-section-title"> + Command + </div> <!-- command-input --> <div class="form-group"> <label for="service_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label> @@ -195,6 +198,59 @@ <!-- !environment-variable-input-list --> </div> <!-- !environment-variables --> + + <div class="col-sm-12 form-section-title"> + Logging + </div> + <!-- logging-driver --> + <div class="form-group"> + <label for="log-driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label> + <div class="col-sm-4"> + <select class="form-control" ng-model="formValues.LogDriverName" id="log-driver"> + <option selected value="">Default logging driver</option> + <option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option> + <option value="none">none</option> + </select> + </div> + <div class="col-sm-5"> + <p class="small text-muted"> + Logging driver for service that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers can be found <a href="https://docs.docker.com/engine/admin/logging/overview/#supported-logging-drivers" target="_blank">in the Docker documentation</a>. + </p> + </div> + </div> + <!-- !logging-driver --> + <!-- logging-opts --> + <div class="form-group"> + <div class="col-sm-12" style="margin-top: 5px;"> + <label class="control-label text-left"> + Options + <portainer-tooltip position="top" message="Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation."></portainer-tooltip> + </label> + <span class="label label-default interactive" style="margin-left: 10px;" ng-click="!formValues.LogDriverName || formValues.LogDriverName === 'none' || addLogDriverOpt(formValues.LogDriverName)"> + <i class="fa fa-plus-circle" aria-hidden="true"></i> add logging driver option + </span> + </div> + <!-- logging-opts-input-list --> + <div class="col-sm-12 form-inline" style="margin-top: 10px;"> + <div ng-repeat="opt in formValues.LogDriverOpts" style="margin-top: 2px;"> + <div class="input-group col-sm-5 input-group-sm"> + <span class="input-group-addon">option</span> + <input type="text" class="form-control" ng-model="opt.name" placeholder="e.g. FOO"> + </div> + <div class="input-group col-sm-5 input-group-sm"> + <span class="input-group-addon">value</span> + <input type="text" class="form-control" ng-model="opt.value" placeholder="e.g. bar"> + </div> + <button class="btn btn-sm btn-danger" type="button" ng-click="removeLogDriverOpt($index)"> + <i class="fa fa-trash" aria-hidden="true"></i> + </button> + </div> + </div> + <!-- logging-opts-input-list --> + </div> + <!-- !logging-opts --> + + </form> </div> <!-- !tab-command --> diff --git a/app/components/service/includes/logging.html b/app/components/service/includes/logging.html new file mode 100644 index 000000000..2172ac902 --- /dev/null +++ b/app/components/service/includes/logging.html @@ -0,0 +1,65 @@ +<div id="service-logging-driver"> + <rd-widget> + <rd-widget-header icon="fa-tasks" title="Logging driver"> + </rd-widget-header> + <rd-widget-body classes="no-padding"> + <div class="form-inline" style="padding: 10px;"> + Driver: + <select class="form-control" ng-model="service.LogDriverName" ng-change="updateLogDriverName(service)" ng-disabled="isUpdating"> + <option selected value="">Default logging driver</option> + <option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option> + <option value="none">none</option> + </select> + <a class="btn btn-default btn-sm" ng-click="!service.LogDriverName || service.LogDriverName === 'none' || addLogDriverOpt(service)"> + <i class="fa fa-plus-circle" aria-hidden="true"></i> add logging driver option + </a> + </div> + <table class="table" > + <thead> + <tr> + <th>Option</th> + <th>Value</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="option in service.LogDriverOpts"> + <td> + <div class="input-group input-group-sm"> + <span class="input-group-addon fit-text-size">name</span> + <input type="text" class="form-control" ng-model="option.key" ng-disabled="option.added || isUpdating" placeholder="e.g. FOO"> + </div> + </td> + <td> + <div class="input-group input-group-sm"> + <span class="input-group-addon fit-text-size">value</span> + <input type="text" class="form-control" ng-model="option.value" ng-change="updateLogDriverOpt(service, option)" placeholder="e.g. bar" ng-disabled="isUpdating"> + <span class="input-group-btn"> + <button class="btn btn-sm btn-danger" type="button" ng-click="removeLogDriverOpt(service, $index)" ng-disabled="isUpdating"> + <i class="fa fa-trash" aria-hidden="true"></i> + </button> + </span> + </div> + </td> + </tr> + <tr ng-if="service.LogDriverOpts.length === 0"> + <td colspan="6" class="text-center text-muted">No options associated to this logging driver.</td> + </tr> + </tbody> + </table> + </rd-widget-body> + <rd-widget-footer> + <div class="btn-toolbar" role="toolbar"> + <div class="btn-group" role="group"> + <button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['LogDriverName', 'LogDriverOpts'])" ng-click="updateService(service)">Apply changes</button> + <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li><a ng-click="cancelChanges(service, ['LogDriverName', 'LogDriverOpts'])">Reset changes</a></li> + <li><a ng-click="cancelChanges(service)">Reset all changes</a></li> + </ul> + </div> + </div> + </rd-widget-footer> + </rd-widget> + </div> \ No newline at end of file diff --git a/app/components/service/service.html b/app/components/service/service.html index b889131d8..16245f8e2 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -116,6 +116,7 @@ <li ng-if="applicationState.endpoint.apiVersion >= 1.30"><a href ng-click="goToItem('service-placement-preferences')">Placement preferences</a></li> <li><a href ng-click="goToItem('service-restart-policy')">Restart policy</a></li> <li><a href ng-click="goToItem('service-update-config')">Update configuration</a></li> + <li><a href ng-click="goToItem('service-logging')">Logging</a></li> <li><a href ng-click="goToItem('service-labels')">Service labels</a></li> <li><a href ng-click="goToItem('service-configs')">Configs</a></li> <li ng-if="applicationState.endpoint.apiVersion >= 1.25"><a href ng-click="goToItem('service-secrets')">Secrets</a></li> @@ -165,6 +166,7 @@ <div id="service-placement-preferences" ng-if="applicationState.endpoint.apiVersion >= 1.30" class="padding-top" ng-include="'app/components/service/includes/placementPreferences.html'"></div> <div id="service-restart-policy" class="padding-top" ng-include="'app/components/service/includes/restart.html'"></div> <div id="service-update-config" class="padding-top" ng-include="'app/components/service/includes/updateconfig.html'"></div> + <div id="service-logging" class="padding-top" ng-include="'app/components/service/includes/logging.html'"></div> <div id="service-labels" class="padding-top" ng-include="'app/components/service/includes/servicelabels.html'"></div> <div id="service-configs" class="padding-top" ng-include="'app/components/service/includes/configs.html'"></div> <div id="service-secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" class="padding-top" ng-include="'app/components/service/includes/secrets.html'"></div> diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 05d6ee618..98e1fff5b 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -1,6 +1,6 @@ angular.module('service', []) -.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'ModalService', -function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, ModalService) { +.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'ModalService', 'PluginService', +function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, ModalService, PluginService) { $scope.state = {}; $scope.tasks = []; @@ -168,6 +168,25 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, } }; + $scope.addLogDriverOpt = function addLogDriverOpt(service) { + service.LogDriverOpts.push({ key: '', value: '', originalValue: '' }); + updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts); + }; + $scope.removeLogDriverOpt = function removeLogDriverOpt(service, index) { + var removedElement = service.LogDriverOpts.splice(index, 1); + if (removedElement !== null) { + updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts); + } + }; + $scope.updateLogDriverOpt = function updateLogDriverOpt(service, variable) { + if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) { + updateServiceArray(service, 'LogDriverOpts', service.LogDriverOpts); + } + }; + $scope.updateLogDriverName = function updateLogDriverName(service) { + updateServiceArray(service, 'LogDriverName', service.LogDriverName); + }; + $scope.addHostsEntry = function (service) { if (!service.Hosts) { service.Hosts = []; @@ -260,6 +279,17 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, MaxAttempts: service.RestartMaxAttempts, Window: ServiceHelper.translateHumanDurationToNanos(service.RestartWindow) || 0 }; + + config.TaskTemplate.LogDriver = null; + if (service.LogDriverName) { + config.TaskTemplate.LogDriver = { Name: service.LogDriverName }; + if (service.LogDriverName !== 'none') { + var logOpts = ServiceHelper.translateKeyValueToLogDriverOpts(service.LogDriverOpts); + if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { + config.TaskTemplate.LogDriver.Options = logOpts; + } + } + } if (service.Ports) { service.Ports.forEach(function (binding) { @@ -312,6 +342,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : []; service.ServiceConfigs = service.Configs ? service.Configs.map(ConfigHelper.flattenConfig) : []; service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env); + service.LogDriverOpts = ServiceHelper.translateLogDriverOptsToKeyValue(service.LogDriverOpts); service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels); service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels); service.ServiceMounts = angular.copy(service.Mounts); @@ -334,7 +365,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, } function initView() { - var apiVersion = $scope.applicationState.endpoint.apiVersion; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + ServiceService.service($transition$.params().id) .then(function success(data) { var service = data; @@ -354,7 +386,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, nodes: NodeService.nodes(), secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], configs: apiVersion >= 1.30 ? ConfigService.configs() : [], - availableImages: ImageService.images() + availableImages: ImageService.images(), + availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25) }); }) .then(function success(data) { @@ -363,6 +396,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, $scope.configs = data.configs; $scope.secrets = data.secrets; $scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages); + $scope.availableLoggingDrivers = data.availableLoggingDrivers; // Set max cpu value var maxCpus = 0; diff --git a/app/helpers/serviceHelper.js b/app/helpers/serviceHelper.js index 36e66bd3e..38adafe0d 100644 --- a/app/helpers/serviceHelper.js +++ b/app/helpers/serviceHelper.js @@ -172,8 +172,7 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe // e.g 3600000000000 nanoseconds = 1h helper.translateNanosToHumanDuration = function(nanos) { - var humanDuration = '0s'; - + var humanDuration = '0s'; var conversionFromNano = {}; conversionFromNano['ns'] = 1; conversionFromNano['us'] = conversionFromNano['ns'] * 1000; @@ -186,11 +185,38 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe if ( nanos % conversionFromNano[unit] === 0 && (nanos / conversionFromNano[unit]) > 0) { humanDuration = (nanos / conversionFromNano[unit]) + unit; } - }); - + }); return humanDuration; }; + helper.translateLogDriverOptsToKeyValue = function(logOptions) { + var options = []; + if (logOptions) { + Object.keys(logOptions).forEach(function(key) { + options.push({ + key: key, + value: logOptions[key], + originalKey: key, + originalValue: logOptions[key], + added: true + }); + }); + } + return options; + }; + + helper.translateKeyValueToLogDriverOpts = function(keyValueLogDriverOpts) { + var options = {}; + if (keyValueLogDriverOpts) { + keyValueLogDriverOpts.forEach(function(option) { + if (option.key && option.key !== '' && option.value && option.value !== '') { + options[option.key] = option.value; + } + }); + } + return options; + }; + helper.translateHostsEntriesToHostnameIP = function(entries) { var ipHostEntries = []; if (entries) { @@ -204,7 +230,6 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe return ipHostEntries; }; - helper.translateHostnameIPToHostsEntries = function(entries) { var ipHostEntries = []; if (entries) { diff --git a/app/models/docker/service.js b/app/models/docker/service.js index c04c7611d..116590df4 100644 --- a/app/models/docker/service.js +++ b/app/models/docker/service.js @@ -41,6 +41,15 @@ function ServiceViewModel(data, runningTasks, allTasks, nodes) { this.RestartMaxAttempts = 0; this.RestartWindow = 0; } + + if (data.Spec.TaskTemplate.LogDriver) { + this.LogDriverName = data.Spec.TaskTemplate.LogDriver.Name || ''; + this.LogDriverOpts = data.Spec.TaskTemplate.LogDriver.Options || []; + } else { + this.LogDriverName = ''; + this.LogDriverOpts = []; + } + this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : []; this.Preferences = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Preferences || [] : []; this.Platforms = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Platforms || [] : []; diff --git a/app/services/docker/pluginService.js b/app/services/docker/pluginService.js index a539105ad..b7d94d4e4 100644 --- a/app/services/docker/pluginService.js +++ b/app/services/docker/pluginService.js @@ -60,5 +60,9 @@ angular.module('portainer.services') return servicePlugins(systemOnly, 'Network', 'docker.networkdriver/1.0'); }; + service.loggingPlugins = function(systemOnly) { + return servicePlugins(systemOnly, 'Log', 'docker.logdriver/1.0'); + }; + return service; }]);