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;
 }]);