1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

Merge branch 'develop' into feat1654-colorize-logs

This commit is contained in:
Ricardo Matsui 2021-04-15 22:38:43 -07:00 committed by GitHub
commit a7fc7816d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
474 changed files with 15372 additions and 8022 deletions

View file

@ -581,6 +581,16 @@ angular.module('portainer.docker', ['portainer.app']).config([
},
};
const dockerFeaturesConfiguration = {
name: 'docker.featuresConfiguration',
url: '/feat-config',
views: {
'content@': {
component: 'dockerFeaturesConfigurationView',
},
},
};
$stateRegistryProvider.register(configs);
$stateRegistryProvider.register(config);
$stateRegistryProvider.register(configCreation);
@ -630,5 +640,6 @@ angular.module('portainer.docker', ['portainer.app']).config([
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(volumeBrowse);
$stateRegistryProvider.register(volumeCreation);
$stateRegistryProvider.register(dockerFeaturesConfiguration);
},
]);

View file

@ -3,17 +3,18 @@
Container capabilities
</div>
<div class="form-group">
<div ng-repeat="cap in $ctrl.capabilities">
<div class="col-xs-8 col-sm-3 col-md-2">
<label for="capability" class="control-label text-left">
{{ cap.capability }}
<portainer-tooltip position="bottom" message="{{ cap.description }}"></portainer-tooltip>
</label>
<div ng-repeat="cap in $ctrl.capabilities" class="col-xs-12 col-sm-6 col-md-4">
<div class="row">
<div class="col-xs-8">
<label for="capability" class="control-label text-left" style="display: flex; padding: 0;">
{{ cap.capability }}
<portainer-tooltip position="bottom" message="{{ cap.description }}"></portainer-tooltip>
</label>
</div>
<div class="col-xs-4">
<label class="switch"> <input type="checkbox" name="capability" ng-model="cap.allowed" /><i></i> </label>
</div>
</div>
<div class="col-xs-4 col-sm-2 col-md-1">
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="capability" ng-model="cap.allowed" /><i></i> </label>
</div>
<div class="col-xs-0 col-sm-1 col-md-1"> </div>
</div>
</div>
</form>

View file

@ -4,100 +4,8 @@
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<div class="settings">
<span
class="setting"
ng-class="{ 'setting-active': $ctrl.columnVisibility.state.open }"
uib-dropdown
dropdown-append-to-body
auto-close="disabled"
is-open="$ctrl.columnVisibility.state.open"
>
<span uib-dropdown-toggle><i class="fa fa-columns space-right" aria-hidden="true"></i>Columns</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Show / Hide Columns
</div>
<div class="menuContent">
<div class="md-checkbox">
<input
id="col_vis_state"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.state.display"
/>
<label for="col_vis_state" ng-bind="$ctrl.columnVisibility.columns.state.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_actions"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.actions.display"
/>
<label for="col_vis_actions" ng-bind="$ctrl.columnVisibility.columns.actions.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_stack"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.stack.display"
/>
<label for="col_vis_stack" ng-bind="$ctrl.columnVisibility.columns.stack.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_image"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.image.display"
/>
<label for="col_vis_image" ng-bind="$ctrl.columnVisibility.columns.image.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_created"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.created.display"
/>
<label for="col_vis_created" ng-bind="$ctrl.columnVisibility.columns.created.label"></label>
</div>
<div class="md-checkbox" ng-if="$ctrl.showHostColumn">
<input
id="col_vis_host"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.host.display"
/>
<label for="col_vis_host" ng-bind="$ctrl.columnVisibility.columns.host.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_ports"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.ports.display"
/>
<label for="col_vis_ports" ng-bind="$ctrl.columnVisibility.columns.ports.label"></label>
</div>
<div class="md-checkbox">
<input
id="col_vis_ownership"
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
type="checkbox"
ng-model="$ctrl.columnVisibility.columns.ownership.display"
/>
<label for="col_vis_ownership" ng-bind="$ctrl.columnVisibility.columns.ownership.label"></label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.columnVisibility.state.open = false;">Close</a>
</div>
</div>
</div>
</span>
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
@ -268,6 +176,13 @@
Created
</a>
</th>
<th ng-show="$ctrl.columnVisibility.columns.ip.display">
<a ng-click="$ctrl.changeOrderBy('IP')">
IP Address
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showHostColumn" ng-show="$ctrl.columnVisibility.columns.host.display">
<a ng-click="$ctrl.changeOrderBy('NodeName')">
Host
@ -342,6 +257,7 @@
<td ng-show="$ctrl.columnVisibility.columns.created.display">
{{ item.Created | getisodatefromtimestamp }}
</td>
<td ng-show="$ctrl.columnVisibility.columns.ip.display">{{ item.IP ? item.IP : '-' }}</td>
<td ng-if="$ctrl.showHostColumn" ng-show="$ctrl.columnVisibility.columns.host.display">{{ item.NodeName ? item.NodeName : '-' }}</td>
<td ng-show="$ctrl.columnVisibility.columns.ports.display">
<a

View file

@ -36,9 +36,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
};
this.columnVisibility = {
state: {
open: false,
},
columns: {
state: {
label: 'State',
@ -60,6 +57,10 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
label: 'Created',
display: true,
},
ip: {
label: 'IP Address',
display: true,
},
host: {
label: 'Host',
display: true,
@ -75,9 +76,11 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
},
};
this.onColumnVisibilityChange = function (columnVisibility) {
DatatableService.setColumnVisibilitySettings(this.tableKey, columnVisibility);
};
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
function onColumnVisibilityChange(columns) {
this.columnVisibility.columns = columns;
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
}
this.onSelectionChanged = function () {
this.updateSelectionState();
@ -199,7 +202,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
if (storedColumnVisibility !== null) {
this.columnVisibility = storedColumnVisibility;
this.columnVisibility.state.open = false;
}
};
},

View file

@ -66,6 +66,13 @@ angular.module('portainer.docker').controller('ServicesDatatableController', [
}
};
this.onDataRefresh = function () {
var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
if (storedExpandedItems !== null) {
this.expandItems(storedExpandedItems);
}
};
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();

View file

@ -37,7 +37,15 @@
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.swarm'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
<a ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.host'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>

View file

@ -0,0 +1,31 @@
export default class porImageRegistryContainerController {
/* @ngInject */
constructor(EndpointHelper, DockerHubService, Notifications) {
this.EndpointHelper = EndpointHelper;
this.DockerHubService = DockerHubService;
this.Notifications = Notifications;
this.pullRateLimits = null;
}
$onChanges({ isDockerHubRegistry }) {
if (isDockerHubRegistry && isDockerHubRegistry.currentValue) {
this.fetchRateLimits();
}
}
async fetchRateLimits() {
this.pullRateLimits = null;
if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
try {
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint);
this.setValidity(this.pullRateLimits.remaining >= 0);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed loading DockerHub pull rate limits', e);
}
} else {
this.setValidity(true);
}
}
}

View file

@ -0,0 +1,34 @@
<div class="form-group" ng-if="$ctrl.isDockerHubRegistry && $ctrl.pullRateLimits">
<div class="col-sm-12 small">
<div ng-if="$ctrl.pullRateLimits.remaining > 0" class="text-muted">
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
<span ng-if="$ctrl.isAuthenticated">
You are currently using a free account to pull images from DockerHub and will be limited to 200 pulls every 6 hours. Remaining pulls:
<span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
</span>
<span ng-if="!$ctrl.isAuthenticated">
<span ng-if="$ctrl.isAdmin">
You are currently using an anonymous account to pull images from DockerHub and will be limited to 100 pulls every 6 hours. You can configure DockerHub authentication in
the
<a ui-sref="portainer.registries">Registries View</a>. Remaining pulls:
<span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
</span>
<span ng-if="!$ctrl.isAdmin">
You are currently using an anonymous account to pull images from DockerHub and will be limited to 100 pulls every 6 hours. Contact your administrator to configure
DockerHub authentication. Remaining pulls: <span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
</span>
</span>
</div>
<div ng-if="$ctrl.pullRateLimits.remaining <= 0" class="text-warning">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span ng-if="$ctrl.isAuthenticated">
Your authorized pull count quota as a free user is now exceeded.
<span ng-transclude="rateLimitExceeded">You will not be able to pull any image from the DockerHub registry.</span>
</span>
<span ng-if="!$ctrl.isAuthenticated">
Your authorized pull count quota as an anonymous user is now exceeded.
<span ng-transclude="rateLimitExceeded">You will not be able to pull any image from the DockerHub registry.</span>
</span>
</div>
</div>
</div>

View file

@ -0,0 +1,18 @@
import angular from 'angular';
import controller from './por-image-registry-rate-limits.controller';
angular.module('portainer.docker').component('porImageRegistryRateLimits', {
bindings: {
endpoint: '<',
setValidity: '<',
isAdmin: '<',
isDockerHubRegistry: '<',
isAuthenticated: '<',
},
controller,
transclude: {
rateLimitExceeded: '?porImageRegistryRateLimitExceeded',
},
templateUrl: './por-image-registry-rate-limits.html',
});

View file

@ -48,7 +48,11 @@ class porImageRegistryController {
this.availableImages = images;
}
onRegistryChange() {
isDockerHubRegistry() {
return this.model.UseRegistry && this.model.Registry.Name === 'DockerHub';
}
async onRegistryChange() {
this.prepareAutocomplete();
if (this.model.Registry.Type === RegistryTypes.GITLAB && this.model.Image) {
this.model.Image = _.replace(this.model.Image, this.model.Registry.Gitlab.ProjectPath, '');

View file

@ -0,0 +1,97 @@
<!-- use registry -->
<div ng-if="$ctrl.model.UseRegistry">
<div class="form-group">
<label for="image_registry" class="control-label text-left" ng-class="$ctrl.labelClass">
Registry
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
ng-model="$ctrl.model.Registry"
id="image_registry"
selected-item-id="ctrl.selectedItemId"
class="form-control"
></select>
</div>
<label for="image_name" ng-class="$ctrl.labelClass" class="margin-sm-top control-label text-left">Image</label>
<div ng-class="$ctrl.inputClass" class="margin-sm-top">
<div class="input-group">
<span class="input-group-addon" id="registry-name">{{ $ctrl.displayedRegistryURL() }}</span>
<input
type="text"
class="form-control"
aria-describedby="registry-name"
uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
ng-model="$ctrl.model.Image"
name="image_name"
placeholder="e.g. myImage:myTag"
ng-change="$ctrl.onImageChange()"
required
/>
<span ng-if="$ctrl.isDockerHubRegistry()" class="input-group-btn">
<a
href="https://hub.docker.com/search?type=image&q={{ $ctrl.model.Image | trimshasum | trimversiontag }}"
class="btn btn-default"
title="Search image on Docker Hub"
target="_blank"
>
<i class="fab fa-docker"></i> Search
</a>
</span>
</div>
</div>
</div>
<!-- ! use registry -->
<!-- don't use registry -->
<div ng-if="!$ctrl.model.UseRegistry">
<div class="form-group">
<span class="small">
<p class="text-muted" style="margin-left: 15px;">
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
When using advanced mode, image and repository <b>must be</b> publicly available.
</p>
</span>
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image </label>
<div ng-class="$ctrl.inputClass">
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
</div>
</div>
</div>
<!-- ! don't use registry -->
<!-- info message -->
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="$ctrl.form.image_name.$error">
<p ng-message="required">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image name is required.
<span ng-if="$ctrl.canPull">Tag must be specified otherwise Portainer will pull all tags associated to the image.</span>
</p>
</div>
</div>
</div>
<!-- ! info message -->
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="!$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = true;">
<i class="fa fa-database space-right" aria-hidden="true"></i> Simple mode
</a>
<a class="small interactive" ng-if="$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = false;">
<i class="fa fa-globe space-right" aria-hidden="true"></i> Advanced mode
</a>
</p>
</div>
</div>
<div ng-transclude></div>
<por-image-registry-rate-limits
ng-show="$ctrl.checkRateLimits"
is-docker-hub-registry="$ctrl.isDockerHubRegistry()"
endpoint="$ctrl.endpoint"
set-validity="$ctrl.setValidity"
is-authenticated="$ctrl.model.Registry.Authentication"
is-admin="$ctrl.isAdmin"
>
</por-image-registry-rate-limits>
</div>

View file

@ -1,5 +1,5 @@
angular.module('portainer.docker').component('porImageRegistry', {
templateUrl: './porImageRegistry.html',
templateUrl: './por-image-registry.html',
controller: 'porImageRegistryController',
bindings: {
model: '=', // must be of type PorImageRegistryModel
@ -7,9 +7,14 @@ angular.module('portainer.docker').component('porImageRegistry', {
autoComplete: '<',
labelClass: '@',
inputClass: '@',
endpoint: '<',
isAdmin: '<',
checkRateLimits: '<',
onImageChange: '&',
setValidity: '<',
},
require: {
form: '^form',
},
transclude: true,
});

View file

@ -1,72 +0,0 @@
<!-- use registry -->
<div ng-if="$ctrl.model.UseRegistry">
<div class="form-group">
<label for="image_registry" class="control-label text-left" ng-class="$ctrl.labelClass">
Registry
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
ng-model="$ctrl.model.Registry"
id="image_registry"
selected-item-id="ctrl.selectedItemId"
class="form-control"
></select>
</div>
<label for="image_name" ng-class="$ctrl.labelClass" class="margin-sm-top control-label text-left">Image</label>
<div ng-class="$ctrl.inputClass" class="margin-sm-top">
<div class="input-group">
<span class="input-group-addon" id="registry-name">{{ $ctrl.displayedRegistryURL() }}</span>
<input
type="text"
class="form-control"
aria-describedby="registry-name"
uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
ng-model="$ctrl.model.Image"
name="image_name"
placeholder="e.g. myImage:myTag"
ng-change="$ctrl.onImageChange()"
required
/>
</div>
</div>
</div>
</div>
<!-- ! use registry -->
<!-- don't use registry -->
<div ng-if="!$ctrl.model.UseRegistry">
<div class="form-group">
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left"
>Image
<portainer-tooltip position="bottom" message="Image and repository should be publicly available."></portainer-tooltip>
</label>
<div ng-class="$ctrl.inputClass">
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
</div>
</div>
</div>
<!-- ! don't use registry -->
<!-- info message -->
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="$ctrl.form.image_name.$error">
<p ng-message="required"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image name is required.
<span ng-if="$ctrl.canPull">Tag must be specified otherwise Portainer will pull all tags associated to the image.</span></p
>
</div>
</div>
</div>
<!-- ! info message -->
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="!$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = true;">
<i class="fa fa-database space-right" aria-hidden="true"></i> Simple mode
</a>
<a class="small interactive" ng-if="$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = false;">
<i class="fa fa-globe space-right" aria-hidden="true"></i> Advanced mode
</a>
</p>
</div>
</div>

View file

@ -7,5 +7,6 @@ angular.module('portainer.docker').component('logViewer', {
logCollectionChange: '<',
sinceTimestamp: '=',
lineCount: '=',
resourceName: '<',
},
});

View file

@ -67,6 +67,7 @@
Actions
</label>
<div class="col-sm-11">
<button class="btn btn-primary btn-sm" type="button" ng-click="$ctrl.downloadLogs()" style="margin-left: 0;"><i class="fa fa-download"></i> Download logs</button>
<button
class="btn btn-primary btn-sm"
ng-click="$ctrl.copy()"

View file

@ -1,8 +1,11 @@
import moment from 'moment';
import _ from 'lodash-es';
angular.module('portainer.docker').controller('LogViewerController', [
'clipboard',
function (clipboard) {
'Blob',
'FileSaver',
function (clipboard, Blob, FileSaver) {
this.state = {
availableSinceDatetime: [
{ desc: 'Last day', value: moment().subtract(1, 'days').format() },
@ -43,5 +46,10 @@ angular.module('portainer.docker').controller('LogViewerController', [
this.state.selectedLines.splice(idx, 1);
}
};
this.downloadLogs = function () {
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log, '')]);
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
};
},
]);

View file

@ -305,4 +305,21 @@ angular
}
return _.split(imageName, '@sha256')[0];
};
})
.filter('trimversiontag', function () {
'use strict';
return function trimversiontag(fullName) {
if (!fullName) {
return fullName;
}
var versionIdx = fullName.lastIndexOf(':');
if (versionIdx < 0) {
return fullName;
}
var hostIdx = fullName.indexOf('/');
if (hostIdx > versionIdx) {
return fullName;
}
return fullName.substring(0, versionIdx);
};
});

