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

feat(gpu) EE-3191 Add GPU support for containers (#7146)

This commit is contained in:
congs 2022-07-18 11:02:14 +12:00 committed by GitHub
parent f0456cbf5f
commit 4997e9c7be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 758 additions and 10 deletions

View file

@ -15,12 +15,19 @@ export function useInputGroupContext() {
interface Props {
size?: Size;
className?: string;
}
export function InputGroup({ children, size }: PropsWithChildren<Props>) {
export function InputGroup({
children,
size,
className,
}: PropsWithChildren<Props>) {
return (
<Context.Provider value>
<div className={clsx('input-group', sizeClass(size))}>{children}</div>
<div className={clsx('input-group', sizeClass(size), className)}>
{children}
</div>
</Context.Provider>
);
}

View file

@ -8,6 +8,7 @@ interface InputGroupSubComponents {
Addon: typeof InputGroupAddon;
ButtonWrapper: typeof InputGroupButtonWrapper;
Input: typeof Input;
className: string | undefined;
}
const InputGroup: typeof MainComponent & InputGroupSubComponents =

View file

@ -31,6 +31,7 @@
background-color: var(--bg-multiselect-color);
border: 1px solid var(--border-multiselect);
padding: 5px;
z-index: 10;
}
.root :global .selector__option {

View file

@ -0,0 +1,12 @@
import { Column } from 'react-table';
import type { DockerContainer } from '@/react/docker/containers/types';
export const gpus: Column<DockerContainer> = {
Header: 'GPUs',
accessor: 'Gpus',
id: 'gpus',
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -10,6 +10,7 @@ import { ports } from './ports';
import { quickActions } from './quick-actions';
import { stack } from './stack';
import { state } from './state';
import { gpus } from './gpus';
export function useColumns() {
return useMemo(
@ -22,6 +23,7 @@ export function useColumns() {
created,
ip,
host,
gpus,
ports,
ownership,
],

View file

@ -50,4 +50,5 @@ export type DockerContainer = {
Ports: Port[];
StackName?: string;
Image: string;
Gpus: string;
};

View file

@ -2,6 +2,7 @@ import { Field, Form, Formik } from 'formik';
import { useReducer } from 'react';
import { useCreateRemoteEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation';
import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware';
import { notifySuccess } from '@/portainer/services/notifications';
import {
Environment,
@ -33,6 +34,7 @@ export function APIForm({ onCreate }: Props) {
groupId: 1,
tagIds: [],
},
gpus: [],
};
const mutation = useCreateRemoteEnvironmentMutation(
@ -67,7 +69,9 @@ export function APIForm({ onCreate }: Props) {
<TLSFieldset />
<MoreSettingsSection />
<MoreSettingsSection>
<Hardware />
</MoreSettingsSection>
<div className="form-group">
<div className="col-sm-12">
@ -96,6 +100,7 @@ export function APIForm({ onCreate }: Props) {
options: {
tls,
meta: values.meta,
gpus: values.gpus,
},
},
{

View file

@ -1,5 +1,7 @@
import { boolean, object, SchemaOf, string } from 'yup';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { nameValidation } from '../../shared/NameField';
@ -14,5 +16,6 @@ export function validation(): SchemaOf<FormValues> {
skipVerify: boolean(),
meta: metadataValidation(),
...certsValidation(),
gpus: gpusListValidation(),
});
}

View file

@ -1,3 +1,4 @@
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
export interface FormValues {
@ -9,4 +10,5 @@ export interface FormValues {
certFile?: File;
keyFile?: File;
meta: EnvironmentMetadata;
gpus?: Gpu[];
}

View file

@ -2,6 +2,7 @@ import { Field, Form, Formik, useFormikContext } from 'formik';
import { useReducer } from 'react';
import { useCreateLocalDockerEnvironmentMutation } from '@/portainer/environments/queries/useCreateEnvironmentMutation';
import { Hardware } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/Hardware';
import { notifySuccess } from '@/portainer/services/notifications';
import { Environment } from '@/portainer/environments/types';
@ -27,6 +28,7 @@ export function SocketForm({ onCreate }: Props) {
socketPath: '',
overridePath: false,
meta: { groupId: 1, tagIds: [] },
gpus: [],
};
const mutation = useCreateLocalDockerEnvironmentMutation();
@ -45,7 +47,10 @@ export function SocketForm({ onCreate }: Props) {
<OverrideSocketFieldset />
<MoreSettingsSection />
<MoreSettingsSection>
<Hardware />
</MoreSettingsSection>
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
@ -68,6 +73,7 @@ export function SocketForm({ onCreate }: Props) {
{
name: values.name,
socketPath: values.overridePath ? values.socketPath : '',
gpus: values.gpus,
},
{
onSuccess(environment) {

View file

@ -1,5 +1,7 @@
import { boolean, object, SchemaOf, string } from 'yup';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { nameValidation } from '../../shared/NameField';
@ -19,5 +21,6 @@ export function validation(): SchemaOf<FormValues> {
)
: schema
),
gpus: gpusListValidation(),
});
}

View file

@ -1,3 +1,4 @@
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
export interface FormValues {
@ -5,4 +6,5 @@ export interface FormValues {
socketPath: string;
overridePath: boolean;
meta: EnvironmentMetadata;
gpus: Gpu[];
}

View file

@ -10,12 +10,14 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
import { NameField } from '../NameField';
import { MoreSettingsSection } from '../MoreSettingsSection';
import { Hardware } from '../Hardware/Hardware';
import { EnvironmentUrlField } from './EnvironmentUrlField';
import { validation } from './AgentForm.validation';
interface Props {
onCreate(environment: Environment): void;
showGpus?: boolean;
}
const initialValues: CreateAgentEnvironmentValues = {
@ -25,9 +27,10 @@ const initialValues: CreateAgentEnvironmentValues = {
groupId: 1,
tagIds: [],
},
gpus: [],
};
export function AgentForm({ onCreate }: Props) {
export function AgentForm({ onCreate, showGpus = false }: Props) {
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
const mutation = useCreateAgentEnvironmentMutation();
@ -45,7 +48,7 @@ export function AgentForm({ onCreate }: Props) {
<NameField />
<EnvironmentUrlField />
<MoreSettingsSection />
<MoreSettingsSection>{showGpus && <Hardware />}</MoreSettingsSection>
<div className="form-group">
<div className="col-sm-12">

View file

@ -1,5 +1,6 @@
import { object, SchemaOf, string } from 'yup';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { CreateAgentEnvironmentValues } from '@/portainer/environments/environment.service/create';
import { metadataValidation } from '../MetadataFieldset/validation';
@ -10,5 +11,6 @@ export function validation(): SchemaOf<CreateAgentEnvironmentValues> {
name: nameValidation(),
environmentUrl: string().required('This field is required.'),
meta: metadataValidation(),
gpus: gpusListValidation(),
});
}

View file

@ -9,6 +9,7 @@ import { FormSection } from '@@/form-components/FormSection';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { MoreSettingsSection } from '../../MoreSettingsSection';
import { Hardware } from '../../Hardware/Hardware';
import { EdgeAgentFieldset } from './EdgeAgentFieldset';
import { validationSchema } from './EdgeAgentForm.validation';
@ -17,11 +18,12 @@ import { FormValues } from './types';
interface Props {
onCreate(environment: Environment): void;
readonly: boolean;
showGpus?: boolean;
}
const initialValues = buildInitialValues();
export function EdgeAgentForm({ onCreate, readonly }: Props) {
export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) {
const createMutation = useCreateEdgeAgentEnvironmentMutation();
return (
@ -43,6 +45,7 @@ export function EdgeAgentForm({ onCreate, readonly }: Props) {
value={values.pollFrequency}
/>
</FormSection>
{showGpus && <Hardware />}
</MoreSettingsSection>
{!readonly && (
@ -82,6 +85,7 @@ export function buildInitialValues(): FormValues {
groupId: 1,
tagIds: [],
},
gpus: [],
};
function defaultPortainerUrl() {

View file

@ -1,5 +1,7 @@
import { number, object, SchemaOf } from 'yup';
import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { metadataValidation } from '../../MetadataFieldset/validation';
import { nameValidation } from '../../NameField';
@ -12,5 +14,6 @@ export function validationSchema(): SchemaOf<FormValues> {
portainerUrl: urlValidation(),
pollFrequency: number().required(),
meta: metadataValidation(),
gpus: gpusListValidation(),
});
}

View file

@ -1,3 +1,4 @@
import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { EnvironmentMetadata } from '@/portainer/environments/environment.service/create';
export interface FormValues {
@ -6,4 +7,5 @@ export interface FormValues {
portainerUrl: string;
pollFrequency: number;
meta: EnvironmentMetadata;
gpus: Gpu[];
}

View file

@ -14,12 +14,14 @@ interface Props {
onCreate: (environment: Environment) => void;
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
isNomadTokenVisible?: boolean;
showGpus?: boolean;
}
export function EdgeAgentTab({
onCreate,
commands,
isNomadTokenVisible,
showGpus = false,
}: Props) {
const [edgeInfo, setEdgeInfo] = useState<EdgeInfo>();
@ -31,6 +33,7 @@ export function EdgeAgentTab({
onCreate={handleCreate}
readonly={!!edgeInfo}
key={formKey}
showGpus={showGpus}
/>
{edgeInfo && (

View file

@ -0,0 +1,68 @@
import { array, object, string } from 'yup';
import { r2a } from '@/react-tools/react2angular';
import { InputList } from '@@/form-components/InputList';
import { ItemProps } from '@@/form-components/InputList/InputList';
import { InputGroup } from '@@/form-components/InputGroup';
export interface Gpu {
value: string;
name: string;
}
interface Props {
value: Gpu[];
onChange(value: Gpu[]): void;
}
function Item({ item, onChange }: ItemProps<Gpu>) {
return (
<div className="flex gap-2 flex-grow">
<InputGroup size="small" className="flex-grow">
<InputGroup.Addon>GPU Name</InputGroup.Addon>
<InputGroup.Input
placeholder="my-gpu"
value={item.name}
onChange={(e) => {
onChange({ ...item, name: e.target.value });
}}
/>
</InputGroup>
<InputGroup size="small" className="flex-grow">
<InputGroup.Addon>Index or UUID</InputGroup.Addon>
<InputGroup.Input
placeholder="0 or GPU-6e2c7185-c3d3-ae22-da43-bc5267b89061"
value={item.value}
onChange={(e) => {
onChange({ ...item, value: e.target.value });
}}
/>
</InputGroup>
</div>
);
}
export function GpusList({ value, onChange }: Props) {
return (
<InputList<Gpu>
label="GPU"
value={value}
onChange={onChange}
itemBuilder={() => ({ value: '', name: '' })}
addLabel="add"
item={Item}
/>
);
}
export function gpusListValidation() {
const gpuShape = object().shape({
name: string().required(),
value: string().required(),
});
return array().of(gpuShape).default([]);
}
export const GpusListAngular = r2a(GpusList, ['value', 'onChange']);

View file

@ -0,0 +1,22 @@
import { useField } from 'formik';
import {
Gpu,
GpusList,
} from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList';
import { FormSection } from '@@/form-components/FormSection';
export function Hardware() {
const [field, , helpers] = useField('gpus');
function onChange(value: Gpu[]) {
helpers.setValue(value);
}
return (
<FormSection title="Hardware acceleration">
<GpusList value={field.value} onChange={onChange} />
</FormSection>
);
}