portainer/app/docker/views/containers/create/createContainerController.js

738 lines
24 KiB
JavaScript

import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { confirmDestructive } from '@@/modals/confirm';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { buildConfirmButton } from '@@/modals/utils';
import { commandsTabUtils } from '@/react/docker/containers/CreateView/CommandsTab';
import { volumesTabUtils } from '@/react/docker/containers/CreateView/VolumesTab';
import { networkTabUtils } from '@/react/docker/containers/CreateView/NetworkTab';
import { ContainerCapabilities, ContainerCapability } from '@/docker/models/containerCapabilities';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { ContainerDetailsViewModel } from '@/docker/models/container';
import './createcontainer.css';
import { envVarsTabUtils } from '@/react/docker/containers/CreateView/EnvVarsTab';
import { getContainers } from '@/react/docker/containers/queries/containers';
import { resourcesTabUtils } from '@/react/docker/containers/CreateView/ResourcesTab';
angular.module('portainer.docker').controller('CreateContainerController', [
'$q',
'$scope',
'$async',
'$state',
'$timeout',
'$transition$',
'$analytics',
'Container',
'ContainerHelper',
'ImageHelper',
'NetworkService',
'ResourceControlService',
'Authentication',
'Notifications',
'ContainerService',
'ImageService',
'FormValidator',
'RegistryService',
'SystemService',
'SettingsService',
'HttpRequestHelper',
'endpoint',
function (
$q,
$scope,
$async,
$state,
$timeout,
$transition$,
$analytics,
Container,
ContainerHelper,
ImageHelper,
NetworkService,
ResourceControlService,
Authentication,
Notifications,
ContainerService,
ImageService,
FormValidator,
RegistryService,
SystemService,
SettingsService,
HttpRequestHelper,
endpoint
) {
$scope.create = create;
$scope.endpoint = endpoint;
$scope.containerWebhookFeature = FeatureId.CONTAINER_WEBHOOK;
$scope.formValues = {
alwaysPull: true,
GPU: {
enabled: false,
useSpecific: false,
selectedGPUs: ['all'],
capabilities: ['compute', 'utility'],
},
Labels: [],
ExtraHosts: [],
MacAddress: '',
IPv4: '',
IPv6: '',
DnsPrimary: '',
DnsSecondary: '',
AccessControlData: new AccessControlFormData(),
NodeName: null,
capabilities: [],
RegistryModel: new PorImageRegistryModel(),
commands: commandsTabUtils.getDefaultViewModel(),
envVars: envVarsTabUtils.getDefaultViewModel(),
volumes: volumesTabUtils.getDefaultViewModel(),
network: networkTabUtils.getDefaultViewModel(),
resources: resourcesTabUtils.getDefaultViewModel(),
};
$scope.state = {
formValidationError: '',
actionInProgress: false,
mode: '',
pullImageValidity: true,
settingUnlimitedResources: false,
containerIsLoaded: false,
};
$scope.onAlwaysPullChange = onAlwaysPullChange;
$scope.handlePublishAllPortsChange = handlePublishAllPortsChange;
$scope.handleAutoRemoveChange = handleAutoRemoveChange;
$scope.handlePrivilegedChange = handlePrivilegedChange;
$scope.handleInitChange = handleInitChange;
$scope.handleCommandsChange = handleCommandsChange;
$scope.handleEnvVarsChange = handleEnvVarsChange;
function handleCommandsChange(commands) {
return $scope.$evalAsync(() => {
$scope.formValues.commands = commands;
});
}
function handleEnvVarsChange(value) {
return $scope.$evalAsync(() => {
$scope.formValues.envVars = value;
});
}
$scope.onVolumesChange = function (volumes) {
return $scope.$evalAsync(() => {
$scope.formValues.volumes = volumes;
});
};
$scope.onNetworkChange = function (network) {
return $scope.$evalAsync(() => {
$scope.formValues.network = network;
});
};
$scope.onResourcesChange = function (resources) {
return $scope.$evalAsync(() => {
$scope.formValues.resources = resources;
});
};
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.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: null,
MacAddress: '',
ExposedPorts: {},
Entrypoint: null,
WorkingDir: '',
User: '',
HostConfig: {
RestartPolicy: {
Name: 'no',
},
PortBindings: [],
PublishAllPorts: false,
Binds: [],
AutoRemove: false,
NetworkMode: 'bridge',
Privileged: false,
Init: false,
Runtime: null,
ExtraHosts: [],
Devices: [],
DeviceRequests: [],
CapAdd: [],
CapDrop: [],
Sysctls: {},
LogConfig: {
Type: '',
Config: {},
},
},
NetworkingConfig: {
EndpointsConfig: {},
},
Labels: {},
};
$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.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 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 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 prepareConfiguration() {
var config = angular.copy($scope.config);
config = commandsTabUtils.toRequest(config, $scope.formValues.commands);
config = envVarsTabUtils.toRequest(config, $scope.formValues.envVars);
config = volumesTabUtils.toRequest(config, $scope.formValues.volumes);
config = networkTabUtils.toRequest(config, $scope.formValues.network, $scope.fromContainer.Id);
config = resourcesTabUtils.toRequest(config, $scope.formValues.resources);
prepareImageConfig(config);
preparePortBindings(config);
prepareLabels(config);
prepareCapabilities(config);
return config;
}
function loadFromContainerPortBindings() {
const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings);
$scope.config.HostConfig.PortBindings = bindings;
}
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 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 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) {
if (fromContainer.ResourceControl.Public) {
$scope.formValues.AccessControlData.AccessControlEnabled = false;
}
// When the container is create by duplicate/edit, the access permission
// shouldn't be copied
fromContainer.ResourceControl.UserAccesses = [];
fromContainer.ResourceControl.TeamAccesses = [];
}
$scope.fromContainer = fromContainer;
$scope.state.mode = 'duplicate';
$scope.config = ContainerHelper.configFromContainer(angular.copy(d));
$scope.formValues.commands = commandsTabUtils.toViewModel(d);
$scope.formValues.envVars = envVarsTabUtils.toViewModel(d);
$scope.formValues.volumes = volumesTabUtils.toViewModel(d);
$scope.formValues.network = networkTabUtils.toViewModel(d, $scope.availableNetworks, $scope.runningContainers);
$scope.formValues.resources = resourcesTabUtils.toViewModel(d);
loadFromContainerPortBindings(d);
loadFromContainerLabels(d);
loadFromContainerImageConfig(d);
loadFromContainerCapabilities(d);
})
.then(() => {
$scope.state.containerIsLoaded = true;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve container');
});
}
async function initView() {
var nodeName = $transition$.params().nodeName;
$scope.formValues.NodeName = nodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
$scope.isAdmin = Authentication.isAdmin();
$scope.showDeviceMapping = await shouldShowDevices();
$scope.allowSysctl = await shouldShowSysctls();
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
$scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
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');
});
getContainers(endpoint.Id)
.then((containers) => {
$scope.runningContainers = containers;
$scope.gpuUseAll = _.get($scope, 'endpoint.Snapshots[0].GpuUseAll', false);
$scope.gpuUseList = _.get($scope, 'endpoint.Snapshots[0].GpuUseList', []);
if ($transition$.params().from) {
loadFromContainerSpec();
} else {
$scope.state.containerIsLoaded = true;
$scope.fromContainer = {};
$scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : [];
}
})
.catch((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;
}
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;
}
}
$scope.redeployUnlimitedResources = function (resources) {
return $async(async () => {
$scope.formValues.resources = resources;
return create();
});
};
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]);
}
function confirmCreateContainer(container) {
if (!container) {
return $q.when(true);
}
return showConfirmationModal();
function showConfirmationModal() {
var deferred = $q.defer();
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?',
confirmButton: buildConfirmButton('Replace', 'danger'),
}).then(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] + '-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);
});
}
async function sendAnalytics() {
const publicSettings = await SettingsService.publicSettings();
const analyticsAllowed = publicSettings.EnableTelemetry;
const image = `${$scope.formValues.RegistryModel.Registry.URL}/${$scope.formValues.RegistryModel.Image}`;
if (analyticsAllowed && $scope.formValues.GPU.enabled) {
$analytics.eventTrack('gpuContainerCreated', {
category: 'docker',
metadata: { gpu: $scope.formValues.GPU, containerImage: image },
});
}
}
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.formValues.network.extraNetworks) {
return $q.when();
}
var connectionPromises = _.forOwn($scope.formValues.network.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);
}
async function onSuccess() {
await sendAnalytics();
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();
},
]);