Skip to content

Commit

Permalink
chore: specify devices when creating a container (podman-desktop#9125)
Browse files Browse the repository at this point in the history
* feat: specify devices when creating a container
Signed-off-by: Philippe Martin <[email protected]>

* feat: rename Device to DeviceMapping
Signed-off-by: Philippe Martin <[email protected]>

* feat: design R/W/M checkboxes
Signed-off-by: Philippe Martin <[email protected]>
  • Loading branch information
feloy authored Sep 30, 2024
1 parent 447cbe5 commit 4c09e30
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 1 deletion.
7 changes: 7 additions & 0 deletions packages/api/src/container-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ interface MountSettings {

type MountConfig = MountSettings[];

export interface DeviceMapping {
CgroupPermissions: string;
PathInContainer: string;
PathOnHost: string;
}

export interface HostConfig {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
PortBindings?: any;
Expand All @@ -105,6 +111,7 @@ export interface HostConfig {
ExtraHosts?: string[];
NetworkMode?: string;
Mounts?: MountConfig;
Devices?: DeviceMapping[];
}

export interface HealthConfig {
Expand Down
60 changes: 60 additions & 0 deletions packages/renderer/src/lib/image/RunImage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,4 +496,64 @@ describe('RunImage', () => {
const button = screen.getByRole('button', { name: 'Start Container' });
expect((button as HTMLButtonElement).disabled).toBeTruthy();
});

test('Expect able to play with devices', async () => {
await createRunImage('', []);

const link1 = screen.getByRole('link', { name: 'Advanced' });
await fireEvent.click(link1);

// set the input field for the path
const deviceHostInput = screen.getByRole('textbox', { name: 'device.host.0' });

// set the value
await userEvent.type(deviceHostInput, '/dev/tty0');

// add a new element
const addDeviceButton = screen.getByRole('button', { name: 'Add device after index 0' });
await fireEvent.click(addDeviceButton);

// again (should be 3 now)
await fireEvent.click(addDeviceButton);

// now set the input for fields 2 and 3
const deviceHostInput2 = screen.getByRole('textbox', { name: 'device.host.1' });
await userEvent.type(deviceHostInput2, '/dev/tty1');

const deviceHostInput3 = screen.getByRole('textbox', { name: 'device.host.2' });
await userEvent.type(deviceHostInput3, '/dev/tty2');
const deviceContainerInput3 = screen.getByRole('textbox', { name: 'device.container.2' });
await userEvent.type(deviceContainerInput3, '/dev/ttyOnContainer2');

// delete the entry 2
const deleteDeviceButton = screen.getByRole('button', { name: 'Delete device at index 1' });
await fireEvent.click(deleteDeviceButton);

// now click on start

const button = screen.getByRole('button', { name: 'Start Container' });

await fireEvent.click(button);

// should have item 1 and item 3 as we deleted item 2
expect(window.createAndStartContainer).toHaveBeenCalledWith(
'engineid',
expect.objectContaining({
HostConfig: expect.objectContaining({
Devices: [
{
CgroupPermissions: 'rwm',
PathOnHost: '/dev/tty0',
PathInContainer: '/dev/tty0',
},
{
CgroupPermissions: 'rwm',
PathOnHost: '/dev/tty2',
PathInContainer: '/dev/ttyOnContainer2',
},
],
}),
}),
);
});
});
64 changes: 63 additions & 1 deletion packages/renderer/src/lib/image/RunImage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { onMount } from 'svelte';
import { router } from 'tinro';
import { array2String } from '/@/lib/string/string.js';
import type { ContainerCreateOptions, HostConfig } from '/@api/container-info';
import type { ContainerCreateOptions, DeviceMapping, HostConfig } from '/@api/container-info';
import type { ImageInspectInfo } from '/@api/image-inspect-info';
import type { NetworkInspectInfo } from '/@api/network-info';
Expand Down Expand Up @@ -50,6 +50,9 @@ let environmentVariables: { key: string; value: string }[] = [{ key: '', value:
let environmentFiles: string[] = [''];
let volumeMounts: { source: string; target: string }[] = [{ source: '', target: '' }];
let hostContainerPortMappings: { hostPort: PortInfo; containerPort: string }[] = [];
let devices: { host: string; container: string; read: boolean; write: boolean; mknod: boolean }[] = [
{ host: '', container: '', read: false, write: false, mknod: false },
];
let invalidName = false;
let invalidPorts = false;
Expand Down Expand Up @@ -332,6 +335,19 @@ async function startContainer() {
const ReadonlyRootfs = readOnly;
const Tty = useTty;
const OpenStdin = useInteractive;
let Devices: DeviceMapping[] | undefined = devices
.filter(d => d.host)
.map(d => ({
PathOnHost: d.host,
PathInContainer: d.container !== '' ? d.container : d.host,
CgroupPermissions:
!d.read && !d.write && !d.mknod ? 'rwm' : `${d.read ? 'r' : ''}${d.write ? 'w' : ''}${d.mknod ? 'm' : ''}`,
}));
if (Devices.length === 0) {
Devices = undefined;
}
const HostConfig: HostConfig = {
Binds,
AutoRemove: autoRemove,
Expand All @@ -343,6 +359,7 @@ async function startContainer() {
CapAdd,
CapDrop,
NetworkMode,
Devices,
};
const Dns = dnsServers.filter(dns => dns);
Expand Down Expand Up @@ -540,6 +557,14 @@ function deleteExtraHost(index: number) {
extraHosts = extraHosts.filter((_, i) => i !== index);
}
function addDevice() {
devices = [...devices, { host: '', container: '', read: false, write: false, mknod: false }];
}
function deleteDevice(index: number) {
devices = devices.filter((_, i) => i !== index);
}
// called when user change the container's name
function checkContainerName(event: any) {
const containerValue = event.target.value;
Expand Down Expand Up @@ -850,6 +875,43 @@ const envDialogOptions: OpenDialogOptions = {
class="w-24 p-2"
disabled={restartPolicyName !== 'on-failure'} />
</div>

<!-- devices -->
<label
for="modalDevices"
class="pt-4 block mb-2 text-sm font-medium text-[var(--pd-content-card-header-text)]">Devices:</label>
<!-- Display the list of existing devices -->
{#each devices as device, index}
<div class="flex flex-row justify-center items-center w-full py-1">
<Input
bind:value={device.host}
placeholder="Host Device"
class="w-full"
aria-label="device.host.{index}" />
<Input
bind:value={device.container}
placeholder="Container Device (leave blank for same as host device)"
class="ml-2"
aria-label="device.container.{index}" />
<div class="flex flew-row space-x-4 ml-2 text-sm">
<Checkbox bind:checked={device.read} title="Read">Read</Checkbox>
<Checkbox bind:checked={device.write} title="Write">Write</Checkbox>
<Checkbox bind:checked={device.mknod} title="Mknod">Mknod</Checkbox>
</div>
<Button
type="link"
hidden={index === devices.length - 1}
aria-label="Delete device at index {index}"
on:click={() => deleteDevice(index)}
icon={faMinusCircle} />
<Button
type="link"
hidden={index < devices.length - 1}
aria-label="Add device after index {index}"
on:click={addDevice}
icon={faPlusCircle} />
</div>
{/each}
</div>
</Route>

Expand Down

0 comments on commit 4c09e30

Please sign in to comment.