1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

refactor(docker/containers): migrate commands tab to react [EE-5208] (#10085)

This commit is contained in:
Chaim Lev-Ari 2023-09-04 19:07:29 +01:00 committed by GitHub
parent 46e73ee524
commit f7366d9788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1783 additions and 951 deletions

View file

@ -1,5 +1,4 @@
import _ from 'lodash-es';
import splitargs from 'splitargs/src/splitargs';
const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/m;
@ -65,18 +64,6 @@ angular.module('portainer.docker').factory('ContainerHelper', [
'use strict';
var helper = {};
helper.commandStringToArray = function (command) {
return splitargs(command);
};
helper.commandArrayToString = function (array) {
return array
.map(function (elem) {
return "'" + elem + "'";
})
.join(' ');
};
helper.configFromContainer = function (container) {
var config = container.Config;
// HostConfig

View file

@ -0,0 +1,9 @@
import { splitargs } from './splitargs';
export function commandStringToArray(command: string) {
return splitargs(command);
}
export function commandArrayToString(array: string[]) {
return array.map((elem) => `'${elem}'`).join(' ');
}

View file

@ -0,0 +1,68 @@
/**
* Created by elgs on 7/2/14.
*/
import { splitargs } from './splitargs';
describe('splitargs Suite', () => {
beforeEach(() => {});
afterEach(() => {});
it('should split double quoted string', () => {
const i = " I said 'I am sorry.', and he said \"it doesn't matter.\" ";
const o = splitargs(i);
expect(7).toBe(o.length);
expect(o[0]).toBe('I');
expect(o[1]).toBe('said');
expect(o[2]).toBe('I am sorry.,');
expect(o[3]).toBe('and');
expect(o[4]).toBe('he');
expect(o[5]).toBe('said');
expect(o[6]).toBe("it doesn't matter.");
});
it('should split pure double quoted string', () => {
const i = 'I said "I am sorry.", and he said "it doesn\'t matter."';
const o = splitargs(i);
expect(o).toHaveLength(7);
expect(o[0]).toBe('I');
expect(o[1]).toBe('said');
expect(o[2]).toBe('I am sorry.,');
expect(o[3]).toBe('and');
expect(o[4]).toBe('he');
expect(o[5]).toBe('said');
expect(o[6]).toBe("it doesn't matter.");
});
it('should split single quoted string', () => {
const i = 'I said "I am sorry.", and he said "it doesn\'t matter."';
const o = splitargs(i);
expect(o).toHaveLength(7);
expect(o[0]).toBe('I');
expect(o[1]).toBe('said');
expect(o[2]).toBe('I am sorry.,');
expect(o[3]).toBe('and');
expect(o[4]).toBe('he');
expect(o[5]).toBe('said');
expect(o[6]).toBe("it doesn't matter.");
});
it('should split pure single quoted string', () => {
const i = "I said 'I am sorry.', and he said \"it doesn't matter.\"";
const o = splitargs(i);
expect(o).toHaveLength(7);
expect(o[0]).toBe('I');
expect(o[1]).toBe('said');
expect(o[2]).toBe('I am sorry.,');
expect(o[3]).toBe('and');
expect(o[4]).toBe('he');
expect(o[5]).toBe('said');
expect(o[6]).toBe("it doesn't matter.");
});
it('should split to 4 empty strings', () => {
const i = ',,,';
const o = splitargs(i, ',', true);
expect(o).toHaveLength(4);
});
});

View file

@ -0,0 +1,114 @@
/**
Splits strings into tokens by given separator except treating quoted part as a single token.
#Usage
```javascript
var splitargs = require('splitargs');
var i1 = "I said 'I am sorry.', and he said \"it doesn't matter.\"";
var o1 = splitargs(i1);
console.log(o1);
[ 'I',
'said',
'I am sorry.,',
'and',
'he',
'said',
'it doesn\'t matter.' ]
var i2 = "I said \"I am sorry.\", and he said \"it doesn't matter.\"";
var o2 = splitargs(i2);
console.log(o2);
[ 'I',
'said',
'I am sorry.,',
'and',
'he',
'said',
'it doesn\'t matter.' ]
var i3 = 'I said "I am sorry.", and he said "it doesn\'t matter."';
var o3 = splitargs(i3);
console.log(o3);
[ 'I',
'said',
'I am sorry.,',
'and',
'he',
'said',
'it doesn\'t matter.' ]
var i4 = 'I said \'I am sorry.\', and he said "it doesn\'t matter."';
var o4 = splitargs(i4);
console.log(o4);
[ 'I',
'said',
'I am sorry.,',
'and',
'he',
'said',
'it doesn\'t matter.' ]
```
*/
export function splitargs(
input: string,
sep?: RegExp | string,
keepQuotes = false
) {
const separator = sep || /\s/g;
let singleQuoteOpen = false;
let doubleQuoteOpen = false;
let tokenBuffer = [];
const ret = [];
const arr = input.split('');
for (let i = 0; i < arr.length; ++i) {
const element = arr[i];
const matches = element.match(separator);
// TODO rewrite without continue
/* eslint-disable no-continue */
if (element === "'" && !doubleQuoteOpen) {
if (keepQuotes) {
tokenBuffer.push(element);
}
singleQuoteOpen = !singleQuoteOpen;
continue;
} else if (element === '"' && !singleQuoteOpen) {
if (keepQuotes) {
tokenBuffer.push(element);
}
doubleQuoteOpen = !doubleQuoteOpen;
continue;
}
/* eslint-enable no-continue */
if (!singleQuoteOpen && !doubleQuoteOpen && matches) {
if (tokenBuffer.length > 0) {
ret.push(tokenBuffer.join(''));
tokenBuffer = [];
} else if (sep) {
ret.push(element);
}
} else {
tokenBuffer.push(element);
}
}
if (tokenBuffer.length > 0) {
ret.push(tokenBuffer.join(''));
} else if (sep) {
ret.push('');
}
return ret;
}

View file

@ -0,0 +1,26 @@
import angular from 'angular';
import { ComponentProps } from 'react';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withFormValidation } from '@/react-tools/withFormValidation';
import {
CommandsTab,
CommandsTabValues,
commandsTabValidation,
} from '@/react/docker/containers/CreateView/CommandsTab';
const ngModule = angular.module(
'portainer.docker.react.components.containers',
[]
);
export const containersModule = ngModule.name;
withFormValidation<ComponentProps<typeof CommandsTab>, CommandsTabValues>(
ngModule,
withUIRouter(withReactQuery(CommandsTab)),
'dockerCreateContainerCommandsTab',
['apiVersion'],
commandsTabValidation
);

View file

@ -22,8 +22,10 @@ import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowse
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
import { containersModule } from './containers';
const ngModule = angular
.module('portainer.docker.react.components', [])
.module('portainer.docker.react.components', [containersModule])
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
.component(

View file

@ -8,7 +8,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
export const containersModule = angular
.module('portainer.docker.containers', [])
.module('portainer.docker.react.views.containers', [])
.component(
'containersView',
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), ['endpoint'])

View file

@ -1,76 +0,0 @@
import { useQuery } from 'react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
export interface VersionResponse {
ApiVersion: string;
}
export async function getVersion(environmentId: EnvironmentId) {
try {
const { data } = await axios.get<VersionResponse>(
buildUrl(environmentId, 'version')
);
return data;
} catch (err) {
throw parseAxiosError(err as Error, 'Unable to retrieve version');
}
}
export interface InfoResponse {
Swarm?: {
NodeID: string;
ControlAvailable: boolean;
};
}
export async function getInfo(environmentId: EnvironmentId) {
try {
const { data } = await axios.get<InfoResponse>(
buildUrl(environmentId, 'info')
);
return data;
} catch (err) {
throw parseAxiosError(err as Error, 'Unable to retrieve version');
}
}
export function useInfo<TSelect = InfoResponse>(
environmentId: EnvironmentId,
select?: (info: InfoResponse) => TSelect
) {
return useQuery(
['environment', environmentId, 'docker', 'info'],
() => getInfo(environmentId),
{
select,
}
);
}
export function useVersion<TSelect = VersionResponse>(
environmentId: EnvironmentId,
select?: (info: VersionResponse) => TSelect
) {
return useQuery(
['environment', environmentId, 'docker', 'version'],
() => getVersion(environmentId),
{
select,
}
);
}
function buildUrl(
environmentId: EnvironmentId,
action: string,
subAction = ''
) {
let url = `/endpoints/${environmentId}/docker/${action}`;
if (subAction) {
url += `/${subAction}`;
}
return url;
}

View file

@ -1,5 +1,6 @@
import { Terminal } from 'xterm';
import { baseHref } from '@/portainer/helpers/pathHelper';
import { commandStringToArray } from '@/docker/helpers/containers';
angular.module('portainer.docker').controller('ContainerConsoleController', [
'$scope',
@ -101,7 +102,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
AttachStderr: true,
Tty: true,
User: $scope.formValues.user,
Cmd: ContainerHelper.commandStringToArray(command),
Cmd: commandStringToArray(command),
};
ContainerService.createExec(execConfig)

View file

@ -7,9 +7,10 @@ import { confirmDestructive } from '@@/modals/confirm';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { buildConfirmButton } from '@@/modals/utils';
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
import { ContainerDetailsViewModel } from '../../../models/container';
import { commandsTabUtils } from '@/react/docker/containers/CreateView/CommandsTab';
import { ContainerCapabilities, ContainerCapability } from '@/docker/models/containerCapabilities';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { ContainerDetailsViewModel } from '@/docker/models/container';
import './createcontainer.css';
@ -20,11 +21,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
'$state',
'$timeout',
'$transition$',
'$filter',
'$analytics',
'Container',
'ContainerHelper',
'Image',
'ImageHelper',
'Volume',
'NetworkService',
@ -37,7 +36,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
'RegistryService',
'SystemService',
'SettingsService',
'PluginService',
'HttpRequestHelper',
'endpoint',
function (
@ -47,11 +45,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$state,
$timeout,
$transition$,
$filter,
$analytics,
Container,
ContainerHelper,
Image,
ImageHelper,
Volume,
NetworkService,
@ -64,7 +60,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
RegistryService,
SystemService,
SettingsService,
PluginService,
HttpRequestHelper,
endpoint
) {
@ -80,7 +75,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
selectedGPUs: ['all'],
capabilities: ['compute', 'utility'],
},
Console: 'none',
Volumes: [],
NetworkContainer: null,
Labels: [],
@ -95,15 +89,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
MemoryLimit: 0,
MemoryReservation: 0,
ShmSize: 64,
CmdMode: 'default',
EntrypointMode: 'default',
Env: [],
NodeName: null,
capabilities: [],
Sysctls: [],
LogDriverName: '',
LogDriverOpts: [],
RegistryModel: new PorImageRegistryModel(),
commands: commandsTabUtils.getDefaultViewModel(),
};
$scope.extraNetworks = {};
@ -114,6 +105,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
mode: '',
pullImageValidity: true,
settingUnlimitedResources: false,
containerIsLoaded: false,
};
$scope.onAlwaysPullChange = onAlwaysPullChange;
@ -121,6 +113,13 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.handleAutoRemoveChange = handleAutoRemoveChange;
$scope.handlePrivilegedChange = handlePrivilegedChange;
$scope.handleInitChange = handleInitChange;
$scope.handleCommandsChange = handleCommandsChange;
function handleCommandsChange(commands) {
return $scope.$evalAsync(() => {
$scope.formValues.commands = commands;
});
}
function onAlwaysPullChange(checked) {
return $scope.$evalAsync(() => {
@ -179,10 +178,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config = {
Image: '',
Env: [],
Cmd: '',
Cmd: null,
MacAddress: '',
ExposedPorts: {},
Entrypoint: '',
Entrypoint: null,
WorkingDir: '',
User: '',
HostConfig: {
RestartPolicy: {
Name: 'no',
@ -201,6 +202,10 @@ angular.module('portainer.docker').controller('CreateContainerController', [
CapAdd: [],
CapDrop: [],
Sysctls: {},
LogConfig: {
Type: '',
Config: {},
},
},
NetworkingConfig: {
EndpointsConfig: {},
@ -262,14 +267,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$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) {
@ -284,36 +281,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
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);
}
@ -447,23 +414,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
}
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;
@ -511,40 +461,22 @@ angular.module('portainer.docker').controller('CreateContainerController', [
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareCmd(config);
prepareEntrypoint(config);
config = commandsTabUtils.toRequest(config, $scope.formValues.commands);
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;
@ -641,18 +573,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
}
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) {
@ -765,15 +685,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.fromContainer = fromContainer;
$scope.state.mode = 'duplicate';
$scope.config = ContainerHelper.configFromContainer(fromContainer.Model);
loadFromContainerCmd(d);
loadFromContainerEntrypoint(d);
loadFromContainerLogging(d);
$scope.formValues.commands = commandsTabUtils.toViewModel(d);
loadFromContainerPortBindings(d);
loadFromContainerVolumes(d);
loadFromContainerNetworkConfig(d);
loadFromContainerEnvironmentVariables(d);
loadFromContainerLabels(d);
loadFromContainerConsole(d);
loadFromContainerDevices(d);
loadFromContainerDeviceRequests(d);
loadFromContainerImageConfig(d);
@ -781,22 +700,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
loadFromContainerCapabilities(d);
loadFromContainerSysctls(d);
})
.then(() => {
$scope.state.containerIsLoaded = true;
})
.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;
@ -845,6 +756,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
if ($transition$.params().from) {
loadFromContainerSpec();
} else {
$scope.state.containerIsLoaded = true;
$scope.fromContainer = {};
$scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : [];
}
@ -872,10 +784,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$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) {

File diff suppressed because it is too large Load diff