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.IPAM = data.IPAM;
|
||||||
this.Containers = data.Containers;
|
this.Containers = data.Containers;
|
||||||
this.Options = data.Options;
|
this.Options = data.Options;
|
||||||
|
this.Ingress = data.Ingress;
|
||||||
|
|
||||||
this.Labels = data.Labels;
|
this.Labels = data.Labels;
|
||||||
if (this.Labels && this.Labels['com.docker.compose.project']) {
|
if (this.Labels && this.Labels['com.docker.compose.project']) {
|
||||||
|
|
|
@ -41,7 +41,6 @@ angular.module('portainer.docker').factory('NetworkService', [
|
||||||
Network.query({ filters: filters })
|
Network.query({ filters: filters })
|
||||||
.$promise.then(function success(data) {
|
.$promise.then(function success(data) {
|
||||||
var networks = data;
|
var networks = data;
|
||||||
|
|
||||||
var filteredNetworks = networks
|
var filteredNetworks = networks
|
||||||
.filter(function (network) {
|
.filter(function (network) {
|
||||||
if (localNetworks && network.Scope === 'local') {
|
if (localNetworks && network.Scope === 'local') {
|
||||||
|
|
|
@ -1,26 +1,74 @@
|
||||||
<div id="service-network-specs">
|
<div id="service-network-specs">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-tasks" title-text="Networks"></rd-widget-header>
|
<rd-widget-header icon="fa-tasks" title-text="Networks">
|
||||||
<rd-widget-body ng-if="!service.VirtualIPs || service.VirtualIPs.length === 0">
|
<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>
|
<p>This service is not connected to any networks.</p>
|
||||||
</rd-widget-body>
|
</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">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>IP address</th>
|
<th>IP address</th>
|
||||||
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="network in service.VirtualIPs">
|
<tr ng-repeat="network in service.Networks">
|
||||||
<td>
|
<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>
|
||||||
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</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>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,7 @@ require('./includes/servicelabels.html');
|
||||||
require('./includes/tasks.html');
|
require('./includes/tasks.html');
|
||||||
require('./includes/updateconfig.html');
|
require('./includes/updateconfig.html');
|
||||||
|
|
||||||
|
import _ from 'lodash-es';
|
||||||
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
||||||
|
|
||||||
angular.module('portainer.docker').controller('ServiceController', [
|
angular.module('portainer.docker').controller('ServiceController', [
|
||||||
|
@ -51,6 +52,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||||
'EndpointProvider',
|
'EndpointProvider',
|
||||||
'clipboard',
|
'clipboard',
|
||||||
'WebhookHelper',
|
'WebhookHelper',
|
||||||
|
'NetworkService',
|
||||||
function (
|
function (
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
|
@ -82,7 +84,8 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||||
WebhookService,
|
WebhookService,
|
||||||
EndpointProvider,
|
EndpointProvider,
|
||||||
clipboard,
|
clipboard,
|
||||||
WebhookHelper
|
WebhookHelper,
|
||||||
|
NetworkService
|
||||||
) {
|
) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
updateInProgress: false,
|
updateInProgress: false,
|
||||||
|
@ -210,6 +213,25 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||||
$scope.updateMount = function updateMount(service) {
|
$scope.updateMount = function updateMount(service) {
|
||||||
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
|
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) {
|
$scope.addPlacementConstraint = function addPlacementConstraint(service) {
|
||||||
service.ServiceConstraints.push({ key: '', operator: '==', value: '' });
|
service.ServiceConstraints.push({ key: '', operator: '==', value: '' });
|
||||||
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
|
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
|
||||||
|
@ -370,6 +392,14 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||||
config.TaskTemplate.ContainerSpec.Image = service.Image;
|
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.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
|
||||||
config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : [];
|
config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : [];
|
||||||
config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : [];
|
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() : [],
|
configs: apiVersion >= 1.3 ? ConfigService.configs() : [],
|
||||||
availableImages: ImageService.images(),
|
availableImages: ImageService.images(),
|
||||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
|
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
|
||||||
|
availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
|
||||||
settings: SettingsService.publicSettings(),
|
settings: SettingsService.publicSettings(),
|
||||||
webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
|
webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(async function success(data) {
|
||||||
$scope.nodes = data.nodes;
|
$scope.nodes = data.nodes;
|
||||||
$scope.configs = data.configs;
|
$scope.configs = data.configs;
|
||||||
$scope.secrets = data.secrets;
|
$scope.secrets = data.secrets;
|
||||||
|
@ -642,6 +673,36 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||||
$scope.availableVolumes = data.volumes;
|
$scope.availableVolumes = data.volumes;
|
||||||
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
|
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
|
||||||
$scope.isAdmin = Authentication.isAdmin();
|
$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) {
|
if (data.webhooks.length > 0) {
|
||||||
var webhook = data.webhooks[0];
|
var webhook = data.webhooks[0];
|
||||||
|
@ -699,6 +760,11 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||||
previousServiceValues.push(name);
|
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) {
|
function updateServiceArray(service, name) {
|
||||||
previousServiceValues.push(name);
|
previousServiceValues.push(name);
|
||||||
service.hasChanges = true;
|
service.hasChanges = true;
|
||||||
|
|
Loading…
Reference in New Issue