mirror of https://github.com/portainer/portainer
				
				
				
			feat(docker/services): Add the ability to edit a service networks (#3957)
* feat(services): update services details view * feat(services): Add the ability to edit a service networks * feat(services): show ingress network * refactor(services): use lodash * feat(networks): disable sending when updating * feat(networks): limit size of select * feat(services): update networks only when network is new * feat(services): prevent submitting of empty networks * feat(services): show unique networks * fix(service): use empty array default for networks * feat(service): show only swarm networks * feat(services): show placeholder for network * feat(services): show spaced select box * feat(services): show macvlan ip * feat(service): fetch the network subnet * feat(services): show empty ip when network is not connected Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>pull/4169/head
							parent
							
								
									e4ca58a042
								
							
						
					
					
						commit
						d85708f6ea
					
				| 
						 | 
				
			
			@ -10,6 +10,7 @@ export function NetworkViewModel(data) {
 | 
			
		|||
  this.IPAM = data.IPAM;
 | 
			
		||||
  this.Containers = data.Containers;
 | 
			
		||||
  this.Options = data.Options;
 | 
			
		||||
  this.Ingress = data.Ingress;
 | 
			
		||||
 | 
			
		||||
  this.Labels = data.Labels;
 | 
			
		||||
  if (this.Labels && this.Labels['com.docker.compose.project']) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,6 @@ angular.module('portainer.docker').factory('NetworkService', [
 | 
			
		|||
      Network.query({ filters: filters })
 | 
			
		||||
        .$promise.then(function success(data) {
 | 
			
		||||
          var networks = data;
 | 
			
		||||
 | 
			
		||||
          var filteredNetworks = networks
 | 
			
		||||
            .filter(function (network) {
 | 
			
		||||
              if (localNetworks && network.Scope === 'local') {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,74 @@
 | 
			
		|||
<div id="service-network-specs">
 | 
			
		||||
  <rd-widget>
 | 
			
		||||
    <rd-widget-header icon="fa-tasks" title-text="Networks"></rd-widget-header>
 | 
			
		||||
    <rd-widget-body ng-if="!service.VirtualIPs || service.VirtualIPs.length === 0">
 | 
			
		||||
    <rd-widget-header icon="fa-tasks" title-text="Networks">
 | 
			
		||||
      <div class="nopadding" authorization="DockerServiceUpdate">
 | 
			
		||||
        <a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addNetwork(service)" ng-disabled="isUpdating">
 | 
			
		||||
          <i class="fa fa-plus-circle" aria-hidden="true"></i> network
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </rd-widget-header>
 | 
			
		||||
    <rd-widget-body ng-if="!service.Networks || service.Networks.length === 0">
 | 
			
		||||
      <p>This service is not connected to any networks.</p>
 | 
			
		||||
    </rd-widget-body>
 | 
			
		||||
    <rd-widget-body ng-if="service.VirtualIPs && service.VirtualIPs.length > 0" classes="no-padding">
 | 
			
		||||
    <rd-widget-body ng-if="service.Networks && service.Networks.length > 0" classes="no-padding">
 | 
			
		||||
      <table class="table">
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>Name</th>
 | 
			
		||||
            <th>ID</th>
 | 
			
		||||
            <th>IP address</th>
 | 
			
		||||
            <th>Actions</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr ng-repeat="network in service.VirtualIPs">
 | 
			
		||||
          <tr ng-repeat="network in service.Networks">
 | 
			
		||||
            <td>
 | 
			
		||||
              <a ui-sref="docker.networks.network({id: network.NetworkID})">{{ network.NetworkID }}</a>
 | 
			
		||||
              <select
 | 
			
		||||
                ng-if="network.Editable"
 | 
			
		||||
                class="form-control"
 | 
			
		||||
                ng-model="network.Id"
 | 
			
		||||
                ng-change="updateNetwork(service)"
 | 
			
		||||
                ng-options="net.Id as net.Name for net in filterNetworks(swarmNetworks, network)"
 | 
			
		||||
                disable-authorization="DockerServiceUpdate"
 | 
			
		||||
                style="width: initial; min-width: 50%;"
 | 
			
		||||
              >
 | 
			
		||||
                <option disabled value="" selected>Select a network</option>
 | 
			
		||||
              </select>
 | 
			
		||||
              <span ng-if="!network.Editable">{{ network.Name }}</span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>{{ network.Addr }}</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <a ui-sref="docker.networks.network({id: network.Id})">{{ network.Id }}</a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>
 | 
			
		||||
              {{ network.Addr }}
 | 
			
		||||
            </td>
 | 
			
		||||
            <td ng-if="network.Editable" authorization="DockerServiceUpdate">
 | 
			
		||||
              <span class="input-group-btn">
 | 
			
		||||
                <button class="btn btn-sm btn-danger" type="button" ng-click="removeNetwork(service, $index)" ng-disabled="isUpdating">
 | 
			
		||||
                  <i class="fa fa-trash" aria-hidden="true"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
              </span>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td ng-if="!network.Editable"></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </rd-widget-body>
 | 
			
		||||
    <rd-widget-footer authorization="DockerServiceUpdate">
 | 
			
		||||
      <div class="btn-toolbar" role="toolbar">
 | 
			
		||||
        <div class="btn-group" role="group">
 | 
			
		||||
          <button type="button" class="btn btn-primary btn-sm" ng-disabled="isUpdating || !hasChanges(service, ['Networks'])" 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, ['Networks'])">Reset changes</a></li>
 | 
			
		||||
            <li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </rd-widget-footer>
 | 
			
		||||
  </rd-widget>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ require('./includes/servicelabels.html');
 | 
			
		|||
require('./includes/tasks.html');
 | 
			
		||||
require('./includes/updateconfig.html');
 | 
			
		||||
 | 
			
		||||
import _ from 'lodash-es';
 | 
			
		||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
 | 
			
		||||
 | 
			
		||||
angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +52,7 @@ angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		|||
  'EndpointProvider',
 | 
			
		||||
  'clipboard',
 | 
			
		||||
  'WebhookHelper',
 | 
			
		||||
  'NetworkService',
 | 
			
		||||
  function (
 | 
			
		||||
    $q,
 | 
			
		||||
    $scope,
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +84,8 @@ angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		|||
    WebhookService,
 | 
			
		||||
    EndpointProvider,
 | 
			
		||||
    clipboard,
 | 
			
		||||
    WebhookHelper
 | 
			
		||||
    WebhookHelper,
 | 
			
		||||
    NetworkService
 | 
			
		||||
  ) {
 | 
			
		||||
    $scope.state = {
 | 
			
		||||
      updateInProgress: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -210,6 +213,25 @@ angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		|||
    $scope.updateMount = function updateMount(service) {
 | 
			
		||||
      updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.addNetwork = function addNetwork(service) {
 | 
			
		||||
      if (!service.Networks) {
 | 
			
		||||
        service.Networks = [];
 | 
			
		||||
      }
 | 
			
		||||
      service.Networks.push({ Editable: true });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.removeNetwork = function removeNetwork(service, index) {
 | 
			
		||||
      var removedElement = service.Networks.splice(index, 1);
 | 
			
		||||
      if (removedElement && removedElement.length && removedElement[0].Id) {
 | 
			
		||||
        updateServiceArray(service, 'Networks', service.Networks);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.updateNetwork = function updateNetwork(service) {
 | 
			
		||||
      updateServiceArray(service, 'Networks', service.Networks);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.addPlacementConstraint = function addPlacementConstraint(service) {
 | 
			
		||||
      service.ServiceConstraints.push({ key: '', operator: '==', value: '' });
 | 
			
		||||
      updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
 | 
			
		||||
| 
						 | 
				
			
			@ -370,6 +392,14 @@ angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		|||
        config.TaskTemplate.ContainerSpec.Image = service.Image;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if ($scope.hasChanges(service, ['Networks'])) {
 | 
			
		||||
        config.Networks = _.map(
 | 
			
		||||
          _.filter(service.Networks, (item) => item.Id && item.Editable),
 | 
			
		||||
          (item) => ({ Target: item.Id })
 | 
			
		||||
        );
 | 
			
		||||
        config.TaskTemplate.Networks = config.Networks;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
 | 
			
		||||
      config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : [];
 | 
			
		||||
      config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : [];
 | 
			
		||||
| 
						 | 
				
			
			@ -629,11 +659,12 @@ angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		|||
            configs: apiVersion >= 1.3 ? ConfigService.configs() : [],
 | 
			
		||||
            availableImages: ImageService.images(),
 | 
			
		||||
            availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
 | 
			
		||||
            availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
 | 
			
		||||
            settings: SettingsService.publicSettings(),
 | 
			
		||||
            webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
 | 
			
		||||
          });
 | 
			
		||||
        })
 | 
			
		||||
        .then(function success(data) {
 | 
			
		||||
        .then(async function success(data) {
 | 
			
		||||
          $scope.nodes = data.nodes;
 | 
			
		||||
          $scope.configs = data.configs;
 | 
			
		||||
          $scope.secrets = data.secrets;
 | 
			
		||||
| 
						 | 
				
			
			@ -642,6 +673,36 @@ angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		|||
          $scope.availableVolumes = data.volumes;
 | 
			
		||||
          $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
 | 
			
		||||
          $scope.isAdmin = Authentication.isAdmin();
 | 
			
		||||
          $scope.availableNetworks = data.availableNetworks;
 | 
			
		||||
          $scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm');
 | 
			
		||||
 | 
			
		||||
          const serviceNetworks = _.uniqBy(_.concat($scope.service.Model.Spec.Networks || [], $scope.service.Model.Spec.TaskTemplate.Networks || []), 'Target');
 | 
			
		||||
          const networks = _.filter(
 | 
			
		||||
            _.map(serviceNetworks, ({ Target }) => _.find(data.availableNetworks, { Id: Target })),
 | 
			
		||||
            Boolean
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (_.some($scope.service.Ports, (port) => port.PublishMode === 'ingress')) {
 | 
			
		||||
            const ingressNetwork = _.find($scope.availableNetworks, (network) => network.Ingress);
 | 
			
		||||
            if (ingressNetwork) {
 | 
			
		||||
              networks.unshift(ingressNetwork);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          $scope.service.Networks = await Promise.all(
 | 
			
		||||
            _.map(networks, async (item) => {
 | 
			
		||||
              let addr = '';
 | 
			
		||||
              if (item.IPAM.Config.length) {
 | 
			
		||||
                addr = item.IPAM.Config[0].Subnet;
 | 
			
		||||
              } else {
 | 
			
		||||
                const network = await NetworkService.network(item.Id);
 | 
			
		||||
                addr = (network && network.IPAM && network.IPAM.Config && network.IPAM.Config.length && network.IPAM.Config[0].Subnet) || '';
 | 
			
		||||
              }
 | 
			
		||||
              return { Id: item.Id, Name: item.Name, Addr: addr, Editable: !item.Ingress };
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          originalService.Networks = angular.copy($scope.service.Networks);
 | 
			
		||||
 | 
			
		||||
          if (data.webhooks.length > 0) {
 | 
			
		||||
            var webhook = data.webhooks[0];
 | 
			
		||||
| 
						 | 
				
			
			@ -699,6 +760,11 @@ angular.module('portainer.docker').controller('ServiceController', [
 | 
			
		|||
      previousServiceValues.push(name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $scope.filterNetworks = filterNetworks;
 | 
			
		||||
    function filterNetworks(networks, current) {
 | 
			
		||||
      return networks.filter((network) => !network.Ingress && (network.Id === current.Id || $scope.service.Networks.every((serviceNetwork) => network.Id !== serviceNetwork.Id)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function updateServiceArray(service, name) {
 | 
			
		||||
      previousServiceValues.push(name);
 | 
			
		||||
      service.hasChanges = true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue