Compare commits

...

3 Commits

8 changed files with 654 additions and 385 deletions

View File

@@ -61,6 +61,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.formValues = {
alwaysPull: true,
GPU: {
enabled: false,
useSpecific: false,
selectedGPUs: [],
capabilities: [],
},
Console: 'none',
Volumes: [],
NetworkContainer: null,
@@ -90,6 +96,15 @@ angular.module('portainer.docker').controller('CreateContainerController', [
formValidationError: '',
actionInProgress: false,
mode: '',
nvidiaCapabilities: [
// Taken from https://github.com/containerd/containerd/blob/master/contrib/nvidia/nvidia.go#L40
{ name: 'compute', description: 'required for CUDA and OpenCL applications', selected: true },
{ name: 'compat32', description: 'required for running 32-bit applications', selected: false },
{ name: 'graphics', description: 'required for running OpenGL and Vulkan applications', selected: false },
{ name: 'utility', description: 'required for using nvidia-smi and NVML', selected: true },
{ name: 'video', description: 'required for using the Video Codec SDK', selected: false },
{ name: 'display', description: 'required for leveraging X11 display', selected: false },
],
};
$scope.refreshSlider = function () {
@@ -124,6 +139,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
Runtime: null,
ExtraHosts: [],
Devices: [],
DeviceRequests: [],
CapAdd: [],
CapDrop: [],
},
@@ -181,6 +197,20 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices.splice(index, 1);
};
$scope.addGPU = () => $scope.formValues.GPU.selectedGPUs.push({ key: '' });
$scope.removeGPU = (index) => $scope.formValues.GPU.selectedGPUs.splice(index, 1);
$scope.computeDockerGPUCommand = () => {
const useSpecific = $scope.formValues.GPU.useSpecific;
let gpuStr = 'all';
if (useSpecific) {
const computeGPUs = _.flow([(arr) => _.map(arr, 'key'), (arr) => _.join(arr, ',')]);
gpuStr = `"device=${computeGPUs($scope.formValues.GPU.selectedGPUs)}"`;
}
const computeCapabilities = _.flow([(arr) => _.map(arr, 'name'), (arr) => _.join(arr, ',')]);
const capStr = `"capabilities=${computeCapabilities($scope.formValues.GPU.capabilities)}"`;
return `--gpus '${gpuStr},${capStr}'`;
};
$scope.addLogDriverOpt = function () {
$scope.formValues.LogDriverOpts.push({ name: '', value: '' });
};
@@ -387,6 +417,36 @@ angular.module('portainer.docker').controller('CreateContainerController', [
config.HostConfig.CapDrop = notAllowed.map(getCapName);
}
function prepareGPUOptions(config) {
const gpuOptions = $scope.formValues.GPU;
if (!gpuOptions.enabled) {
return;
}
const driver = 'nvidia';
const existingDeviceRequest = _.find($scope.config.HostConfig.DeviceRequests, { Driver: driver });
if (existingDeviceRequest) {
_.pullAllBy(config.HostConfig.DeviceRequests, [existingDeviceRequest], 'Driver');
}
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 ?
};
if (gpuOptions.useSpecific) {
const gpuIds = _.map(gpuOptions.selectedGPUs, 'key');
deviceRequest.DeviceIDs = gpuIds;
deviceRequest.Count = gpuIds.length;
}
const caps = _.map(gpuOptions.capabilities, 'name');
// we only support a single set of capabilities for now
// UI needs to be reworked in order to support OR combinations of AND capabilities
deviceRequest.Capabilities = [caps];
config.HostConfig.DeviceRequests.push(deviceRequest);
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareCmd(config);
@@ -402,6 +462,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
prepareResources(config);
prepareLogDriver(config);
prepareCapabilities(config);
prepareGPUOptions(config);
return config;
}
@@ -547,6 +608,32 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices = path;
}
function loadFromContainerDeviceRequests() {
const deviceRequest = _.find($scope.config.HostConfig.DeviceRequests, { Driver: 'nvidia' });
if (deviceRequest) {
$scope.formValues.GPU.enabled = true;
$scope.formValues.GPU.useSpecific = deviceRequest.Count !== -1;
if ($scope.formValues.GPU.useSpecific) {
$scope.formValues.GPU.selectedGPUs = _.map(deviceRequest.DevicesIDs, (id) => {
return { key: id };
});
}
// we only support a single set of capabilities for now
// UI needs to be reworked in order to support OR combinations of AND capabilities
const caps = deviceRequest.Capabilities[0];
const fvCaps = _.map(caps, (cap) => {
return { name: cap };
});
$scope.formValues.GPU.capabilities = fvCaps;
_.forEach(caps, (cap) => {
const c = _.find($scope.state.nvidiaCapabilities, { name: cap });
if (c) {
c.selected = true;
}
});
}
}
function loadFromContainerImageConfig() {
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
.then((model) => {
@@ -619,6 +706,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
loadFromContainerLabels(d);
loadFromContainerConsole(d);
loadFromContainerDevices(d);
loadFromContainerDeviceRequests(d);
loadFromContainerImageConfig(d);
loadFromContainerResources(d);
loadFromContainerCapabilities(d);

View File

@@ -53,10 +53,8 @@
<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>
<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>
</div>
@@ -88,7 +86,8 @@
<portainer-tooltip
position="bottom"
message="When a range of ports on the host and a single port on the container is specified, Docker will randomly choose a single available port in the defined range and forward that to the container port."
></portainer-tooltip>
>
</portainer-tooltip>
</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> publish a new network port
@@ -151,7 +150,8 @@
<portainer-tooltip
position="bottom"
message="When enabled, Portainer will automatically remove the container when it exits. This is useful when you want to use the container only once."
></portainer-tooltip>
>
</portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="config.HostConfig.AutoRemove" /><i></i> </label>
</div>
@@ -227,7 +227,8 @@
<portainer-tooltip
position="bottom"
message="When container entrypoint is entered as part of the Command field, set Entrypoint to Override mode and leave blank, else it will revert to default."
></portainer-tooltip>
>
</portainer-tooltip>
</label>
<div class="col-sm-9">
<div class="input-group">
@@ -322,7 +323,8 @@
<portainer-tooltip
position="top"
message="Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation."
></portainer-tooltip>
>
</portainer-tooltip>
</label>
<span
class="label label-default interactive"
@@ -670,8 +672,6 @@
</div>
</div>
<!-- !runtimes -->
</form>
<form class="form-horizontal" style="margin-top: 15px;">
<!-- devices -->
<div ng-if="showDeviceMapping" class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
@@ -699,6 +699,86 @@
<!-- !devices-input-list -->
</div>
<!-- !devices-->
<!-- #region GPU -->
<div class="col-sm-12 form-section-title">
GPU
</div>
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.4">
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
Enable GPU
</label>
<div class="col-xs-9 col-sm-9 col-lg-4">
<label class="switch" style="margin-left: 5px;"> <input type="checkbox" name="use_gpu" ng-model="formValues.GPU.enabled" /><i></i> </label>
</div>
</div>
<div ng-if="formValues.GPU.enabled">
<!-- #region GPU DEVICES -->
<div class="form-group">
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
Use specific GPUs
<portainer-tooltip
position="top"
message="By default all GPUs will be usable by the container. Enable this if you want to give access to only specific host GPUs. Accept GPU indexes or UUIDs."
>
</portainer-tooltip>
</label>
<div class="col-xs-9 col-lg-4">
<label class="switch" style="margin-left: 5px;"> <input type="checkbox" name="use_gpu" ng-model="formValues.GPU.useSpecific" /><i></i> </label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-if="formValues.GPU.useSpecific" ng-click="addGPU()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add gpu
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-if="formValues.GPU.useSpecific && formValues.GPU.selectedGPUs.length">
<div ng-repeat="gpu in formValues.GPU.selectedGPUs track by $index" style="margin-top: 2px;">
<div class="input-group col-sm-8 input-group-sm">
<span class="input-group-addon">index or UUID</span>
<input type="text" class="form-control" ng-model="gpu.key" placeholder="e.g. 0 or GPU-fef8089b" />
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeGPU($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- #endregion -->
<div class="form-group">
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
Capabilities
<portainer-tooltip
position="top"
message="'compute' and 'utility' capabilities are preselected by Portainer because they are used by default when you don't explicitly specify capabilities with docker CLI '--gpus' option."
>
</portainer-tooltip>
</label>
<div class="col-xs-12 col-sm-9 col-lg-4">
<span
isteven-multi-select
input-model="state.nvidiaCapabilities"
output-model="formValues.GPU.capabilities"
output-properties="name"
button-label="name"
item-label="name - description"
tick-property="selected"
directive-id="nvidia_capabilities"
helper-elements=""
translation="{nothingSelected: 'No capabilities selected'}"
>
</span>
</div>
</div>
<div class="form-group">
<label class="col-xs-3 col-sm-3 col-lg-2 control-label text-left">
Control
<portainer-tooltip position="top" message="This is the generated equivalent of the '--gpus' docker CLI parameter based on your settings."> </portainer-tooltip>
</label>
<label class="col-xs-12 col-sm-9 col-lg-4">
<code>{{ computeDockerGPUCommand() }}</code>
</label>
</div>
</div>
<!-- #endregion GPU -->
<div class="col-sm-12 form-section-title">
Resources
</div>

View File

@@ -274,6 +274,10 @@
</container-restart-policy>
</td>
</tr>
<tr ng-if="container.HostConfig.DeviceRequests.length">
<td>GPUS</td>
<td>{{ computeDockerGPUCommand() }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>

View File

@@ -62,6 +62,21 @@ angular.module('portainer.docker').controller('ContainerController', [
$scope.updateRestartPolicy = updateRestartPolicy;
$scope.computeDockerGPUCommand = () => {
const gpuOptions = _.find($scope.container.HostConfig.DeviceRequests, { Driver: 'nvidia' });
if (!gpuOptions) {
return 'No GPU config found';
}
let gpuStr = 'all';
if (gpuOptions.Count !== -1) {
gpuStr = `"device=${_.join(gpuOptions.DeviceIDs, ',')}"`;
}
// we only support a single set of capabilities for now
// creation UI needs to be reworked in order to support OR combinations of AND capabilities
const capStr = `"capabilities=${_.join(gpuOptions.Capabilities[0], ',')}"`;
return `${gpuStr},${capStr}`;
};
var update = function () {
var nodeName = $transition$.params().nodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);

View File

@@ -46,8 +46,8 @@
ng-if="ctrl.availableUsersAndTeams.length > 0"
input-model="ctrl.availableUsersAndTeams"
output-model="ctrl.formValues.multiselectOutput"
button-label="icon '-' Name"
item-label="icon '-' Name"
button-label="icon Name"
item-label="icon Name"
tick-property="ticked"
helper-elements="filter"
search-property="Name"

View File

@@ -17,8 +17,8 @@
ng-if="ctrl.availableUsersAndTeams.length > 0"
input-model="ctrl.availableUsersAndTeams"
output-model="ctrl.formValues.multiselectOutput"
button-label="icon '-' Name"
item-label="icon '-' Name"
button-label="icon Name"
item-label="icon Name"
tick-property="ticked"
helper-elements="filter"
search-property="Name"

View File

@@ -62,7 +62,7 @@
"angular-messages": "1.8.0",
"angular-mocks": "1.8.0",
"angular-moment-picker": "^0.10.2",
"angular-multiselect": "github:portainer/angular-multi-select#semver:~v4.0.1",
"angular-multiselect": "github:portainer/angular-multi-select#semver:v4.0.x",
"angular-resource": "1.8.0",
"angular-sanitize": "1.8.0",
"angular-ui-bootstrap": "~2.5.0",
@@ -105,7 +105,7 @@
"clean-terminal-webpack-plugin": "^1.1.0",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^1.0.0",
"cssnano": "^3.10.0",
"cssnano": "^4.1.10",
"cypress": "^5.2.0",
"cypress-wait-until": "^1.7.1",
"eslint": "5.16.0",

820
yarn.lock

File diff suppressed because it is too large Load Diff