mirror of https://github.com/portainer/portainer
1113 lines
36 KiB
JavaScript
1113 lines
36 KiB
JavaScript
import _ from 'lodash-es';
|
|
|
|
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
|
|
|
|
import * as envVarsUtils from '@/portainer/helpers/env-vars';
|
|
import { FeatureId } from 'Portainer/feature-flags/enums';
|
|
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
|
|
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
|
|
import { ContainerDetailsViewModel } from '../../../models/container';
|
|
|
|
import './createcontainer.css';
|
|
|
|
angular.module('portainer.docker').controller('CreateContainerController', [
|
|
'$q',
|
|
'$scope',
|
|
'$async',
|
|
'$state',
|
|
'$timeout',
|
|
'$transition$',
|
|
'$filter',
|
|
'Container',
|
|
'ContainerHelper',
|
|
'Image',
|
|
'ImageHelper',
|
|
'Volume',
|
|
'NetworkService',
|
|
'ResourceControlService',
|
|
'Authentication',
|
|
'Notifications',
|
|
'ContainerService',
|
|
'ImageService',
|
|
'FormValidator',
|
|
'ModalService',
|
|
'RegistryService',
|
|
'SystemService',
|
|
'PluginService',
|
|
'HttpRequestHelper',
|
|
'endpoint',
|
|
function (
|
|
$q,
|
|
$scope,
|
|
$async,
|
|
$state,
|
|
$timeout,
|
|
$transition$,
|
|
$filter,
|
|
Container,
|
|
ContainerHelper,
|
|
Image,
|
|
ImageHelper,
|
|
Volume,
|
|
NetworkService,
|
|
ResourceControlService,
|
|
Authentication,
|
|
Notifications,
|
|
ContainerService,
|
|
ImageService,
|
|
FormValidator,
|
|
ModalService,
|
|
RegistryService,
|
|
SystemService,
|
|
PluginService,
|
|
HttpRequestHelper,
|
|
endpoint
|
|
) {
|
|
$scope.create = create;
|
|
$scope.update = update;
|
|
$scope.endpoint = endpoint;
|
|
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
|
|
$scope.formValues = {
|
|
alwaysPull: true,
|
|
GPU: {
|
|
enabled: false,
|
|
useSpecific: false,
|
|
selectedGPUs: [],
|
|
capabilities: ['compute', 'utility'],
|
|
},
|
|
Console: 'none',
|
|
Volumes: [],
|
|
NetworkContainer: null,
|
|
Labels: [],
|
|
ExtraHosts: [],
|
|
MacAddress: '',
|
|
IPv4: '',
|
|
IPv6: '',
|
|
DnsPrimary: '',
|
|
DnsSecondary: '',
|
|
AccessControlData: new AccessControlFormData(),
|
|
CpuLimit: 0,
|
|
MemoryLimit: 0,
|
|
MemoryReservation: 0,
|
|
CmdMode: 'default',
|
|
EntrypointMode: 'default',
|
|
Env: [],
|
|
NodeName: null,
|
|
capabilities: [],
|
|
Sysctls: [],
|
|
LogDriverName: '',
|
|
LogDriverOpts: [],
|
|
RegistryModel: new PorImageRegistryModel(),
|
|
};
|
|
|
|
$scope.extraNetworks = {};
|
|
|
|
$scope.state = {
|
|
formValidationError: '',
|
|
actionInProgress: false,
|
|
mode: '',
|
|
pullImageValidity: true,
|
|
settingUnlimitedResources: false,
|
|
};
|
|
|
|
$scope.onAlwaysPullChange = onAlwaysPullChange;
|
|
$scope.handlePublishAllPortsChange = handlePublishAllPortsChange;
|
|
$scope.handleAutoRemoveChange = handleAutoRemoveChange;
|
|
$scope.handlePrivilegedChange = handlePrivilegedChange;
|
|
$scope.handleInitChange = handleInitChange;
|
|
|
|
function onAlwaysPullChange(checked) {
|
|
return $scope.$evalAsync(() => {
|
|
$scope.formValues.alwaysPull = checked;
|
|
});
|
|
}
|
|
|
|
function handlePublishAllPortsChange(checked) {
|
|
return $scope.$evalAsync(() => {
|
|
$scope.config.HostConfig.PublishAllPorts = checked;
|
|
});
|
|
}
|
|
|
|
function handleAutoRemoveChange(checked) {
|
|
return $scope.$evalAsync(() => {
|
|
$scope.config.HostConfig.AutoRemove = checked;
|
|
});
|
|
}
|
|
|
|
function handlePrivilegedChange(checked) {
|
|
return $scope.$evalAsync(() => {
|
|
$scope.config.HostConfig.Privileged = checked;
|
|
});
|
|
}
|
|
|
|
function handleInitChange(checked) {
|
|
return $scope.$evalAsync(() => {
|
|
$scope.config.HostConfig.Init = checked;
|
|
});
|
|
}
|
|
|
|
$scope.handleEnvVarChange = handleEnvVarChange;
|
|
function handleEnvVarChange(value) {
|
|
$scope.formValues.Env = value;
|
|
}
|
|
|
|
$scope.refreshSlider = function () {
|
|
$timeout(function () {
|
|
$scope.$broadcast('rzSliderForceRender');
|
|
});
|
|
};
|
|
|
|
$scope.onImageNameChange = function () {
|
|
$scope.formValues.CmdMode = 'default';
|
|
$scope.formValues.EntrypointMode = 'default';
|
|
};
|
|
|
|
$scope.setPullImageValidity = setPullImageValidity;
|
|
function setPullImageValidity(validity) {
|
|
if (!validity) {
|
|
$scope.formValues.alwaysPull = false;
|
|
}
|
|
$scope.state.pullImageValidity = validity;
|
|
}
|
|
|
|
$scope.config = {
|
|
Image: '',
|
|
Env: [],
|
|
Cmd: '',
|
|
MacAddress: '',
|
|
ExposedPorts: {},
|
|
Entrypoint: '',
|
|
HostConfig: {
|
|
RestartPolicy: {
|
|
Name: 'no',
|
|
},
|
|
PortBindings: [],
|
|
PublishAllPorts: false,
|
|
Binds: [],
|
|
AutoRemove: false,
|
|
NetworkMode: 'bridge',
|
|
Privileged: false,
|
|
Init: false,
|
|
Runtime: null,
|
|
ExtraHosts: [],
|
|
Devices: [],
|
|
DeviceRequests: [],
|
|
CapAdd: [],
|
|
CapDrop: [],
|
|
Sysctls: {},
|
|
},
|
|
NetworkingConfig: {
|
|
EndpointsConfig: {},
|
|
},
|
|
Labels: {},
|
|
};
|
|
|
|
$scope.addVolume = function () {
|
|
$scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, type: 'volume' });
|
|
};
|
|
|
|
$scope.removeVolume = function (index) {
|
|
$scope.formValues.Volumes.splice(index, 1);
|
|
};
|
|
|
|
$scope.addPortBinding = function () {
|
|
$scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
|
};
|
|
|
|
$scope.removePortBinding = function (index) {
|
|
$scope.config.HostConfig.PortBindings.splice(index, 1);
|
|
};
|
|
|
|
$scope.addLabel = function () {
|
|
$scope.formValues.Labels.push({ name: '', value: '' });
|
|
};
|
|
|
|
$scope.removeLabel = function (index) {
|
|
$scope.formValues.Labels.splice(index, 1);
|
|
};
|
|
|
|
$scope.addExtraHost = function () {
|
|
$scope.formValues.ExtraHosts.push({ value: '' });
|
|
};
|
|
|
|
$scope.removeExtraHost = function (index) {
|
|
$scope.formValues.ExtraHosts.splice(index, 1);
|
|
};
|
|
|
|
$scope.addDevice = function () {
|
|
$scope.config.HostConfig.Devices.push({ pathOnHost: '', pathInContainer: '' });
|
|
};
|
|
|
|
$scope.removeDevice = function (index) {
|
|
$scope.config.HostConfig.Devices.splice(index, 1);
|
|
};
|
|
|
|
$scope.onGpuChange = function (values) {
|
|
return $async(async () => {
|
|
$scope.formValues.GPU = values;
|
|
});
|
|
};
|
|
|
|
$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: '' });
|
|
};
|
|
|
|
$scope.removeLogDriverOpt = function (index) {
|
|
$scope.formValues.LogDriverOpts.splice(index, 1);
|
|
};
|
|
|
|
$scope.fromContainerMultipleNetworks = false;
|
|
|
|
function prepareImageConfig(config) {
|
|
const imageConfig = ImageHelper.createImageConfigForContainer($scope.formValues.RegistryModel);
|
|
config.Image = imageConfig.fromImage;
|
|
}
|
|
|
|
function preparePortBindings(config) {
|
|
const bindings = ContainerHelper.preparePortBindings(config.HostConfig.PortBindings);
|
|
config.ExposedPorts = {};
|
|
_.forEach(bindings, (_, key) => (config.ExposedPorts[key] = {}));
|
|
config.HostConfig.PortBindings = bindings;
|
|
}
|
|
|
|
function prepareConsole(config) {
|
|
var value = $scope.formValues.Console;
|
|
var openStdin = true;
|
|
var tty = true;
|
|
if (value === 'tty') {
|
|
openStdin = false;
|
|
} else if (value === 'interactive') {
|
|
tty = false;
|
|
} else if (value === 'none') {
|
|
openStdin = false;
|
|
tty = false;
|
|
}
|
|
config.OpenStdin = openStdin;
|
|
config.Tty = tty;
|
|
}
|
|
|
|
function prepareCmd(config) {
|
|
if (_.isEmpty(config.Cmd) || $scope.formValues.CmdMode == 'default') {
|
|
delete config.Cmd;
|
|
} else {
|
|
config.Cmd = ContainerHelper.commandStringToArray(config.Cmd);
|
|
}
|
|
}
|
|
|
|
function prepareEntrypoint(config) {
|
|
if ($scope.formValues.EntrypointMode == 'default' || (_.isEmpty(config.Cmd) && _.isEmpty(config.Entrypoint))) {
|
|
config.Entrypoint = null;
|
|
}
|
|
}
|
|
|
|
function prepareEnvironmentVariables(config) {
|
|
config.Env = envVarsUtils.convertToArrayOfStrings($scope.formValues.Env);
|
|
}
|
|
|
|
function prepareVolumes(config) {
|
|
var binds = [];
|
|
var volumes = {};
|
|
|
|
$scope.formValues.Volumes.forEach(function (volume) {
|
|
var name = volume.name;
|
|
var containerPath = volume.containerPath;
|
|
if (name && containerPath) {
|
|
var bind = name + ':' + containerPath;
|
|
volumes[containerPath] = {};
|
|
if (volume.readOnly) {
|
|
bind += ':ro';
|
|
}
|
|
binds.push(bind);
|
|
}
|
|
});
|
|
config.HostConfig.Binds = binds;
|
|
config.Volumes = volumes;
|
|
}
|
|
|
|
function prepareNetworkConfig(config) {
|
|
var mode = config.HostConfig.NetworkMode;
|
|
var container = $scope.formValues.NetworkContainer;
|
|
var containerName = container;
|
|
if (container && typeof container === 'object') {
|
|
containerName = $filter('trimcontainername')(container.Names[0]);
|
|
}
|
|
var networkMode = mode;
|
|
if (containerName) {
|
|
networkMode += ':' + containerName;
|
|
config.Hostname = '';
|
|
}
|
|
config.HostConfig.NetworkMode = networkMode;
|
|
config.MacAddress = $scope.formValues.MacAddress;
|
|
|
|
config.NetworkingConfig.EndpointsConfig[networkMode] = {
|
|
IPAMConfig: {
|
|
IPv4Address: $scope.formValues.IPv4,
|
|
IPv6Address: $scope.formValues.IPv6,
|
|
},
|
|
};
|
|
|
|
if (networkMode && _.get($scope.config.NetworkingConfig.EndpointsConfig[networkMode], 'Aliases')) {
|
|
var aliases = $scope.config.NetworkingConfig.EndpointsConfig[networkMode].Aliases;
|
|
config.NetworkingConfig.EndpointsConfig[networkMode].Aliases = _.filter(aliases, (o) => {
|
|
return !_.startsWith($scope.fromContainer.Id, o);
|
|
});
|
|
}
|
|
|
|
var dnsServers = [];
|
|
if ($scope.formValues.DnsPrimary) {
|
|
dnsServers.push($scope.formValues.DnsPrimary);
|
|
}
|
|
if ($scope.formValues.DnsSecondary) {
|
|
dnsServers.push($scope.formValues.DnsSecondary);
|
|
}
|
|
config.HostConfig.Dns = dnsServers;
|
|
|
|
config.HostConfig.ExtraHosts = _.map(
|
|
_.filter($scope.formValues.ExtraHosts, (v) => v.value),
|
|
'value'
|
|
);
|
|
}
|
|
|
|
function prepareLabels(config) {
|
|
var labels = {};
|
|
$scope.formValues.Labels.forEach(function (label) {
|
|
if (label.name) {
|
|
if (label.value) {
|
|
labels[label.name] = label.value;
|
|
} else {
|
|
labels[label.name] = '';
|
|
}
|
|
}
|
|
});
|
|
config.Labels = labels;
|
|
}
|
|
|
|
function prepareDevices(config) {
|
|
var path = [];
|
|
config.HostConfig.Devices.forEach(function (p) {
|
|
if (p.pathOnHost) {
|
|
if (p.pathInContainer === '') {
|
|
p.pathInContainer = p.pathOnHost;
|
|
}
|
|
path.push({ PathOnHost: p.pathOnHost, PathInContainer: p.pathInContainer, CgroupPermissions: 'rwm' });
|
|
}
|
|
});
|
|
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) {
|
|
var memoryLimit = (Math.round($scope.formValues.MemoryLimit * 8) / 8).toFixed(3);
|
|
memoryLimit *= 1024 * 1024;
|
|
config.HostConfig.Memory = memoryLimit;
|
|
}
|
|
|
|
// Memory Resevation - Round to 0.125
|
|
if ($scope.formValues.MemoryReservation >= 0) {
|
|
var memoryReservation = (Math.round($scope.formValues.MemoryReservation * 8) / 8).toFixed(3);
|
|
memoryReservation *= 1024 * 1024;
|
|
config.HostConfig.MemoryReservation = memoryReservation;
|
|
}
|
|
|
|
// CPU Limit
|
|
if ($scope.formValues.CpuLimit >= 0) {
|
|
config.HostConfig.NanoCpus = $scope.formValues.CpuLimit * 1000000000;
|
|
}
|
|
}
|
|
|
|
function prepareLogDriver(config) {
|
|
var logOpts = {};
|
|
if ($scope.formValues.LogDriverName) {
|
|
config.HostConfig.LogConfig = { Type: $scope.formValues.LogDriverName };
|
|
if ($scope.formValues.LogDriverName !== 'none') {
|
|
$scope.formValues.LogDriverOpts.forEach(function (opt) {
|
|
if (opt.name) {
|
|
logOpts[opt.name] = opt.value;
|
|
}
|
|
});
|
|
if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) {
|
|
config.HostConfig.LogConfig.Config = logOpts;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function prepareCapabilities(config) {
|
|
var allowed = $scope.formValues.capabilities.filter(function (item) {
|
|
return item.allowed === true;
|
|
});
|
|
var notAllowed = $scope.formValues.capabilities.filter(function (item) {
|
|
return item.allowed === false;
|
|
});
|
|
|
|
var getCapName = function (item) {
|
|
return item.capability;
|
|
};
|
|
config.HostConfig.CapAdd = allowed.map(getCapName);
|
|
config.HostConfig.CapDrop = notAllowed.map(getCapName);
|
|
}
|
|
|
|
function prepareGPUOptions(config) {
|
|
const driver = 'nvidia';
|
|
const gpuOptions = $scope.formValues.GPU;
|
|
|
|
const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
|
|
return o.Driver === driver || o.Capabilities[0][0] === 'gpu';
|
|
});
|
|
if (existingDeviceRequest) {
|
|
_.pullAllBy(config.HostConfig.DeviceRequests, [existingDeviceRequest], 'Driver');
|
|
}
|
|
|
|
if (!gpuOptions.enabled) {
|
|
return;
|
|
}
|
|
|
|
const deviceRequest = existingDeviceRequest || {
|
|
Driver: driver,
|
|
Count: -1,
|
|
DeviceIDs: [], // must be empty if Count != 0 https://github.com/moby/moby/blob/master/daemon/nvidia_linux.go#L50
|
|
Capabilities: [], // array of ORed arrays of ANDed capabilites = [ [c1 AND c2] OR [c1 AND c3] ] : https://github.com/moby/moby/blob/master/api/types/container/host_config.go#L272
|
|
// Options: { property1: "string", property2: "string" }, // seems to never be evaluated/used in docker API ?
|
|
};
|
|
|
|
deviceRequest.DeviceIDs = gpuOptions.selectedGPUs;
|
|
deviceRequest.Count = 0;
|
|
deviceRequest.Capabilities = [gpuOptions.capabilities];
|
|
|
|
config.HostConfig.DeviceRequests.push(deviceRequest);
|
|
}
|
|
|
|
function prepareConfiguration() {
|
|
var config = angular.copy($scope.config);
|
|
prepareCmd(config);
|
|
prepareEntrypoint(config);
|
|
prepareNetworkConfig(config);
|
|
prepareImageConfig(config);
|
|
preparePortBindings(config);
|
|
prepareConsole(config);
|
|
prepareEnvironmentVariables(config);
|
|
prepareVolumes(config);
|
|
prepareLabels(config);
|
|
prepareDevices(config);
|
|
prepareResources(config);
|
|
prepareLogDriver(config);
|
|
prepareCapabilities(config);
|
|
prepareSysctls(config);
|
|
prepareGPUOptions(config);
|
|
return config;
|
|
}
|
|
|
|
function loadFromContainerCmd() {
|
|
if ($scope.config.Cmd) {
|
|
$scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd);
|
|
$scope.formValues.CmdMode = 'override';
|
|
}
|
|
}
|
|
|
|
function loadFromContainerEntrypoint() {
|
|
if (_.has($scope.config, 'Entrypoint')) {
|
|
if ($scope.config.Entrypoint == null) {
|
|
$scope.config.Entrypoint = '';
|
|
}
|
|
$scope.formValues.EntrypointMode = 'override';
|
|
}
|
|
}
|
|
|
|
function loadFromContainerPortBindings() {
|
|
const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings);
|
|
$scope.config.HostConfig.PortBindings = bindings;
|
|
}
|
|
|
|
function loadFromContainerVolumes(d) {
|
|
for (var v in d.Mounts) {
|
|
if ({}.hasOwnProperty.call(d.Mounts, v)) {
|
|
var mount = d.Mounts[v];
|
|
var volume = {
|
|
type: mount.Type,
|
|
name: mount.Name || mount.Source,
|
|
containerPath: mount.Destination,
|
|
readOnly: mount.RW === false,
|
|
};
|
|
$scope.formValues.Volumes.push(volume);
|
|
}
|
|
}
|
|
}
|
|
|
|
$scope.resetNetworkConfig = function () {
|
|
$scope.config.NetworkingConfig = {
|
|
EndpointsConfig: {},
|
|
};
|
|
};
|
|
|
|
function loadFromContainerNetworkConfig(d) {
|
|
$scope.config.NetworkingConfig = {
|
|
EndpointsConfig: {},
|
|
};
|
|
var networkMode = d.HostConfig.NetworkMode;
|
|
if (networkMode === 'default') {
|
|
$scope.config.HostConfig.NetworkMode = 'bridge';
|
|
if (!_.find($scope.availableNetworks, { Name: 'bridge' })) {
|
|
$scope.config.HostConfig.NetworkMode = 'nat';
|
|
}
|
|
}
|
|
if ($scope.config.HostConfig.NetworkMode.indexOf('container:') === 0) {
|
|
var netContainer = $scope.config.HostConfig.NetworkMode.split(/^container:/)[1];
|
|
$scope.config.HostConfig.NetworkMode = 'container';
|
|
for (var c in $scope.runningContainers) {
|
|
if ($scope.runningContainers[c].Id == netContainer) {
|
|
$scope.formValues.NetworkContainer = $scope.runningContainers[c];
|
|
}
|
|
}
|
|
}
|
|
$scope.fromContainerMultipleNetworks = Object.keys(d.NetworkSettings.Networks).length >= 2;
|
|
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]) {
|
|
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig) {
|
|
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address) {
|
|
$scope.formValues.IPv4 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address;
|
|
}
|
|
if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address) {
|
|
$scope.formValues.IPv6 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address;
|
|
}
|
|
}
|
|
}
|
|
$scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode];
|
|
if (Object.keys(d.NetworkSettings.Networks).length > 1) {
|
|
var firstNetwork = d.NetworkSettings.Networks[Object.keys(d.NetworkSettings.Networks)[0]];
|
|
$scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = firstNetwork;
|
|
$scope.extraNetworks = angular.copy(d.NetworkSettings.Networks);
|
|
delete $scope.extraNetworks[Object.keys(d.NetworkSettings.Networks)[0]];
|
|
}
|
|
$scope.formValues.MacAddress = d.Config.MacAddress;
|
|
|
|
if (d.HostConfig.Dns && d.HostConfig.Dns[0]) {
|
|
$scope.formValues.DnsPrimary = d.HostConfig.Dns[0];
|
|
if (d.HostConfig.Dns[1]) {
|
|
$scope.formValues.DnsSecondary = d.HostConfig.Dns[1];
|
|
}
|
|
}
|
|
|
|
// ExtraHosts
|
|
if ($scope.config.HostConfig.ExtraHosts) {
|
|
var extraHosts = $scope.config.HostConfig.ExtraHosts;
|
|
for (var i = 0; i < extraHosts.length; i++) {
|
|
var host = extraHosts[i];
|
|
$scope.formValues.ExtraHosts.push({ value: host });
|
|
}
|
|
$scope.config.HostConfig.ExtraHosts = [];
|
|
}
|
|
}
|
|
|
|
function loadFromContainerEnvironmentVariables() {
|
|
$scope.formValues.Env = envVarsUtils.parseArrayOfStrings($scope.config.Env);
|
|
}
|
|
|
|
function loadFromContainerLabels() {
|
|
for (var l in $scope.config.Labels) {
|
|
if ({}.hasOwnProperty.call($scope.config.Labels, l)) {
|
|
$scope.formValues.Labels.push({ name: l, value: $scope.config.Labels[l] });
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadFromContainerConsole() {
|
|
if ($scope.config.OpenStdin && $scope.config.Tty) {
|
|
$scope.formValues.Console = 'both';
|
|
} else if (!$scope.config.OpenStdin && $scope.config.Tty) {
|
|
$scope.formValues.Console = 'tty';
|
|
} else if ($scope.config.OpenStdin && !$scope.config.Tty) {
|
|
$scope.formValues.Console = 'interactive';
|
|
} else if (!$scope.config.OpenStdin && !$scope.config.Tty) {
|
|
$scope.formValues.Console = 'none';
|
|
}
|
|
}
|
|
|
|
function loadFromContainerDevices() {
|
|
var path = [];
|
|
for (var dev in $scope.config.HostConfig.Devices) {
|
|
if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) {
|
|
var device = $scope.config.HostConfig.Devices[dev];
|
|
path.push({ pathOnHost: device.PathOnHost, pathInContainer: device.PathInContainer });
|
|
}
|
|
}
|
|
$scope.config.HostConfig.Devices = path;
|
|
}
|
|
|
|
function loadFromContainerDeviceRequests() {
|
|
const deviceRequest = _.find($scope.config.HostConfig.DeviceRequests, function (o) {
|
|
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
|
|
});
|
|
if (deviceRequest) {
|
|
$scope.formValues.GPU.enabled = true;
|
|
$scope.formValues.GPU.useSpecific = deviceRequest.Count !== -1;
|
|
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs || [];
|
|
if ($scope.formValues.GPU.useSpecific) {
|
|
$scope.formValues.GPU.selectedGPUs = deviceRequest.DeviceIDs;
|
|
}
|
|
// we only support a single set of capabilities for now
|
|
// UI needs to be reworked in order to support OR combinations of AND capabilities
|
|
$scope.formValues.GPU.capabilities = deviceRequest.Capabilities[0];
|
|
$scope.formValues.GPU = { ...$scope.formValues.GPU };
|
|
}
|
|
}
|
|
|
|
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, endpoint.Id)
|
|
.then((model) => {
|
|
$scope.formValues.RegistryModel = model;
|
|
})
|
|
.catch(function error(err) {
|
|
Notifications.error('Failure', err, 'Unable to retrieve registry');
|
|
});
|
|
}
|
|
|
|
function loadFromContainerResources(d) {
|
|
if (d.HostConfig.NanoCpus) {
|
|
$scope.formValues.CpuLimit = d.HostConfig.NanoCpus / 1000000000;
|
|
}
|
|
if (d.HostConfig.Memory) {
|
|
$scope.formValues.MemoryLimit = d.HostConfig.Memory / 1024 / 1024;
|
|
}
|
|
if (d.HostConfig.MemoryReservation) {
|
|
$scope.formValues.MemoryReservation = d.HostConfig.MemoryReservation / 1024 / 1024;
|
|
}
|
|
}
|
|
|
|
function loadFromContainerCapabilities(d) {
|
|
if (d.HostConfig.CapAdd) {
|
|
d.HostConfig.CapAdd.forEach(function (cap) {
|
|
$scope.formValues.capabilities.push(new ContainerCapability(cap, true));
|
|
});
|
|
}
|
|
if (d.HostConfig.CapDrop) {
|
|
d.HostConfig.CapDrop.forEach(function (cap) {
|
|
$scope.formValues.capabilities.push(new ContainerCapability(cap, false));
|
|
});
|
|
}
|
|
|
|
function hasCapability(item) {
|
|
return item.capability === cap.capability;
|
|
}
|
|
|
|
var capabilities = new ContainerCapabilities();
|
|
for (var i = 0; i < capabilities.length; i++) {
|
|
var cap = capabilities[i];
|
|
if (!_.find($scope.formValues.capabilities, hasCapability)) {
|
|
$scope.formValues.capabilities.push(cap);
|
|
}
|
|
}
|
|
|
|
$scope.formValues.capabilities.sort(function (a, b) {
|
|
return a.capability < b.capability ? -1 : 1;
|
|
});
|
|
}
|
|
|
|
function loadFromContainerSpec() {
|
|
// Get container
|
|
Container.get({ id: $transition$.params().from })
|
|
.$promise.then(function success(d) {
|
|
var fromContainer = new ContainerDetailsViewModel(d);
|
|
if (fromContainer.ResourceControl && fromContainer.ResourceControl.Public) {
|
|
$scope.formValues.AccessControlData.AccessControlEnabled = false;
|
|
}
|
|
$scope.fromContainer = fromContainer;
|
|
$scope.state.mode = 'duplicate';
|
|
$scope.config = ContainerHelper.configFromContainer(fromContainer.Model);
|
|
loadFromContainerCmd(d);
|
|
loadFromContainerEntrypoint(d);
|
|
loadFromContainerLogging(d);
|
|
loadFromContainerPortBindings(d);
|
|
loadFromContainerVolumes(d);
|
|
loadFromContainerNetworkConfig(d);
|
|
loadFromContainerEnvironmentVariables(d);
|
|
loadFromContainerLabels(d);
|
|
loadFromContainerConsole(d);
|
|
loadFromContainerDevices(d);
|
|
loadFromContainerDeviceRequests(d);
|
|
loadFromContainerImageConfig(d);
|
|
loadFromContainerResources(d);
|
|
loadFromContainerCapabilities(d);
|
|
loadFromContainerSysctls(d);
|
|
})
|
|
.catch(function error(err) {
|
|
Notifications.error('Failure', err, 'Unable to retrieve container');
|
|
});
|
|
}
|
|
|
|
function loadFromContainerLogging(config) {
|
|
var logConfig = config.HostConfig.LogConfig;
|
|
$scope.formValues.LogDriverName = logConfig.Type;
|
|
$scope.formValues.LogDriverOpts = _.map(logConfig.Config, function (value, name) {
|
|
return {
|
|
name: name,
|
|
value: value,
|
|
};
|
|
});
|
|
}
|
|
|
|
async function initView() {
|
|
var nodeName = $transition$.params().nodeName;
|
|
$scope.formValues.NodeName = nodeName;
|
|
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
|
|
|
$scope.isAdmin = Authentication.isAdmin();
|
|
$scope.showDeviceMapping = await shouldShowDevices();
|
|
$scope.showSysctls = await shouldShowSysctls();
|
|
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
|
|
$scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
|
|
|
|
Volume.query(
|
|
{},
|
|
function (d) {
|
|
$scope.availableVolumes = d.Volumes.sort((vol1, vol2) => {
|
|
return vol1.Name.localeCompare(vol2.Name);
|
|
});
|
|
},
|
|
function (e) {
|
|
Notifications.error('Failure', e, 'Unable to retrieve volumes');
|
|
}
|
|
);
|
|
|
|
var provider = $scope.applicationState.endpoint.mode.provider;
|
|
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
|
NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25)
|
|
.then(function success(networks) {
|
|
networks.push({ Name: 'container' });
|
|
$scope.availableNetworks = networks.sort((a, b) => a.Name.localeCompare(b.Name));
|
|
|
|
if (_.find(networks, { Name: 'nat' })) {
|
|
$scope.config.HostConfig.NetworkMode = 'nat';
|
|
}
|
|
})
|
|
.catch(function error(err) {
|
|
Notifications.error('Failure', err, 'Unable to retrieve networks');
|
|
});
|
|
|
|
Container.query(
|
|
{},
|
|
function (d) {
|
|
var containers = d;
|
|
$scope.runningContainers = containers;
|
|
$scope.gpuUseAll = $scope.endpoint.Snapshots[0].GpuUseAll;
|
|
$scope.gpuUseList = $scope.endpoint.Snapshots[0].GpuUseList;
|
|
if ($transition$.params().from) {
|
|
loadFromContainerSpec();
|
|
} else {
|
|
$scope.fromContainer = {};
|
|
$scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : [];
|
|
}
|
|
},
|
|
function (e) {
|
|
Notifications.error('Failure', e, 'Unable to retrieve running containers');
|
|
}
|
|
);
|
|
|
|
SystemService.info()
|
|
.then(function success(data) {
|
|
$scope.availableRuntimes = data.Runtimes ? Object.keys(data.Runtimes) : [];
|
|
$scope.state.sliderMaxCpu = 32;
|
|
if (data.NCPU) {
|
|
$scope.state.sliderMaxCpu = data.NCPU;
|
|
}
|
|
$scope.state.sliderMaxMemory = 32768;
|
|
if (data.MemTotal) {
|
|
$scope.state.sliderMaxMemory = Math.floor(data.MemTotal / 1000 / 1000);
|
|
}
|
|
})
|
|
.catch(function error(err) {
|
|
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
|
});
|
|
|
|
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
|
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
|
|
|
|
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
|
|
$scope.availableLoggingDrivers = loggingDrivers;
|
|
});
|
|
}
|
|
|
|
function validateForm(accessControlData, isAdmin) {
|
|
$scope.state.formValidationError = '';
|
|
var error = '';
|
|
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
|
|
|
if (error) {
|
|
$scope.state.formValidationError = error;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
$scope.handleResourceChange = handleResourceChange;
|
|
function handleResourceChange() {
|
|
$scope.state.settingUnlimitedResources = false;
|
|
if (
|
|
($scope.config.HostConfig.Memory > 0 && $scope.formValues.MemoryLimit === 0) ||
|
|
($scope.config.HostConfig.MemoryReservation > 0 && $scope.formValues.MemoryReservation === 0) ||
|
|
($scope.config.HostConfig.NanoCpus > 0 && $scope.formValues.CpuLimit === 0)
|
|
) {
|
|
$scope.state.settingUnlimitedResources = true;
|
|
}
|
|
}
|
|
|
|
async function updateLimits(config) {
|
|
try {
|
|
if ($scope.state.settingUnlimitedResources) {
|
|
create();
|
|
} else {
|
|
await ContainerService.updateLimits($transition$.params().from, config);
|
|
$scope.config = config;
|
|
Notifications.success('Success', 'Limits updated');
|
|
}
|
|
} catch (err) {
|
|
Notifications.error('Failure', err, 'Update Limits fail');
|
|
}
|
|
}
|
|
|
|
async function update() {
|
|
$scope.state.actionInProgress = true;
|
|
var config = angular.copy($scope.config);
|
|
prepareResources(config);
|
|
await updateLimits(config);
|
|
$scope.state.actionInProgress = false;
|
|
}
|
|
|
|
function create() {
|
|
var oldContainer = null;
|
|
HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName);
|
|
return findCurrentContainer().then(setOldContainer).then(confirmCreateContainer).then(startCreationProcess).catch(notifyOnError).finally(final);
|
|
|
|
function final() {
|
|
$scope.state.actionInProgress = false;
|
|
}
|
|
|
|
function setOldContainer(container) {
|
|
oldContainer = container;
|
|
return container;
|
|
}
|
|
|
|
function findCurrentContainer() {
|
|
return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } })
|
|
.$promise.then(function onQuerySuccess(containers) {
|
|
if (!containers.length) {
|
|
return;
|
|
}
|
|
return containers[0];
|
|
})
|
|
.catch(notifyOnError);
|
|
|
|
function notifyOnError(err) {
|
|
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
|
}
|
|
}
|
|
|
|
function startCreationProcess(confirmed) {
|
|
if (!confirmed) {
|
|
return $q.when();
|
|
}
|
|
if (!validateAccessControl()) {
|
|
return $q.when();
|
|
}
|
|
$scope.state.actionInProgress = true;
|
|
return pullImageIfNeeded()
|
|
.then(stopAndRenameContainer)
|
|
.then(createNewContainer)
|
|
.then(applyResourceControl)
|
|
.then(connectToExtraNetworks)
|
|
.then(removeOldContainer)
|
|
.then(onSuccess)
|
|
.catch(onCreationProcessFail);
|
|
}
|
|
|
|
function onCreationProcessFail(error) {
|
|
var deferred = $q.defer();
|
|
removeNewContainer()
|
|
.then(restoreOldContainerName)
|
|
.then(function () {
|
|
deferred.reject(error);
|
|
})
|
|
.catch(function (restoreError) {
|
|
deferred.reject(restoreError);
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
|
|
function removeNewContainer() {
|
|
return findCurrentContainer().then(function onContainerLoaded(container) {
|
|
if (container && (!oldContainer || container.Id !== oldContainer.Id)) {
|
|
return ContainerService.remove(container, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
function restoreOldContainerName() {
|
|
if (!oldContainer) {
|
|
return;
|
|
}
|
|
return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1));
|
|
}
|
|
|
|
function confirmCreateContainer(container) {
|
|
if (!container) {
|
|
return $q.when(true);
|
|
}
|
|
|
|
return showConfirmationModal();
|
|
|
|
function showConfirmationModal() {
|
|
var deferred = $q.defer();
|
|
|
|
ModalService.confirmDestructive({
|
|
title: 'Are you sure ?',
|
|
message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?',
|
|
buttons: {
|
|
confirm: {
|
|
label: 'Replace',
|
|
className: 'btn-danger',
|
|
},
|
|
},
|
|
callback: function onConfirm(confirmed) {
|
|
deferred.resolve(confirmed);
|
|
},
|
|
});
|
|
|
|
return deferred.promise;
|
|
}
|
|
}
|
|
|
|
function stopAndRenameContainer() {
|
|
if (!oldContainer) {
|
|
return $q.when();
|
|
}
|
|
return stopContainerIfNeeded(oldContainer).then(renameContainer);
|
|
}
|
|
|
|
function stopContainerIfNeeded(oldContainer) {
|
|
if (oldContainer.State !== 'running') {
|
|
return $q.when();
|
|
}
|
|
return ContainerService.stopContainer(oldContainer.Id);
|
|
}
|
|
|
|
function renameContainer() {
|
|
return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1) + '-old');
|
|
}
|
|
|
|
function pullImageIfNeeded() {
|
|
return $q.when($scope.formValues.alwaysPull && ImageService.pullImage($scope.formValues.RegistryModel, true));
|
|
}
|
|
|
|
function createNewContainer() {
|
|
return $async(async () => {
|
|
const config = prepareConfiguration();
|
|
return await ContainerService.createAndStartContainer(config);
|
|
});
|
|
}
|
|
|
|
function applyResourceControl(newContainer) {
|
|
const userId = Authentication.getUserDetails().ID;
|
|
const resourceControl = newContainer.Portainer.ResourceControl;
|
|
const containerId = newContainer.Id;
|
|
const accessControlData = $scope.formValues.AccessControlData;
|
|
|
|
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl).then(function onApplyResourceControlSuccess() {
|
|
return containerId;
|
|
});
|
|
}
|
|
|
|
function connectToExtraNetworks(newContainerId) {
|
|
if (!$scope.extraNetworks) {
|
|
return $q.when();
|
|
}
|
|
|
|
var connectionPromises = _.forOwn($scope.extraNetworks, function (network, networkName) {
|
|
if (_.has(network, 'Aliases')) {
|
|
var aliases = _.filter(network.Aliases, (o) => {
|
|
return !_.startsWith($scope.fromContainer.Id, o);
|
|
});
|
|
}
|
|
return NetworkService.connectContainer(networkName, newContainerId, aliases);
|
|
});
|
|
|
|
return $q.all(connectionPromises);
|
|
}
|
|
|
|
function removeOldContainer() {
|
|
var deferred = $q.defer();
|
|
|
|
if (!oldContainer) {
|
|
deferred.resolve();
|
|
return;
|
|
}
|
|
|
|
ContainerService.remove(oldContainer, true).then(notifyOnRemoval).catch(notifyOnRemoveError);
|
|
|
|
return deferred.promise;
|
|
|
|
function notifyOnRemoval() {
|
|
Notifications.success('Container Removed', oldContainer.Id);
|
|
deferred.resolve();
|
|
}
|
|
|
|
function notifyOnRemoveError(err) {
|
|
deferred.reject({ msg: 'Unable to remove container', err: err });
|
|
}
|
|
}
|
|
|
|
function notifyOnError(err) {
|
|
Notifications.error('Failure', err, 'Unable to create container');
|
|
}
|
|
|
|
function validateAccessControl() {
|
|
var accessControlData = $scope.formValues.AccessControlData;
|
|
return validateForm(accessControlData, $scope.isAdmin);
|
|
}
|
|
|
|
function onSuccess() {
|
|
Notifications.success('Success', 'Container successfully created');
|
|
$state.go('docker.containers', {}, { reload: true });
|
|
}
|
|
}
|
|
|
|
async function shouldShowDevices() {
|
|
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
|
}
|
|
|
|
async function shouldShowSysctls() {
|
|
return endpoint.SecuritySettings.allowSysctlSettingForRegularUsers || Authentication.isAdmin();
|
|
}
|
|
|
|
async function checkIfContainerCapabilitiesEnabled() {
|
|
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
|
}
|
|
|
|
initView();
|
|
},
|
|
]);
|