View file

@ -5,7 +5,7 @@ const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655
function parsePort(port) {
if (portPattern.test(port)) {
return parseInt(port);
return parseInt(port, 10);
} else {
return 0;
}
@ -211,14 +211,14 @@ angular.module('portainer.docker').factory('ContainerHelper', [
_.forEach(portBindingKeysByHostIp, (portBindingKeys, ip) => {
// Sort by host port
const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => {
return parseInt(_.split(portKey, '/')[0]);
return parseInt(_.split(portKey, '/')[0], 10);
});
let previousHostPort = -1;
let previousContainerPort = -1;
_.forEach(sortedPortBindingKeys, (portKey) => {
const portKeySplit = _.split(portKey, '/');
const containerPort = parseInt(portKeySplit[0]);
const containerPort = parseInt(portKeySplit[0], 10);
const portBinding = portBindings[portKey][0];
portBindings[portKey].shift();
const hostPort = parsePort(portBinding.HostPort);
@ -259,6 +259,10 @@ angular.module('portainer.docker').factory('ContainerHelper', [
return bindings;
};
helper.getContainerNames = function (containers) {
return _.map(_.flatten(_.map(containers, 'Names')), (name) => _.trimStart(name, '/'));
};
return helper;
},
]);

View file

@ -40,6 +40,10 @@ angular.module('portainer.docker').factory('ImageHelper', [
if (registry.Registry.Type === RegistryTypes.GITLAB) {
const slash = _.startsWith(registry.Image, ':') ? '' : '/';
fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image;
} else if (registry.Registry.Type === RegistryTypes.QUAY) {
const name = registry.Registry.Quay.UseOrganisation ? registry.Registry.Quay.OrganisationName : registry.Registry.Username;
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + name + '/' + registry.Image;
} else {
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + registry.Image;

View file

@ -1,14 +1,18 @@
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
function b64DecodeUnicode(str) {
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
try {
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
} catch (err) {
return atob(str);
}
}
export function ConfigViewModel(data) {

View file

@ -4,7 +4,6 @@ export function ImageViewModel(data) {
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
if (!this.RepoTags && data.RepoDigests) {
this.RepoTags = [];
@ -21,6 +20,7 @@ export function ImageViewModel(data) {
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
this.NodeName = data.Portainer.Agent.NodeName;
}
this.Labels = data.Labels;
}
export function ImageBuildModel(data) {

View file

@ -16,4 +16,5 @@ export function ImageDetailsViewModel(data) {
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
this.Labels = data.ContainerConfig.Labels;
}

View file

@ -19,7 +19,6 @@ angular.module('portainer.docker').factory('Container', [
params: { all: 0, action: 'json', filters: '@filters' },
isArray: true,
interceptor: ContainersInterceptor,
timeout: 15000,
},
get: {
method: 'GET',
@ -48,20 +47,17 @@ angular.module('portainer.docker').factory('Container', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
stats: {
method: 'GET',
params: { id: '@id', stream: false, action: 'stats' },
timeout: 4500,
ignoreLoadingBar: true,
},
top: {
method: 'GET',
params: { id: '@id', action: 'top' },
timeout: 4500,
ignoreLoadingBar: true,
},
start: {

View file

@ -16,7 +16,7 @@ angular.module('portainer.docker').factory('Image', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor, timeout: 15000 },
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor },
get: { method: 'GET', params: { action: 'json' } },
search: { method: 'GET', params: { action: 'search' } },
history: { method: 'GET', params: { action: 'history' }, isArray: true },

View file

@ -18,7 +18,6 @@ angular.module('portainer.docker').factory('Network', [
method: 'GET',
isArray: true,
interceptor: NetworksInterceptor,
timeout: 15000,
},
get: {
method: 'GET',

View file

@ -35,7 +35,6 @@ angular.module('portainer.docker').factory('Service', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View file

@ -18,10 +18,9 @@ angular.module('portainer.docker').factory('System', [
info: {
method: 'GET',
params: { action: 'info' },
timeout: 15000,
interceptor: InfoInterceptor,
},
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
version: { method: 'GET', params: { action: 'version' }, interceptor: VersionInterceptor },
events: {
method: 'GET',
params: { action: 'events', since: '@since', until: '@until' },

View file

@ -17,7 +17,6 @@ angular.module('portainer.docker').factory('Task', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View file

@ -18,7 +18,7 @@ angular.module('portainer.docker').factory('Volume', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000 },
query: { method: 'GET', interceptor: VolumesInterceptor },
get: { method: 'GET', params: { id: '@id' } },
create: {
method: 'POST',

View file

@ -5,9 +5,11 @@ import angular from 'angular';
class CreateConfigController {
/* @ngInject */
constructor($async, $state, $transition$, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
constructor($async, $state, $transition$, $window, ModalService, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
this.$state = $state;
this.$transition$ = $transition$;
this.$window = $window;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.ConfigService = ConfigService;
this.Authentication = Authentication;
@ -24,6 +26,7 @@ class CreateConfigController {
this.state = {
formValidationError: '',
isEditorDirty: false,
};
this.editorUpdate = this.editorUpdate.bind(this);
@ -31,6 +34,12 @@ class CreateConfigController {
}
async $onInit() {
this.$window.onbeforeunload = () => {
if (this.formValues.displayCodeEditor && this.formValues.ConfigContent && this.state.isEditorDirty) {
return '';
}
};
if (!this.$transition$.params().id) {
this.formValues.displayCodeEditor = true;
return;
@ -53,6 +62,12 @@ class CreateConfigController {
}
}
async uiCanExit() {
if (this.formValues.displayCodeEditor && this.formValues.ConfigContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
addLabel() {
this.formValues.Labels.push({ name: '', value: '' });
}
@ -122,6 +137,7 @@ class CreateConfigController {
const userId = userDetails.ID;
await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
this.Notifications.success('Config successfully created');
this.state.isEditorDirty = false;
this.$state.go('docker.configs', {}, { reload: true });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create config');
@ -130,6 +146,7 @@ class CreateConfigController {
editorUpdate(cm) {
this.formValues.ConfigContent = cm.getValue();
this.state.isEditorDirty = true;
}
}

View file

@ -27,9 +27,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
'ModalService',
'RegistryService',
'SystemService',
'SettingsService',
'PluginService',
'HttpRequestHelper',
'endpoint',
function (
$q,
$scope,
@ -53,11 +53,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
ModalService,
RegistryService,
SystemService,
SettingsService,
PluginService,
HttpRequestHelper
HttpRequestHelper,
endpoint
) {
$scope.create = create;
$scope.endpoint = endpoint;
$scope.formValues = {
alwaysPull: true,
@ -79,6 +80,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
EntrypointMode: 'default',
NodeName: null,
capabilities: [],
Sysctls: [],
LogDriverName: '',
LogDriverOpts: [],
RegistryModel: new PorImageRegistryModel(),
@ -90,6 +92,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
formValidationError: '',
actionInProgress: false,
mode: '',
pullImageValidity: true,
};
$scope.refreshSlider = function () {
@ -103,6 +106,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.formValues.EntrypointMode = 'default';
};
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
if (!validity) {
$scope.formValues.alwaysPull = false;
}
$scope.state.pullImageValidity = validity;
}
$scope.config = {
Image: '',
Env: [],
@ -126,6 +137,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
Devices: [],
CapAdd: [],
CapDrop: [],
Sysctls: {},
},
NetworkingConfig: {
EndpointsConfig: {},
@ -181,6 +193,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices.splice(index, 1);
};
$scope.addSysctl = function () {
$scope.formValues.Sysctls.push({ name: '', value: '' });
};
$scope.removeSysctl = function (index) {
$scope.formValues.Sysctls.splice(index, 1);
};
$scope.addLogDriverOpt = function () {
$scope.formValues.LogDriverOpts.push({ name: '', value: '' });
};
@ -334,6 +354,16 @@ angular.module('portainer.docker').controller('CreateContainerController', [
config.HostConfig.Devices = path;
}
function prepareSysctls(config) {
var sysctls = {};
$scope.formValues.Sysctls.forEach(function (sysctl) {
if (sysctl.name && sysctl.value) {
sysctls[sysctl.name] = sysctl.value;
}
});
config.HostConfig.Sysctls = sysctls;
}
function prepareResources(config) {
// Memory Limit - Round to 0.125
if ($scope.formValues.MemoryLimit >= 0) {
@ -402,6 +432,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
prepareResources(config);
prepareLogDriver(config);
prepareCapabilities(config);
prepareSysctls(config);
return config;
}
@ -547,6 +578,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices = path;
}
function loadFromContainerSysctls() {
for (var s in $scope.config.HostConfig.Sysctls) {
if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) {
$scope.formValues.Sysctls.push({ name: s, value: $scope.config.HostConfig.Sysctls[s] });
}
}
}
function loadFromContainerImageConfig() {
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
.then((model) => {
@ -622,6 +661,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
loadFromContainerImageConfig(d);
loadFromContainerResources(d);
loadFromContainerCapabilities(d);
loadFromContainerSysctls(d);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve container');
@ -646,6 +686,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.isAdmin = Authentication.isAdmin();
$scope.showDeviceMapping = await shouldShowDevices();
$scope.showSysctls = await shouldShowSysctls();
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
$scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
@ -709,14 +750,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
Notifications.error('Failure', err, 'Unable to retrieve engine details');
});
SettingsService.publicSettings()
.then(function success(data) {
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers;
$scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');
});
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
$scope.availableLoggingDrivers = loggingDrivers;
@ -933,15 +968,17 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
async function shouldShowDevices() {
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
}
return allowDeviceMappingForRegularUsers || Authentication.isAdmin();
async function shouldShowSysctls() {
const { allowSysctlSettingForRegularUsers } = $scope.applicationState.application;
return allowSysctlSettingForRegularUsers || Authentication.isAdmin();
}
async function checkIfContainerCapabilitiesEnabled() {
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
}
initView();

View file

@ -31,10 +31,10 @@
</div>
<div ng-if="!formValues.RegistryModel.Registry && fromContainer">
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
<span class="small text-danger" style="margin-left: 5px;"
>The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register
that registry first.</span
>
<span class="small text-danger" style="margin-left: 5px;">
The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that
registry first.
</span>
</div>
<div ng-if="formValues.RegistryModel.Registry || !fromContainer">
<!-- image-and-registry -->
@ -45,23 +45,30 @@
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="formValues.alwaysPull"
on-image-change="onImageNameChange()"
></por-image-registry>
<!-- !image-and-registry -->
<!-- always-pull -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Always pull the image
<portainer-tooltip
position="bottom"
message="When enabled, Portainer will automatically try to pull the specified image before creating the container."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.alwaysPull" /><i></i> </label>
set-validity="setPullImageValidity"
>
<!-- always-pull -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Always pull the image
<portainer-tooltip
position="bottom"
message="When enabled, Portainer will automatically try to pull the specified image before creating the container."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull" ng-disabled="!state.pullImageValidity" /><i></i>
</label>
</div>
</div>
</div>
<!-- !always-pull -->
<!-- !always-pull -->
</por-image-registry>
<!-- !image-and-registry -->
</div>
<div class="col-sm-12 form-section-title">
Network ports configuration
@ -699,6 +706,33 @@
<!-- !devices-input-list -->
</div>
<!-- !devices-->
<!-- sysctls -->
<div ng-if="showSysctls" class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Sysctls</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSysctl()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add sysctl
</span>
</div>
<!-- sysctls-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="sysctl in formValues.Sysctls" 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="sysctl.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="sysctl.value" placeholder="e.g. bar" />
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSysctl($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !sysctls-input-list -->
</div>
<!-- !sysctls -->
<div class="col-sm-12 form-section-title">
Resources
</div>

View file

@ -274,6 +274,17 @@
</container-restart-policy>
</td>
</tr>
<tr ng-if="!(container.HostConfig.Sysctls | emptyobject)">
<td>Sysctls</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in container.HostConfig.Sysctls">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>

View file

@ -22,6 +22,7 @@ angular.module('portainer.docker').controller('ContainerController', [
'HttpRequestHelper',
'Authentication',
'StateManager',
'endpoint',
function (
$q,
$scope,
@ -41,7 +42,8 @@ angular.module('portainer.docker').controller('ContainerController', [
ImageService,
HttpRequestHelper,
Authentication,
StateManager
StateManager,
endpoint
) {
$scope.activityTime = 0;
$scope.portBindings = [];
@ -94,22 +96,24 @@ angular.module('portainer.docker').controller('ContainerController', [
});
}
$scope.container.Config.Env = _.sortBy($scope.container.Config.Env, _.toLower);
const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id'];
const autoRemove = $scope.container.HostConfig.AutoRemove;
const admin = Authentication.isAdmin();
const appState = StateManager.getState();
const {
allowContainerCapabilitiesForRegularUsers,
allowHostNamespaceForRegularUsers,
allowDeviceMappingForRegularUsers,
allowSysctlSettingForRegularUsers,
allowBindMountsForRegularUsers,
allowPrivilegedModeForRegularUsers,
} = appState.application;
} = endpoint.SecuritySettings;
const settingRestrictsRegularUsers =
!allowContainerCapabilitiesForRegularUsers ||
!allowBindMountsForRegularUsers ||
!allowDeviceMappingForRegularUsers ||
!allowSysctlSettingForRegularUsers ||
!allowHostNamespaceForRegularUsers ||
!allowPrivilegedModeForRegularUsers;

View file

@ -12,4 +12,5 @@
display-timestamps="state.displayTimestamps"
line-count="state.lineCount"
since-timestamp="state.sinceTimestamp"
resource-name="container.Name"
></log-viewer>

View file

@ -17,6 +17,7 @@ angular.module('portainer.docker').controller('DashboardController', [
'EndpointProvider',
'StateManager',
'TagService',
'endpoint',
function (
$scope,
$q,
@ -32,7 +33,8 @@ angular.module('portainer.docker').controller('DashboardController', [
Notifications,
EndpointProvider,
StateManager,
TagService
TagService,
endpoint
) {
$scope.dismissInformationPanel = function (id) {
StateManager.dismissInformationPanel(id);
@ -89,9 +91,8 @@ angular.module('portainer.docker').controller('DashboardController', [
async function shouldShowStacks() {
const isAdmin = Authentication.isAdmin();
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
return isAdmin || allowStackManagementForRegularUsers;
return isAdmin || endpoint.SecuritySettings.allowStackManagementForRegularUsers;
}
initView();

View file

@ -0,0 +1,99 @@
export default class DockerFeaturesConfigurationController {
/* @ngInject */
constructor($async, EndpointService, Notifications, StateManager) {
this.$async = $async;
this.EndpointService = EndpointService;
this.Notifications = Notifications;
this.StateManager = StateManager;
this.formValues = {
enableHostManagementFeatures: false,
allowVolumeBrowserForRegularUsers: false,
disableBindMountsForRegularUsers: false,
disablePrivilegedModeForRegularUsers: false,
disableHostNamespaceForRegularUsers: false,
disableStackManagementForRegularUsers: false,
disableDeviceMappingForRegularUsers: false,
disableContainerCapabilitiesForRegularUsers: false,
disableSysctlSettingForRegularUsers: false,
};
this.isAgent = false;
this.state = {
actionInProgress: false,
};
this.save = this.save.bind(this);
}
isContainerEditDisabled() {
const {
disableBindMountsForRegularUsers,
disableHostNamespaceForRegularUsers,
disablePrivilegedModeForRegularUsers,
disableDeviceMappingForRegularUsers,
disableContainerCapabilitiesForRegularUsers,
disableSysctlSettingForRegularUsers,
} = this.formValues;
return (
disableBindMountsForRegularUsers ||
disableHostNamespaceForRegularUsers ||
disablePrivilegedModeForRegularUsers ||
disableDeviceMappingForRegularUsers ||
disableContainerCapabilitiesForRegularUsers ||
disableSysctlSettingForRegularUsers
);
}
async save() {
return this.$async(async () => {
try {
this.state.actionInProgress = true;
const securitySettings = {
enableHostManagementFeatures: this.formValues.enableHostManagementFeatures,
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
allowVolumeBrowserForRegularUsers: this.formValues.allowVolumeBrowserForRegularUsers,
allowHostNamespaceForRegularUsers: !this.formValues.disableHostNamespaceForRegularUsers,
allowDeviceMappingForRegularUsers: !this.formValues.disableDeviceMappingForRegularUsers,
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers,
};
await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings);
this.endpoint.SecuritySettings = securitySettings;
this.Notifications.success('Saved settings successfully');
} catch (e) {
this.Notifications.error('Failure', e, 'Failed saving settings');
}
this.state.actionInProgress = false;
});
}
checkAgent() {
const applicationState = this.StateManager.getState();
return applicationState.endpoint.mode.agentProxy;
}
$onInit() {
const securitySettings = this.endpoint.SecuritySettings;
const isAgent = this.checkAgent();
this.isAgent = isAgent;
this.formValues = {
enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures,
allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
disableDeviceMappingForRegularUsers: !securitySettings.allowDeviceMappingForRegularUsers,
disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers,
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers,
};
}
}

View file

@ -0,0 +1,147 @@
<rd-header>
<rd-header-title title-text="Docker features configuration"></rd-header-title>
<rd-header-content>Docker configuration</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="$ctrl.form">
<div class="col-sm-12 form-section-title">
Host and Filesystem
</div>
<div ng-if="!$ctrl.isAgent" class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
These features are only available for an Agent enabled endpoints.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.enableHostManagementFeatures"
name="enableHostManagementFeatures"
label="Enable host management features"
tooltip="Enable host management features: host system browsing and advanced host details."
disabled="!$ctrl.isAgent"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.allowVolumeBrowserForRegularUsers"
name="allowVolumeBrowserForRegularUsers"
label="Enable volume management for non-administrators"
tooltip="When enabled, regular users will be able to use Portainer volume management features."
disabled="!$ctrl.isAgent"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<!-- security -->
<div class="col-sm-12 form-section-title">
Docker Security Settings
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableBindMountsForRegularUsers"
name="disableBindMountsForRegularUsers"
label="Disable bind mounts for non-administrators"
tooltip="When enabled, regular users will not be able to use bind mounts when creating containers."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disablePrivilegedModeForRegularUsers"
name="disablePrivilegedModeForRegularUsers"
label="Disable privileged mode for non-administrators"
tooltip="When enabled, regular users will not be able to use privileged mode when creating containers."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableHostNamespaceForRegularUsers"
name="disableHostNamespaceForRegularUsers"
label="Disable the use of host PID 1 for non-administrators"
tooltip="Prevent users from accessing the host filesystem through the host PID namespace."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableStackManagementForRegularUsers"
name="disableStackManagementForRegularUsers"
label="Disable the use of Stacks for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableDeviceMappingForRegularUsers"
name="disableDeviceMappingForRegularUsers"
label="Disable device mappings for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableContainerCapabilitiesForRegularUsers"
name="disableContainerCapabilitiesForRegularUsers"
label="Disable container capabilities for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableSysctlSettingForRegularUsers"
name="disableSysctlSettingForRegularUsers"
label="Disable sysctl settings for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group" ng-if="$ctrl.isContainerEditDisabled()">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
</span>
</div>
<!-- !security -->
<!-- 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-click="$ctrl.save()" ng-disabled="$ctrl.state.actionInProgress" button-spinner="$ctrl.state.actionInProgress">
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,11 @@
import angular from 'angular';
import controller from './docker-features-configuration.controller';
angular.module('portainer.docker').component('dockerFeaturesConfigurationView', {
templateUrl: './docker-features-configuration.html',
controller,
bindings: {
endpoint: '<',
},
});

View file

@ -29,7 +29,7 @@ angular.module('portainer.docker').controller('HostViewController', [
ctrl.state.isAdmin = Authentication.isAdmin();
var agentApiVersion = applicationState.endpoint.agentApiVersion;
ctrl.state.agentApiVersion = agentApiVersion;
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
$q.all({
version: SystemService.version(),

View file

@ -1,4 +1,7 @@
angular.module('portainer.docker').component('hostView', {
templateUrl: './host-view.html',
controller: 'HostViewController',
bindings: {
endpoint: '<',
},
});

View file

@ -1,14 +1,16 @@
angular.module('portainer.docker').controller('BuildImageController', [
'$scope',
'$state',
'$window',
'ModalService',
'BuildService',
'Notifications',
'HttpRequestHelper',
function ($scope, $state, BuildService, Notifications, HttpRequestHelper) {
function ($scope, $window, ModalService, BuildService, Notifications, HttpRequestHelper) {
$scope.state = {
BuildType: 'editor',
actionInProgress: false,
activeTab: 0,
isEditorDirty: false,
};
$scope.formValues = {
@ -20,6 +22,12 @@ angular.module('portainer.docker').controller('BuildImageController', [
NodeName: null,
};
$window.onbeforeunload = () => {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return '';
}
};
$scope.addImageName = function () {
$scope.formValues.ImageNames.push({ Name: '' });
};
@ -93,6 +101,13 @@ angular.module('portainer.docker').controller('BuildImageController', [
$scope.editorUpdate = function (cm) {
$scope.formValues.DockerFileContent = cm.getValue();
$scope.state.isEditorDirty = true;
};
this.uiCanExit = async function () {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return ModalService.confirmWebEditorDiscard();
}
};
},
]);

View file

@ -128,6 +128,17 @@
<td>Build</td>
<td>Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }}</td>
</tr>
<tr ng-if="!(image.Labels | emptyobject)">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in image.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="image.Author">
<td>Author</td>
<td>{{ image.Author }}</td>

View file

@ -154,6 +154,7 @@ angular.module('portainer.docker').controller('ImageController', [
.then(function success(data) {
$scope.image = data.image;
$scope.history = data.history;
$scope.image.Env = _.sortBy($scope.image.Env, _.toLower);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve image details');

View file

@ -14,30 +14,41 @@
<rd-widget-body>
<form class="form-horizontal">
<!-- image-and-registry -->
<por-image-registry model="formValues.RegistryModel" auto-complete="true" pull-warning="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
pull-warning="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
set-validity="setPullImageValidity"
check-rate-limits="true"
>
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12 form-section-title">
Deployment
</div>
<!-- node-selection -->
<node-selector model="formValues.NodeName"> </node-selector>
<!-- !node-selection -->
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !state.pullRateValid"
ng-click="pullImage()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Pull the image</span>
<span ng-show="state.actionInProgress">Download in progress...</span>
</button>
</div>
</div>
</por-image-registry>
<!-- !image-and-registry -->
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12 form-section-title">
Deployment
</div>
<!-- node-selection -->
<node-selector model="formValues.NodeName"> </node-selector>
<!-- !node-selection -->
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image"
ng-click="pullImage()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Pull the image</span>
<span ng-show="state.actionInProgress">Download in progress...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>

View file

@ -4,6 +4,7 @@ import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ImagesController', [
'$scope',
'$state',
'Authentication',
'ImageService',
'Notifications',
'ModalService',
@ -11,10 +12,15 @@ angular.module('portainer.docker').controller('ImagesController', [
'FileSaver',
'Blob',
'EndpointProvider',
function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider) {
'endpoint',
function ($scope, $state, Authentication, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider, endpoint) {
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
$scope.state = {
actionInProgress: false,
exportInProgress: false,
pullRateValid: false,
};
$scope.formValues = {
@ -140,6 +146,11 @@ angular.module('portainer.docker').controller('ImagesController', [
});
}
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullRateValid = validity;
}
function initView() {
getImages();
}

View file

@ -19,7 +19,7 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button class="btn btn-sm btn-primary" ngf-select ngf-min-size="10" ngf-accept="'application/x-tar,application/x-gzip'" ng-model="formValues.UploadFile"
<button type="button" class="btn btn-sm btn-primary" ngf-select ngf-min-size="10" ngf-accept="'application/x-tar,application/x-gzip'" ng-model="formValues.UploadFile"
>Select file</button
>
<span style="margin-left: 5px;">

View file

@ -196,7 +196,8 @@
<div class="form-group" ng-hide="config.Driver === 'macvlan' && formValues.Macvlan.Scope === 'local'">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Restrict external access to the network
Isolated network
<portainer-tooltip position="bottom" message="An isolated network has no inbound or outbound communications."></portainer-tooltip>
</label>
<label name="ownership" class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.Internal" />

View file

@ -20,7 +20,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [
var applicationState = StateManager.getState();
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
ctrl.state.isAdmin = Authentication.isAdmin();
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
var fetchJobs = ctrl.state.isAdmin && ctrl.state.isAgent;

View file

@ -1,4 +1,7 @@
angular.module('portainer.docker').component('nodeDetailsView', {
templateUrl: './node-details-view.html',
controller: 'NodeDetailsViewController',
bindings: {
endpoint: '<',
},
});

View file

@ -30,9 +30,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [
'RegistryService',
'HttpRequestHelper',
'NodeService',
'SettingsService',
'WebhookService',
'EndpointProvider',
'endpoint',
function (
$q,
$scope,
@ -56,10 +55,11 @@ angular.module('portainer.docker').controller('CreateServiceController', [
RegistryService,
HttpRequestHelper,
NodeService,
SettingsService,
WebhookService,
EndpointProvider
endpoint
) {
$scope.endpoint = endpoint;
$scope.formValues = {
Name: '',
RegistryModel: new PorImageRegistryModel(),
@ -104,6 +104,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
$scope.state = {
formValidationError: '',
actionInProgress: false,
pullImageValidity: false,
};
$scope.allowBindMounts = false;
@ -114,6 +115,11 @@ angular.module('portainer.docker').controller('CreateServiceController', [
});
};
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullImageValidity = validity;
}
$scope.addPortBinding = function () {
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
};
@ -493,7 +499,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
const resourceControl = data.Portainer.ResourceControl;
const userId = Authentication.getUserDetails().ID;
const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
const webhookPromise = $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, EndpointProvider.endpointID()));
const webhookPromise = $q.when(endpoint.Type !== 4 && $scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, endpoint.ID));
return $q.all([rcPromise, webhookPromise]);
})
.then(function success() {
@ -520,6 +526,12 @@ angular.module('portainer.docker').controller('CreateServiceController', [
return true;
}
$scope.volumesAreValid = volumesAreValid;
function volumesAreValid() {
const volumes = $scope.formValues.Volumes;
return volumes.every((volume) => volume.Target && volume.Source);
}
$scope.create = function createService() {
var accessControlData = $scope.formValues.AccessControlData;
@ -587,10 +599,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
async function checkIfAllowedBindMounts() {
const isAdmin = Authentication.isAdmin();
const settings = await SettingsService.publicSettings();
const { AllowBindMountsForRegularUsers } = settings;
const { allowBindMountsForRegularUsers } = endpoint.SecuritySettings;
return isAdmin || AllowBindMountsForRegularUsers;
return isAdmin || allowBindMountsForRegularUsers;
}
},
]);

View file

@ -20,7 +20,17 @@
Image configuration
</div>
<!-- image-and-registry -->
<por-image-registry model="formValues.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="true"
set-validity="setPullImageValidity"
>
</por-image-registry>
<!-- !image-and-registry -->
<div class="col-sm-12 form-section-title">
Scheduling
@ -95,19 +105,21 @@
</div>
<!-- !port-mapping -->
<!-- create-webhook -->
<div class="col-sm-12 form-section-title">
Webhooks
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Create a service webhook
<portainer-tooltip
position="top"
message="Create a webhook (or callback URI) to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Webhook" /><i></i> </label>
<div ng-if="endpoint.Type !== 4">
<div class="col-sm-12 form-section-title">
Webhooks
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Create a service webhook
<portainer-tooltip
position="top"
message="Create a webhook (or callback URI) to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Webhook" /><i></i> </label>
</div>
</div>
</div>
<!-- !create-webhook -->
@ -123,7 +135,7 @@
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !volumesAreValid()"
ng-click="create()"
button-spinner="state.actionInProgress"
>
@ -298,16 +310,19 @@
<!-- volume-line1 -->
<div class="col-sm-12 form-inline">
<!-- container-path -->
<div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
<div class="input-group col-sm-6">
<div class="input-group input-group-sm w-full">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
</div>
<div class="small text-warning" ng-show="!volume.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="input-group col-sm-5" style="margin-left: 5px; vertical-align: top;">
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.Source = null">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Source = null">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
@ -318,27 +333,35 @@
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
<div style="height: 30px; display: inline-block; vertical-align: top; display: inline-flex; align-items: center;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</div>
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
<span class="input-group-addon">volume</span>
<select
class="form-control"
ng-model="volume.Source"
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
>
<option selected disabled hidden value="">Select a volume</option>
</select>
<div class="col-sm-6 input-group" ng-if="volume.Type === 'volume'" style="float: none; padding: 0;">
<div class="input-group input-group-sm w-full">
<span class="input-group-addon">volume</span>
<select
class="form-control"
ng-model="volume.Source"
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
>
<option selected disabled value="">Select a volume</option>
</select>
</div>
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
</div>
<!-- !volume -->
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
<div class="input-group input-group-sm w-full">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
</div>
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px; vertical-align: top;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>

View file

@ -1,6 +1,9 @@
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group">
<div class="col-sm-12 small text-muted"> By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers. </div>
<div class="col-sm-12 small text-muted">
By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers (Linux) or
<code>C:\ProgramData\Docker\secrets\$SECRET_NAME</code> (Windows).</div
>
</div>
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">

View file

@ -19,7 +19,7 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="var in service.EnvironmentVariables">
<tr ng-repeat="var in service.EnvironmentVariables | orderBy: 'originalKey'">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
@ -39,7 +39,7 @@
disable-authorization="DockerServiceUpdate"
/>
<span class="input-group-btn" authorization="DockerServiceUpdate">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable(service, $index)" ng-disabled="isUpdating">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable(service, var)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>

View file

@ -3,7 +3,17 @@
<rd-widget-header icon="fa-clone" title-text="Change container image"> </rd-widget-header>
<rd-widget-body ng-if="!isUpdating">
<form class="form-horizontal">
<por-image-registry model="formValues.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="true"
set-validity="setPullImageValidity"
>
</por-image-registry>
</form>
</rd-widget-body>
<rd-widget-body ng-if="isUpdating">

View file

@ -24,7 +24,14 @@
<tbody>
<tr ng-repeat="mount in service.ServiceMounts">
<td ng-if="isAdmin || allowBindMounts">
<select name="mountType" class="form-control" ng-model="mount.Type" ng-disabled="isUpdating" disable-authorization="DockerServiceUpdate">
<select
name="mountType"
class="form-control"
ng-model="mount.Type"
ng-change="onChangeMountType(service, mount)"
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
>
<option value="volume">Volume</option>
<option value="bind">Bind</option>
</select>
@ -35,6 +42,7 @@
ng-model="mount.Source"
ng-options="vol.Id as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
ng-if="mount.Type === 'volume'"
ng-change="updateMount(service, mount)"
disable-authorization="DockerServiceUpdate"
>
<option selected disabled hidden value="">Select a volume</option>
@ -42,12 +50,14 @@
<input
type="text"
class="form-control"
name=""
ng-model="mount.Source"
placeholder="e.g. /tmp/portainer/data"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating || (!isAdmin && !allowBindMounts && mount.Type === 'bind')"
ng-if="mount.Type === 'bind'"
/>
<div class="col-sm-12 small text-warning" ng-show="!mount.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
</td>
<td>
<input
@ -59,6 +69,7 @@
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
/>
<div class="col-sm-12 small text-warning" ng-show="!mount.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
</td>
<td authorization="DockerServiceUpdate">
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating" />
@ -77,7 +88,9 @@
<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="!hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!mountsAreValid() || !hasChanges(service, ['ServiceMounts'])" 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>

View file

@ -34,7 +34,6 @@ angular.module('portainer.docker').controller('ServiceController', [
'SecretService',
'ImageService',
'SecretHelper',
'Service',
'ServiceHelper',
'LabelHelper',
'TaskService',
@ -45,7 +44,6 @@ angular.module('portainer.docker').controller('ServiceController', [
'ModalService',
'PluginService',
'Authentication',
'SettingsService',
'VolumeService',
'ImageHelper',
'WebhookService',
@ -53,6 +51,7 @@ angular.module('portainer.docker').controller('ServiceController', [
'clipboard',
'WebhookHelper',
'NetworkService',
'endpoint',
function (
$q,
$scope,
@ -67,7 +66,6 @@ angular.module('portainer.docker').controller('ServiceController', [
SecretService,
ImageService,
SecretHelper,
Service,
ServiceHelper,
LabelHelper,
TaskService,
@ -78,15 +76,17 @@ angular.module('portainer.docker').controller('ServiceController', [
ModalService,
PluginService,
Authentication,
SettingsService,
VolumeService,
ImageHelper,
WebhookService,
EndpointProvider,
clipboard,
WebhookHelper,
NetworkService
NetworkService,
endpoint
) {
$scope.endpoint = endpoint;
$scope.state = {
updateInProgress: false,
deletionInProgress: false,
@ -117,8 +117,9 @@ angular.module('portainer.docker').controller('ServiceController', [
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
};
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
var removedElement = service.EnvironmentVariables.splice(index, 1);
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, item) {
const index = service.EnvironmentVariables.indexOf(item);
const removedElement = service.EnvironmentVariables.splice(index, 1);
if (removedElement !== null) {
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}
@ -210,6 +211,12 @@ angular.module('portainer.docker').controller('ServiceController', [
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
}
};
$scope.onChangeMountType = function onChangeMountType(service, mount) {
mount.Source = null;
$scope.updateMount(service, mount);
};
$scope.updateMount = function updateMount(service) {
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
};
@ -378,6 +385,12 @@ angular.module('portainer.docker').controller('ServiceController', [
return hasChanges;
};
$scope.mountsAreValid = mountsAreValid;
function mountsAreValid() {
const mounts = $scope.service.ServiceMounts;
return mounts.every((mount) => mount.Source && mount.Target);
}
function buildChanges(service) {
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;
@ -521,6 +534,11 @@ angular.module('portainer.docker').controller('ServiceController', [
});
};
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullImageValidity = validity;
}
$scope.updateService = function updateService(service) {
let config = {};
service, (config = buildChanges(service));
@ -660,7 +678,6 @@ angular.module('portainer.docker').controller('ServiceController', [
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()),
});
})
@ -671,7 +688,7 @@ angular.module('portainer.docker').controller('ServiceController', [
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
$scope.availableVolumes = data.volumes;
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
$scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers;
$scope.isAdmin = Authentication.isAdmin();
$scope.availableNetworks = data.availableNetworks;
$scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm');

View file

@ -12,4 +12,5 @@
display-timestamps="state.displayTimestamps"
line-count="state.lineCount"
since-timestamp="state.sinceTimestamp"
resource-name="service.Name"
></log-viewer>

View file

@ -13,12 +13,13 @@ angular.module('portainer.docker').controller('ServicesController', [
function getServices() {
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
$q.all({
services: ServiceService.services(),
tasks: TaskService.tasks(),
containers: agentProxy ? ContainerService.containers(1) : [],
nodes: NodeService.nodes(),
})
return $q
.all({
services: ServiceService.services(),
tasks: TaskService.tasks(),
containers: agentProxy ? ContainerService.containers(1) : [],
nodes: NodeService.nodes(),
})
.then(function success(data) {
var services = data.services;
var tasks = data.tasks;

View file

@ -13,4 +13,5 @@
display-timestamps="state.displayTimestamps"
line-count="state.lineCount"
since-timestamp="state.sinceTimestamp"
resource-name="task.Id"
></log-viewer>

View file

@ -70,9 +70,12 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
function prepareNFSConfiguration(driverOptions) {
var data = $scope.formValues.NFSData;
driverOptions.push({ name: 'type', value: data.version.toLowerCase() });
driverOptions.push({ name: 'type', value: 'nfs' });
var options = 'addr=' + data.serverAddress + ',' + data.options;
if (data.version === 'NFS4') {
options = options + ',nfsvers=4';
}
driverOptions.push({ name: 'o', value: options });
var mountPoint = data.mountPoint[0] === ':' ? data.mountPoint : ':' + data.mountPoint;

View file

@ -51,14 +51,18 @@ angular.module('portainer.docker').controller('VolumeController', [
};
$scope.removeVolume = function removeVolume() {
VolumeService.remove($scope.volume)
.then(function success() {
Notifications.success('Volume successfully removed', $transition$.params().id);
$state.go('docker.volumes', {});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
});
ModalService.confirmDeletion('Do you want to remove this volume?', (confirmed) => {
if (confirmed) {
VolumeService.remove($scope.volume)
.then(function success() {
Notifications.success('Volume successfully removed', $transition$.params().id);
$state.go('docker.volumes', {});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
});
}
});
};
function getVolumeDataFromContainer(container, volumeId) {

View file

@ -9,26 +9,32 @@ angular.module('portainer.docker').controller('VolumesController', [
'HttpRequestHelper',
'EndpointProvider',
'Authentication',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication) {
'ModalService',
'endpoint',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService, endpoint) {
$scope.removeAction = function (selectedItems) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
VolumeService.remove(volume)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => {
if (confirmed) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
VolumeService.remove(volume)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove volume');
})
.finally(function final() {
--actionCount;
if (actionCount === 0) {
$state.reload();
}
});
});
}
});
};
@ -70,8 +76,7 @@ angular.module('portainer.docker').controller('VolumesController', [
function initView() {
getVolumes();
$scope.showBrowseAction =
$scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || $scope.applicationState.application.enableVolumeBrowserForNonAdminUsers);
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || endpoint.SecuritySettings.allowVolumeBrowserForRegularUsers);
}
initView();