mirror of https://github.com/portainer/portainer
				
				
				
			feat(secrets): add secret management (#894)
							parent
							
								
									128601bb58
								
							
						
					
					
						commit
						42d28db47a
					
				
							
								
								
									
										42
									
								
								app/app.js
								
								
								
								
							
							
						
						
									
										42
									
								
								app/app.js
								
								
								
								
							| 
						 | 
				
			
			@ -28,6 +28,7 @@ angular.module('portainer', [
 | 
			
		|||
  'containers',
 | 
			
		||||
  'createContainer',
 | 
			
		||||
  'createNetwork',
 | 
			
		||||
  'createSecret',
 | 
			
		||||
  'createService',
 | 
			
		||||
  'createVolume',
 | 
			
		||||
  'docker',
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +43,8 @@ angular.module('portainer', [
 | 
			
		|||
  'network',
 | 
			
		||||
  'networks',
 | 
			
		||||
  'node',
 | 
			
		||||
  'secrets',
 | 
			
		||||
  'secret',
 | 
			
		||||
  'service',
 | 
			
		||||
  'services',
 | 
			
		||||
  'settings',
 | 
			
		||||
| 
						 | 
				
			
			@ -249,6 +252,19 @@ angular.module('portainer', [
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .state('actions.create.secret', {
 | 
			
		||||
      url: '/secret',
 | 
			
		||||
      views: {
 | 
			
		||||
        'content@': {
 | 
			
		||||
          templateUrl: 'app/components/createSecret/createsecret.html',
 | 
			
		||||
          controller: 'CreateSecretController'
 | 
			
		||||
        },
 | 
			
		||||
        'sidebar@': {
 | 
			
		||||
          templateUrl: 'app/components/sidebar/sidebar.html',
 | 
			
		||||
          controller: 'SidebarController'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .state('actions.create.service', {
 | 
			
		||||
      url: '/service',
 | 
			
		||||
      views: {
 | 
			
		||||
| 
						 | 
				
			
			@ -414,6 +430,32 @@ angular.module('portainer', [
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .state('secrets', {
 | 
			
		||||
      url: '^/secrets/',
 | 
			
		||||
      views: {
 | 
			
		||||
        'content@': {
 | 
			
		||||
          templateUrl: 'app/components/secrets/secrets.html',
 | 
			
		||||
          controller: 'SecretsController'
 | 
			
		||||
        },
 | 
			
		||||
        'sidebar@': {
 | 
			
		||||
          templateUrl: 'app/components/sidebar/sidebar.html',
 | 
			
		||||
          controller: 'SidebarController'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .state('secret', {
 | 
			
		||||
      url: '^/secret/:id/',
 | 
			
		||||
      views: {
 | 
			
		||||
        'content@': {
 | 
			
		||||
          templateUrl: 'app/components/secret/secret.html',
 | 
			
		||||
          controller: 'SecretController'
 | 
			
		||||
        },
 | 
			
		||||
        'sidebar@': {
 | 
			
		||||
          templateUrl: 'app/components/sidebar/sidebar.html',
 | 
			
		||||
          controller: 'SidebarController'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .state('services', {
 | 
			
		||||
      url: '/services/',
 | 
			
		||||
      views: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
angular.module('createSecret', [])
 | 
			
		||||
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService',
 | 
			
		||||
function ($scope, $state, Notifications, SecretService) {
 | 
			
		||||
  $scope.formValues = {
 | 
			
		||||
    Name: '',
 | 
			
		||||
    Data: '',
 | 
			
		||||
    Labels: [],
 | 
			
		||||
    encodeSecret: true
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.addLabel = function() {
 | 
			
		||||
    $scope.formValues.Labels.push({ name: '', value: ''});
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.removeLabel = function(index) {
 | 
			
		||||
    $scope.formValues.Labels.splice(index, 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function prepareLabelsConfig(config) {
 | 
			
		||||
    var labels = {};
 | 
			
		||||
    $scope.formValues.Labels.forEach(function (label) {
 | 
			
		||||
      if (label.name && label.value) {
 | 
			
		||||
          labels[label.name] = label.value;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    config.Labels = labels;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function prepareSecretData(config) {
 | 
			
		||||
    if ($scope.formValues.encodeSecret) {
 | 
			
		||||
      config.Data = btoa(unescape(encodeURIComponent($scope.formValues.Data)));
 | 
			
		||||
    } else {
 | 
			
		||||
      config.Data = $scope.formValues.Data;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function prepareConfiguration() {
 | 
			
		||||
    var config = {};
 | 
			
		||||
    config.Name = $scope.formValues.Name;
 | 
			
		||||
    prepareSecretData(config);
 | 
			
		||||
    prepareLabelsConfig(config);
 | 
			
		||||
    return config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function createSecret(config) {
 | 
			
		||||
    $('#createSecretSpinner').show();
 | 
			
		||||
    SecretService.create(config)
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      Notifications.success('Secret successfully created');
 | 
			
		||||
      $state.go('secrets', {}, {reload: true});
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      Notifications.error('Failure', err, 'Unable to create secret');
 | 
			
		||||
    })
 | 
			
		||||
    .finally(function final() {
 | 
			
		||||
      $('#createSecretSpinner').hide();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $scope.create = function () {
 | 
			
		||||
    var config = prepareConfiguration();
 | 
			
		||||
    createSecret(config);
 | 
			
		||||
  };
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
<rd-header>
 | 
			
		||||
  <rd-header-title title="Create secret"></rd-header-title>
 | 
			
		||||
  <rd-header-content>
 | 
			
		||||
    <a ui-sref="secrets">Secrets</a> > Add secret
 | 
			
		||||
  </rd-header-content>
 | 
			
		||||
</rd-header>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col-lg-12 col-md-12 col-xs-12">
 | 
			
		||||
    <rd-widget>
 | 
			
		||||
      <rd-widget-body>
 | 
			
		||||
        <form class="form-horizontal">
 | 
			
		||||
          <!-- name-input -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="secret_name" class="col-sm-1 control-label text-left">Name</label>
 | 
			
		||||
            <div class="col-sm-11">
 | 
			
		||||
              <input type="text" class="form-control" ng-model="formValues.Name" id="secret_name" placeholder="e.g. mySecret">
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- !name-input -->
 | 
			
		||||
          <!-- secret-data -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="secret_data" class="col-sm-1 control-label text-left">Secret</label>
 | 
			
		||||
            <div class="col-sm-11">
 | 
			
		||||
              <textarea class="form-control" rows="5" ng-model="formValues.Data"></textarea>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- !secret-data -->
 | 
			
		||||
          <!-- encode-secret -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <div class="col-sm-12">
 | 
			
		||||
              <label for="encode_secret" class="control-label text-left">
 | 
			
		||||
                Encode secret
 | 
			
		||||
                <portainer-tooltip position="bottom" message="Secrets need to be base64 encoded. Disable this if your secret is already base64 encoded."></portainer-tooltip>
 | 
			
		||||
              </label>
 | 
			
		||||
              <label class="switch" style="margin-left: 20px;">
 | 
			
		||||
                <input type="checkbox" name="encode_secret" ng-model="formValues.encodeSecret"><i></i>
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- !encode-secret -->
 | 
			
		||||
          <!-- labels -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <div class="col-sm-12" style="margin-top: 5px;">
 | 
			
		||||
              <label class="control-label text-left">Labels</label>
 | 
			
		||||
              <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
 | 
			
		||||
                <i class="fa fa-plus-circle" aria-hidden="true"></i> add label
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- labels-input-list -->
 | 
			
		||||
            <div class="col-sm-12 form-inline" style="margin-top: 10px;">
 | 
			
		||||
              <div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
 | 
			
		||||
                <div class="input-group col-sm-5 input-group-sm">
 | 
			
		||||
                  <span class="input-group-addon">name</span>
 | 
			
		||||
                  <input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.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="label.value" placeholder="e.g. bar">
 | 
			
		||||
                </div>
 | 
			
		||||
                <button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
 | 
			
		||||
                  <i class="fa fa-trash" aria-hidden="true"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- !labels-input-list -->
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- !labels-->
 | 
			
		||||
          <!-- actions -->
 | 
			
		||||
          <div class="col-sm-12 form-section-title">
 | 
			
		||||
            Actions
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <div class="col-sm-12">
 | 
			
		||||
              <button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button>
 | 
			
		||||
              <a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a>
 | 
			
		||||
              <i id="createSecretSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- !actions -->
 | 
			
		||||
        </form>
 | 
			
		||||
      </rd-widget-body>
 | 
			
		||||
    </rd-widget>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -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', ['$scope', '$state', 'Service', 'ServiceHelper', 'Volume', 'Network', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
 | 
			
		||||
function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator) {
 | 
			
		||||
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
 | 
			
		||||
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator) {
 | 
			
		||||
 | 
			
		||||
  $scope.formValues = {
 | 
			
		||||
    Name: '',
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +24,8 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
 | 
			
		|||
    Parallelism: 1,
 | 
			
		||||
    PlacementConstraints: [],
 | 
			
		||||
    UpdateDelay: 0,
 | 
			
		||||
    FailureAction: 'pause'
 | 
			
		||||
    FailureAction: 'pause',
 | 
			
		||||
    Secrets: []
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +56,14 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
 | 
			
		|||
    $scope.formValues.Volumes.splice(index, 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.addSecret = function() {
 | 
			
		||||
    $scope.formValues.Secrets.push({});
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.removeSecret = function(index) {
 | 
			
		||||
    $scope.formValues.Secrets.splice(index, 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.addEnvironmentVariable = function() {
 | 
			
		||||
    $scope.formValues.Env.push({ name: '', value: ''});
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -62,18 +71,23 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
 | 
			
		|||
  $scope.removeEnvironmentVariable = function(index) {
 | 
			
		||||
    $scope.formValues.Env.splice(index, 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.addPlacementConstraint = function() {
 | 
			
		||||
    $scope.formValues.PlacementConstraints.push({ key: '', operator: '==', value: '' });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.removePlacementConstraint = function(index) {
 | 
			
		||||
    $scope.formValues.PlacementConstraints.splice(index, 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.addPlacementPreference = function() {
 | 
			
		||||
    $scope.formValues.PlacementPreferences.push({ key: '', operator: '==', value: '' });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.removePlacementPreference = function(index) {
 | 
			
		||||
    $scope.formValues.PlacementPreferences.splice(index, 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.addLabel = function() {
 | 
			
		||||
    $scope.formValues.Labels.push({ name: '', value: ''});
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -203,6 +217,18 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
 | 
			
		|||
    config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function prepareSecretConfig(config, input) {
 | 
			
		||||
    if (input.Secrets) {
 | 
			
		||||
      var secrets = [];
 | 
			
		||||
      angular.forEach(input.Secrets, function(secret) {
 | 
			
		||||
        if (secret.model) {
 | 
			
		||||
          secrets.push(SecretHelper.secretConfig(secret.model));
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      config.TaskTemplate.ContainerSpec.Secrets = secrets;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function prepareConfiguration() {
 | 
			
		||||
    var input = $scope.formValues;
 | 
			
		||||
    var config = {
 | 
			
		||||
| 
						 | 
				
			
			@ -225,6 +251,7 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
 | 
			
		|||
    prepareVolumes(config, input);
 | 
			
		||||
    prepareNetworks(config, input);
 | 
			
		||||
    prepareUpdateConfig(config, input);
 | 
			
		||||
    prepareSecretConfig(config, input);
 | 
			
		||||
    preparePlacementConfig(config, input);
 | 
			
		||||
    return config;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -277,20 +304,22 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  function initView() {
 | 
			
		||||
    Volume.query({}, function (d) {
 | 
			
		||||
      $scope.availableVolumes = d.Volumes;
 | 
			
		||||
    }, function (e) {
 | 
			
		||||
      Notifications.error('Failure', e, 'Unable to retrieve volumes');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    Network.query({}, function (d) {
 | 
			
		||||
      $scope.availableNetworks = d.filter(function (network) {
 | 
			
		||||
        if (network.Scope === 'swarm') {
 | 
			
		||||
          return network;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }, function (e) {
 | 
			
		||||
      Notifications.error('Failure', e, 'Unable to retrieve networks');
 | 
			
		||||
    $('#loadingViewSpinner').show();
 | 
			
		||||
    $q.all({
 | 
			
		||||
      volumes: VolumeService.volumes(),
 | 
			
		||||
      networks: NetworkService.retrieveSwarmNetworks(),
 | 
			
		||||
      secrets: SecretService.secrets()
 | 
			
		||||
    })
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      $scope.availableVolumes = data.volumes;
 | 
			
		||||
      $scope.availableNetworks = data.networks;
 | 
			
		||||
      $scope.availableSecrets = data.secrets;
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      Notifications.error('Failure', err, 'Unable to initialize view');
 | 
			
		||||
    })
 | 
			
		||||
    .finally(function final() {
 | 
			
		||||
      $('#loadingViewSpinner').hide();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
<rd-header>
 | 
			
		||||
  <rd-header-title title="Create service"></rd-header-title>
 | 
			
		||||
  <rd-header-title title="Create service">
 | 
			
		||||
    <i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
 | 
			
		||||
  </rd-header-title>
 | 
			
		||||
  <rd-header-content>
 | 
			
		||||
    <a ui-sref="services">Services</a> > Add service
 | 
			
		||||
  </rd-header-content>
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +142,7 @@
 | 
			
		|||
          <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>
 | 
			
		||||
          <li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
 | 
			
		||||
          <li class="interactive"><a data-target="#secrets" data-toggle="tab" ng-if="applicationState.endpoint.apiVersion >= 1.25">Secrets</a></li>
 | 
			
		||||
          <li class="interactive"><a data-target="#placement" data-toggle="tab">Placement</a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <!-- tab-content -->
 | 
			
		||||
| 
						 | 
				
			
			@ -422,7 +425,9 @@
 | 
			
		|||
            </form>
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- !tab-update-config -->
 | 
			
		||||
 | 
			
		||||
          <!-- tab-secrets -->
 | 
			
		||||
          <div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/components/createService/includes/secret.html'"></div>
 | 
			
		||||
          <!-- !tab-secrets -->
 | 
			
		||||
          <!-- tab-placement -->
 | 
			
		||||
          <div class="tab-pane" id="placement" ng-include="'app/components/createService/includes/placement.html'"></div>
 | 
			
		||||
          <!-- !tab-placement -->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
  <div class="form-group">
 | 
			
		||||
    <div class="col-sm-12" style="margin-top: 5px;">
 | 
			
		||||
      <label class="control-label text-left">Placement constraints</label>
 | 
			
		||||
      <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint(service)">
 | 
			
		||||
      <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint()">
 | 
			
		||||
        <i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
<form class="form-horizontal" style="margin-top: 15px;">
 | 
			
		||||
  <div class="form-group">
 | 
			
		||||
    <div class="col-sm-12 small text-muted">
 | 
			
		||||
      Secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers.
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="form-group">
 | 
			
		||||
    <div class="col-sm-12" style="margin-top: 5px;">
 | 
			
		||||
      <label class="control-label text-left">Secrets</label>
 | 
			
		||||
      <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSecret()">
 | 
			
		||||
        <i class="fa fa-plus-circle" aria-hidden="true"></i> add a secret
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-sm-12 form-inline" style="margin-top: 10px;">
 | 
			
		||||
      <div ng-repeat="secret in formValues.Secrets" style="margin-top: 2px;">
 | 
			
		||||
        <div class="input-group col-sm-4 input-group-sm">
 | 
			
		||||
          <span class="input-group-addon">secret</span>
 | 
			
		||||
          <select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets">
 | 
			
		||||
            <option value="" selected="selected">Select a secret</option>
 | 
			
		||||
          </select>
 | 
			
		||||
        </div>
 | 
			
		||||
        <button class="btn btn-sm btn-danger" type="button" ng-click="removeSecret($index)">
 | 
			
		||||
          <i class="fa fa-trash" aria-hidden="true"></i>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</form>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
<rd-header>
 | 
			
		||||
  <rd-header-title title="Secret details">
 | 
			
		||||
    <a data-toggle="tooltip" title="Refresh" ui-sref="secret({id: secret.Id})" ui-sref-opts="{reload: true}">
 | 
			
		||||
      <i class="fa fa-refresh" aria-hidden="true"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
    <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
 | 
			
		||||
  </rd-header-title>
 | 
			
		||||
  <rd-header-content>
 | 
			
		||||
    <a ui-sref="secrets">Secrets</a> > <a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a>
 | 
			
		||||
  </rd-header-content>
 | 
			
		||||
</rd-header>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col-lg-12 col-md-12 col-xs-12">
 | 
			
		||||
    <rd-widget>
 | 
			
		||||
      <rd-widget-header icon="fa-user-secret" title="Secret details"></rd-widget-header>
 | 
			
		||||
      <rd-widget-body classes="no-padding">
 | 
			
		||||
        <table class="table">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td>Name</td>
 | 
			
		||||
              <td>{{ secret.Name }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td>ID</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                {{ secret.Id }}
 | 
			
		||||
                <button class="btn btn-xs btn-danger" ng-click="removeSecret(secret.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this secret</button>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td>Created</td>
 | 
			
		||||
              <td>{{ secret.CreatedAt | getisodate }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td>Last updated</td>
 | 
			
		||||
              <td>{{ secret.UpdatedAt | getisodate }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr ng-if="!(secret.Labels | emptyobject)">
 | 
			
		||||
              <td>Labels</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <table class="table table-bordered table-condensed">
 | 
			
		||||
                  <tr ng-repeat="(k, v) in secret.Labels">
 | 
			
		||||
                    <td>{{ k }}</td>
 | 
			
		||||
                    <td>{{ v }}</td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </table>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </rd-widget-body>
 | 
			
		||||
    </rd-widget>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
angular.module('secret', [])
 | 
			
		||||
.controller('SecretController', ['$scope', '$stateParams', '$state', 'SecretService', 'Notifications',
 | 
			
		||||
function ($scope, $stateParams, $state, SecretService, Notifications) {
 | 
			
		||||
 | 
			
		||||
  $scope.removeSecret = function removeSecret(secretId) {
 | 
			
		||||
    $('#loadingViewSpinner').show();
 | 
			
		||||
    SecretService.remove(secretId)
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      Notifications.success('Secret successfully removed');
 | 
			
		||||
      $state.go('secrets', {});
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      Notifications.error('Failure', err, 'Unable to remove secret');
 | 
			
		||||
    })
 | 
			
		||||
    .finally(function final() {
 | 
			
		||||
      $('#loadingViewSpinner').hide();
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function initView() {
 | 
			
		||||
    $('#loadingViewSpinner').show();
 | 
			
		||||
    SecretService.secret($stateParams.id)
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      $scope.secret = data;
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      Notifications.error('Failure', err, 'Unable to retrieve secret details');
 | 
			
		||||
    })
 | 
			
		||||
    .finally(function final() {
 | 
			
		||||
      $('#loadingViewSpinner').hide();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initView();
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
<rd-header>
 | 
			
		||||
  <rd-header-title title="Secrets list">
 | 
			
		||||
    <a data-toggle="tooltip" title="Refresh" ui-sref="secrets" ui-sref-opts="{reload: true}">
 | 
			
		||||
      <i class="fa fa-refresh" aria-hidden="true"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
    <i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
 | 
			
		||||
  </rd-header-title>
 | 
			
		||||
  <rd-header-content>Secrets</rd-header-content>
 | 
			
		||||
</rd-header>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col-lg-12 col-md-12 col-xs-12">
 | 
			
		||||
    <rd-widget>
 | 
			
		||||
      <rd-widget-header icon="fa-user-secret" title="Secrets">
 | 
			
		||||
      </rd-widget-header>
 | 
			
		||||
      <rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
 | 
			
		||||
        <div class="pull-left">
 | 
			
		||||
          <button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
 | 
			
		||||
          <a class="btn btn-primary" type="button" ui-sref="actions.create.secret">Add secret</a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pull-right">
 | 
			
		||||
          <input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </rd-widget-taskbar>
 | 
			
		||||
      <rd-widget-body classes="no-padding">
 | 
			
		||||
        <div class="table-responsive">
 | 
			
		||||
          <table class="table table-hover">
 | 
			
		||||
            <thead>
 | 
			
		||||
              <th>
 | 
			
		||||
                <input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
 | 
			
		||||
              </th>
 | 
			
		||||
              <th>
 | 
			
		||||
                <a ui-sref="secrets" ng-click="order('Name')">
 | 
			
		||||
                  Name
 | 
			
		||||
                  <span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
 | 
			
		||||
                  <span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
 | 
			
		||||
                </a>
 | 
			
		||||
              </th>
 | 
			
		||||
              <th>
 | 
			
		||||
                <a ui-sref="secrets" ng-click="order('CreatedAt')">
 | 
			
		||||
                  Created at
 | 
			
		||||
                  <span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
 | 
			
		||||
                  <span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
 | 
			
		||||
                </a>
 | 
			
		||||
              </th>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
 | 
			
		||||
                <td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td>
 | 
			
		||||
                <td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
 | 
			
		||||
                <td>{{ secret.CreatedAt | getisodate }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr ng-if="!secrets">
 | 
			
		||||
                <td colspan="3" class="text-center text-muted">Loading...</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr ng-if="secrets.length == 0">
 | 
			
		||||
                <td colspan="3" class="text-center text-muted">No secrets available.</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
          <div ng-if="secrets" class="pull-left pagination-controls">
 | 
			
		||||
            <dir-pagination-controls></dir-pagination-controls>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </rd-widget-body>
 | 
			
		||||
    <rd-widget>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
angular.module('secrets', [])
 | 
			
		||||
.controller('SecretsController', ['$scope', '$stateParams', '$state', 'SecretService', 'Notifications', 'Pagination',
 | 
			
		||||
function ($scope, $stateParams, $state, SecretService, Notifications, Pagination) {
 | 
			
		||||
  $scope.state = {};
 | 
			
		||||
  $scope.state.selectedItemCount = 0;
 | 
			
		||||
  $scope.state.pagination_count = Pagination.getPaginationCount('secrets');
 | 
			
		||||
  $scope.sortType = 'Name';
 | 
			
		||||
  $scope.sortReverse = false;
 | 
			
		||||
 | 
			
		||||
  $scope.order = function (sortType) {
 | 
			
		||||
    $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
 | 
			
		||||
    $scope.sortType = sortType;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.selectItems = function (allSelected) {
 | 
			
		||||
    angular.forEach($scope.state.filteredSecrets, function (secret) {
 | 
			
		||||
      if (secret.Checked !== allSelected) {
 | 
			
		||||
        secret.Checked = allSelected;
 | 
			
		||||
        $scope.selectItem(secret);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.selectItem = function (item) {
 | 
			
		||||
    if (item.Checked) {
 | 
			
		||||
      $scope.state.selectedItemCount++;
 | 
			
		||||
    } else {
 | 
			
		||||
      $scope.state.selectedItemCount--;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $scope.removeAction = function () {
 | 
			
		||||
    $('#loadingViewSpinner').show();
 | 
			
		||||
    var counter = 0;
 | 
			
		||||
    var complete = function () {
 | 
			
		||||
      counter = counter - 1;
 | 
			
		||||
      if (counter === 0) {
 | 
			
		||||
        $('#loadingViewSpinner').hide();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    angular.forEach($scope.secrets, function (secret) {
 | 
			
		||||
      if (secret.Checked) {
 | 
			
		||||
        counter = counter + 1;
 | 
			
		||||
        SecretService.remove(secret.Id)
 | 
			
		||||
        .then(function success() {
 | 
			
		||||
          Notifications.success('Secret deleted', secret.Id);
 | 
			
		||||
          var index = $scope.secrets.indexOf(secret);
 | 
			
		||||
          $scope.secrets.splice(index, 1);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function error(err) {
 | 
			
		||||
          Notifications.error('Failure', err, 'Unable to remove secret');
 | 
			
		||||
        })
 | 
			
		||||
        .finally(function final() {
 | 
			
		||||
          complete();
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function initView() {
 | 
			
		||||
    $('#loadingViewSpinner').show();
 | 
			
		||||
    SecretService.secrets()
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      $scope.secrets = data;
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      $scope.secrets = [];
 | 
			
		||||
      Notifications.error('Failure', err, 'Unable to retrieve secrets');
 | 
			
		||||
    })
 | 
			
		||||
    .finally(function final() {
 | 
			
		||||
      $('#loadingViewSpinner').hide();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initView();
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
<div ng-if="applicationState.endpoint.apiVersion >= 1.25">
 | 
			
		||||
  <rd-widget>
 | 
			
		||||
    <rd-widget-header icon="fa-tasks" title="Secrets">
 | 
			
		||||
    </rd-widget-header>
 | 
			
		||||
    <rd-widget-body classes="no-padding">
 | 
			
		||||
      <div class="form-inline" style="padding: 10px;">
 | 
			
		||||
        Add a secret:
 | 
			
		||||
        <select class="form-control" ng-options="secret.Name for secret in secrets" ng-model="newSecret">
 | 
			
		||||
          <option selected disabled hidden value="">Select a secret</option>
 | 
			
		||||
        </select>
 | 
			
		||||
        <a class="btn btn-default btn-sm" ng-click="addSecret(service, newSecret)">
 | 
			
		||||
          <i class="fa fa-plus-circle" aria-hidden="true"></i> add secret
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <table class="table" style="margin-top: 5px;">
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>Name</th>
 | 
			
		||||
            <th>File name</th>
 | 
			
		||||
            <th>UID</th>
 | 
			
		||||
            <th>GID</th>
 | 
			
		||||
            <th>Mode</th>
 | 
			
		||||
            <th></th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr ng-repeat="secret in service.ServiceSecrets">
 | 
			
		||||
            <td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
 | 
			
		||||
            <td>{{ secret.FileName }}</td>
 | 
			
		||||
            <td>{{ secret.Uid }}</td>
 | 
			
		||||
            <td>{{ secret.Gid }}</td>
 | 
			
		||||
            <td>{{ secret.Mode }}</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <button class="btn btn-xs btn-danger pull-right" type="button" ng-click="removeSecret(service, $index)" ng-disabled="isUpdating">
 | 
			
		||||
                <i class="fa fa-trash" aria-hidden="true"></i> Remove secret
 | 
			
		||||
              </button>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr ng-if="service.ServiceSecrets.length === 0">
 | 
			
		||||
            <td colspan="6" class="text-center text-muted">No secrets associated to this service.</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, ['ServiceSecrets'])" 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, ['ServiceSecrets'])">Reset changes</a></li>
 | 
			
		||||
            <li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </rd-widget-footer>
 | 
			
		||||
  </rd-widget>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +109,7 @@
 | 
			
		|||
          <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-labels')">Service labels</a></li>
 | 
			
		||||
          <li><a href ng-click="goToItem('service-secrets')">Secrets</a></li>
 | 
			
		||||
          <li><a href ng-click="goToItem('service-tasks')">Tasks</a></li>
 | 
			
		||||
        <ul>
 | 
			
		||||
      </rd-widget-body>
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +148,7 @@
 | 
			
		|||
    <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-labels" class="padding-top" ng-include="'app/components/service/includes/servicelabels.html'"></div>
 | 
			
		||||
    <div id="service-secrets" class="padding-top" ng-include="'app/components/service/includes/secrets.html'"></div>
 | 
			
		||||
    <div id="service-tasks" class="padding-top" ng-include="'app/components/service/includes/tasks.html'"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
angular.module('service', [])
 | 
			
		||||
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'ServiceService', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
 | 
			
		||||
function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceService, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
 | 
			
		||||
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
 | 
			
		||||
function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
 | 
			
		||||
 | 
			
		||||
  $scope.state = {};
 | 
			
		||||
  $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +55,18 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
 | 
			
		|||
      updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  $scope.addSecret = function addSecret(service, secret) {
 | 
			
		||||
    if (secret && service.ServiceSecrets.filter(function(serviceSecret) { return serviceSecret.Id === secret.Id;}).length === 0) {
 | 
			
		||||
      service.ServiceSecrets.push({ Id: secret.Id, Name: secret.Name, FileName: secret.Name, Uid: '0', Gid: '0', Mode: 444 });
 | 
			
		||||
      updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  $scope.removeSecret = function removeSecret(service, index) {
 | 
			
		||||
    var removedElement = service.ServiceSecrets.splice(index, 1);
 | 
			
		||||
    if (removedElement !== null) {
 | 
			
		||||
      updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  $scope.addLabel = function addLabel(service) {
 | 
			
		||||
    service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
 | 
			
		||||
    updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
 | 
			
		||||
| 
						 | 
				
			
			@ -162,7 +174,7 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
 | 
			
		|||
    config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
 | 
			
		||||
    config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
 | 
			
		||||
    config.TaskTemplate.ContainerSpec.Image = service.Image;
 | 
			
		||||
    config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets;
 | 
			
		||||
    config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
 | 
			
		||||
 | 
			
		||||
    if (service.Mode === 'replicated') {
 | 
			
		||||
      config.Mode.Replicated.Replicas = service.Replicas;
 | 
			
		||||
| 
						 | 
				
			
			@ -246,7 +258,7 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  function translateServiceArrays(service) {
 | 
			
		||||
    service.ServiceSecrets = service.Secrets;
 | 
			
		||||
    service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
 | 
			
		||||
    service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
 | 
			
		||||
    service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
 | 
			
		||||
    service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
 | 
			
		||||
| 
						 | 
				
			
			@ -272,14 +284,19 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
 | 
			
		|||
 | 
			
		||||
      return $q.all({
 | 
			
		||||
        tasks: TaskService.serviceTasks(service.Name),
 | 
			
		||||
        nodes: NodeService.nodes()
 | 
			
		||||
        nodes: NodeService.nodes(),
 | 
			
		||||
        secrets: Secret.query({}).$promise
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      $scope.tasks = data.tasks;
 | 
			
		||||
      $scope.nodes = data.nodes;
 | 
			
		||||
      $scope.secrets = data.secrets.map(function (secret) {
 | 
			
		||||
        return new SecretViewModel(secret);
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      $scope.secrets = [];
 | 
			
		||||
      Notifications.error('Failure', err, 'Unable to retrieve service details');
 | 
			
		||||
    })
 | 
			
		||||
    .finally(function final() {
 | 
			
		||||
| 
						 | 
				
			
			@ -287,6 +304,20 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
 | 
			
		|||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function fetchSecrets() {
 | 
			
		||||
    $('#loadSecretsSpinner').show();
 | 
			
		||||
    Secret.query({}, function (d) {
 | 
			
		||||
      $scope.secrets = d.map(function (secret) {
 | 
			
		||||
        return new SecretViewModel(secret);
 | 
			
		||||
      });
 | 
			
		||||
      $('#loadSecretsSpinner').hide();
 | 
			
		||||
    }, function(e) {
 | 
			
		||||
      $('#loadSecretsSpinner').hide();
 | 
			
		||||
      Notifications.error('Failure', e, 'Unable to retrieve secrets');
 | 
			
		||||
      $scope.secrets = [];
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $scope.updateServiceAttribute = function updateServiceAttribute(service, name) {
 | 
			
		||||
    if (service[name] !== originalService[name] || !(name in originalService)) {
 | 
			
		||||
      service.hasChanges = true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,9 @@
 | 
			
		|||
    <li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
 | 
			
		||||
      <a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
 | 
			
		||||
      <a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li class="sidebar-list">
 | 
			
		||||
      <a ui-sref="containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server"></span></a>
 | 
			
		||||
    </li>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -163,7 +163,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, Config, Cont
 | 
			
		|||
      $q.all({
 | 
			
		||||
        templates: TemplateService.getTemplates(templatesKey),
 | 
			
		||||
        containers: ContainerService.getContainers(0, c.hiddenLabels),
 | 
			
		||||
        networks: NetworkService.getNetworks(),
 | 
			
		||||
        networks: NetworkService.networks(),
 | 
			
		||||
        volumes: VolumeService.getVolumes()
 | 
			
		||||
      })
 | 
			
		||||
      .then(function success(data) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
angular.module('portainer.helpers')
 | 
			
		||||
.factory('SecretHelper', [function SecretHelperFactory() {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  return {
 | 
			
		||||
    flattenSecret: function(secret) {
 | 
			
		||||
      if (secret) {
 | 
			
		||||
        return {
 | 
			
		||||
          Id: secret.SecretID,
 | 
			
		||||
          Name: secret.SecretName,
 | 
			
		||||
          FileName: secret.File.Name,
 | 
			
		||||
          Uid: secret.File.UID,
 | 
			
		||||
          Gid: secret.File.GID,
 | 
			
		||||
          Mode: secret.File.Mode
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
    },
 | 
			
		||||
    secretConfig: function(secret) {
 | 
			
		||||
      if (secret) {
 | 
			
		||||
        return {
 | 
			
		||||
          SecretID: secret.Id,
 | 
			
		||||
          SecretName: secret.Name,
 | 
			
		||||
          File: {
 | 
			
		||||
            Name: secret.Name,
 | 
			
		||||
            UID: '0',
 | 
			
		||||
            GID: '0',
 | 
			
		||||
            Mode: 444
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
function SecretViewModel(data) {
 | 
			
		||||
  this.Id = data.ID;
 | 
			
		||||
  this.CreatedAt = data.CreatedAt;
 | 
			
		||||
  this.UpdatedAt = data.UpdatedAt;
 | 
			
		||||
  this.Version = data.Version.Index;
 | 
			
		||||
  this.Name = data.Spec.Name;
 | 
			
		||||
  this.Labels = data.Spec.Labels;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
angular.module('portainer.rest')
 | 
			
		||||
.factory('Secret', ['$resource', 'Settings', 'EndpointProvider', function SecretFactory($resource, Settings, EndpointProvider) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  return $resource(Settings.url + '/:endpointId/secrets/:id/:action', {
 | 
			
		||||
    endpointId: EndpointProvider.endpointID
 | 
			
		||||
  }, {
 | 
			
		||||
    get: { method: 'GET', params: {id: '@id'} },
 | 
			
		||||
    query: { method: 'GET', isArray: true },
 | 
			
		||||
    create: { method: 'POST', params: {action: 'create'} },
 | 
			
		||||
    remove: { method: 'DELETE', params: {id: '@id'} }
 | 
			
		||||
  });
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			@ -3,10 +3,29 @@ angular.module('portainer.services')
 | 
			
		|||
  'use strict';
 | 
			
		||||
  var service = {};
 | 
			
		||||
 | 
			
		||||
  service.getNetworks = function() {
 | 
			
		||||
  service.networks = function() {
 | 
			
		||||
    return Network.query({}).$promise;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  service.retrieveSwarmNetworks = function() {
 | 
			
		||||
    var deferred = $q.defer();
 | 
			
		||||
 | 
			
		||||
    service.networks()
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      var networks = data.filter(function (network) {
 | 
			
		||||
        if (network.Scope === 'swarm') {
 | 
			
		||||
          return network;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      deferred.resolve(networks);
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      deferred.reject({msg: 'Unable to retrieve networks', err: err});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return deferred.promise;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  service.filterGlobalNetworks = function(networks) {
 | 
			
		||||
    return networks.filter(function (network) {
 | 
			
		||||
      if (network.Scope === 'global') {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
angular.module('portainer.services')
 | 
			
		||||
.factory('SecretService', ['$q', 'Secret', function SecretServiceFactory($q, Secret) {
 | 
			
		||||
  'use strict';
 | 
			
		||||
  var service = {};
 | 
			
		||||
 | 
			
		||||
  service.secret = function(secretId) {
 | 
			
		||||
    var deferred = $q.defer();
 | 
			
		||||
 | 
			
		||||
    Secret.get({id: secretId}).$promise
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      var secret = new SecretViewModel(data);
 | 
			
		||||
      deferred.resolve(secret);
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      deferred.reject({ msg: 'Unable to retrieve secret details', err: err });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return deferred.promise;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  service.secrets = function() {
 | 
			
		||||
    var deferred = $q.defer();
 | 
			
		||||
 | 
			
		||||
    Secret.query({}).$promise
 | 
			
		||||
    .then(function success(data) {
 | 
			
		||||
      var secrets = data.map(function (item) {
 | 
			
		||||
        return new SecretViewModel(item);
 | 
			
		||||
      });
 | 
			
		||||
      deferred.resolve(secrets);
 | 
			
		||||
    })
 | 
			
		||||
    .catch(function error(err) {
 | 
			
		||||
      deferred.reject({ msg: 'Unable to retrieve secrets', err: err });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return deferred.promise;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  service.remove = function(secretId) {
 | 
			
		||||
    return Secret.remove({ id: secretId }).$promise;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  service.create = function(secretConfig) {
 | 
			
		||||
    return Secret.create(secretConfig).$promise;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return service;
 | 
			
		||||
}]);
 | 
			
		||||
		Loading…
	
		Reference in New Issue