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:
commit
a7fc7816d1
474 changed files with 15372 additions and 8022 deletions
|
@ -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);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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',
|
||||
});
|
|
@ -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, '');
|
97
app/docker/components/imageRegistry/por-image-registry.html
Normal file
97
app/docker/components/imageRegistry/por-image-registry.html
Normal 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>
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -7,5 +7,6 @@ angular.module('portainer.docker').component('logViewer', {
|
|||
logCollectionChange: '<',
|
||||
sinceTimestamp: '=',
|
||||
lineCount: '=',
|
||||
resourceName: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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()"
|
||||
|
|
|
@ -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');
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -18,7 +18,6 @@ angular.module('portainer.docker').factory('Network', [
|
|||
method: 'GET',
|
||||
isArray: true,
|
||||
interceptor: NetworksInterceptor,
|
||||
timeout: 15000,
|
||||
},
|
||||
get: {
|
||||
method: 'GET',
|
||||
|
|
|
@ -35,7 +35,6 @@ angular.module('portainer.docker').factory('Service', [
|
|||
logs: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', action: 'logs' },
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: logsHandler,
|
||||
},
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -17,7 +17,6 @@ angular.module('portainer.docker').factory('Task', [
|
|||
logs: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', action: 'logs' },
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: logsHandler,
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -12,4 +12,5 @@
|
|||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="container.Name"
|
||||
></log-viewer>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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>
|
11
app/docker/views/docker-features-configuration/index.js
Normal file
11
app/docker/views/docker-features-configuration/index.js
Normal 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: '<',
|
||||
},
|
||||
});
|
|
@ -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(),
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
angular.module('portainer.docker').component('hostView', {
|
||||
templateUrl: './host-view.html',
|
||||
controller: 'HostViewController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;">
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
angular.module('portainer.docker').component('nodeDetailsView', {
|
||||
templateUrl: './node-details-view.html',
|
||||
controller: 'NodeDetailsViewController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -12,4 +12,5 @@
|
|||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="service.Name"
|
||||
></log-viewer>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -13,4 +13,5 @@
|
|||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="task.Id"
|
||||
></log-viewer>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue