Compare commits
3 Commits
fix/EE-167
...
feat/GH/31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
785c4e0627 | ||
|
|
aae9ecc49f | ||
|
|
a99ff34c05 |
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user