Skip to content

Commit

Permalink
feat: allow entrypoint and cmd when starting container (containers#3031)
Browse files Browse the repository at this point in the history
* feat: allow entrypoint and cmd when starting container

Fixes containers#958

Signed-off-by: Jeff MAURY <jmaury@redhat.com>
  • Loading branch information
jeffmaury authored and mairin committed Jul 7, 2023
1 parent af91d81 commit 6bb51e8
Show file tree
Hide file tree
Showing 5 changed files with 381 additions and 4 deletions.
2 changes: 2 additions & 0 deletions packages/main/src/plugin/api/container-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,6 @@ export interface ContainerCreateOptions {
HostConfig?: HostConfig;
Image?: string;
Tty?: boolean;
Cmd?: string[];
Entrypoint?: string | string[];
}
257 changes: 257 additions & 0 deletions packages/renderer/src/lib/image/RunImage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

/* eslint-disable @typescript-eslint/no-explicit-any */

import '@testing-library/jest-dom';
import { test, vi, type Mock } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { runImageInfo } from '../../stores/run-image-store';
import RunImage from '/@/lib/image/RunImage.svelte';
import type { ImageInspectInfo } from '../../../../main/src/plugin/api/image-inspect-info';

// fake the window.events object
beforeAll(() => {
(window.events as unknown) = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
receive: (_channel: string, func: any) => {
func();
},
};
(window as any).getImageInspect = vi.fn();
(window as any).listNetworks = vi.fn().mockResolvedValue([]);
(window as any).listContainers = vi.fn().mockResolvedValue([]);
(window as any).createAndStartContainer = vi.fn();
});

async function waitRender() {
const result = render(RunImage);

//wait until dataReady is true
while (result.component.$$.ctx[29] !== true) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return result;
}

async function createRunImage(entrypoint: string | string[], cmd: string[]) {
runImageInfo.set({
age: '',
base64RepoTag: '',
createdAt: 0,
engineId: '',
engineName: '',
humanSize: '',
id: '',
inUse: false,
name: '',
selected: false,
shortId: '',
tag: '',
});
const imageInfo: ImageInspectInfo = {
Architecture: '',
Author: '',
Comment: '',
Config: {
ArgsEscaped: false,
AttachStderr: false,
AttachStdin: false,
AttachStdout: false,
Cmd: cmd,
Domainname: '',
Entrypoint: entrypoint,
Env: [],
ExposedPorts: {},
Hostname: '',
Image: '',
Labels: {},
OnBuild: [],
OpenStdin: false,
StdinOnce: false,
Tty: false,
User: '',
Volumes: {},
WorkingDir: '',
},
Container: '',
ContainerConfig: {
ArgsEscaped: false,
AttachStderr: false,
AttachStdin: false,
AttachStdout: false,
Cmd: [],
Domainname: '',
Env: [],
ExposedPorts: {},
Hostname: '',
Image: '',
Labels: {},
OpenStdin: false,
StdinOnce: false,
Tty: false,
User: '',
Volumes: {},
WorkingDir: '',
},
Created: '',
DockerVersion: '',
GraphDriver: { Data: { DeviceId: '', DeviceName: '', DeviceSize: '' }, Name: '' },
Id: '',
Os: '',
Parent: '',
RepoDigests: [],
RepoTags: [],
RootFS: {
Type: '',
},
Size: 0,
VirtualSize: 0,
engineId: 'engineid',
engineName: 'engineName',
};
(window.getImageInspect as Mock).mockResolvedValue(imageInfo);
await waitRender();
}

describe('RunImage', () => {
test('Expect that entrypoint is displayed', async () => {
await createRunImage('entrypoint', []);

const link = screen.getByRole('link', { name: 'Basic' });

await fireEvent.click(link);

const entryPoint = screen.getByRole('textbox', { name: 'Entrypoint:' });
expect(entryPoint).toBeInTheDocument();
expect((entryPoint as HTMLInputElement).value).toBe('entrypoint');
});

test('Expect that single element array entrypoint is displayed', async () => {
await createRunImage(['entrypoint'], []);

const link = screen.getByRole('link', { name: 'Basic' });

await fireEvent.click(link);

const entryPoint = screen.getByRole('textbox', { name: 'Entrypoint:' });
expect(entryPoint).toBeInTheDocument();
expect((entryPoint as HTMLInputElement).value).toBe('entrypoint');
});

test('Expect that two elements array entrypoint is displayed', async () => {
await createRunImage(['entrypoint1', 'entrypoint2'], []);

const link = screen.getByRole('link', { name: 'Basic' });

await fireEvent.click(link);

const entryPoint = screen.getByRole('textbox', { name: 'Entrypoint:' });
expect(entryPoint).toBeInTheDocument();
expect((entryPoint as HTMLInputElement).value).toBe('entrypoint1 entrypoint2');
});

test('Expect that single element array command is displayed', async () => {
await createRunImage([], ['command']);

const link = screen.getByRole('link', { name: 'Basic' });

await fireEvent.click(link);

const command = screen.getByRole('textbox', { name: 'Command:' });
expect(command).toBeInTheDocument();
expect((command as HTMLInputElement).value).toBe('command');
});

test('Expect that two elements array command is displayed', async () => {
await createRunImage([], ['command1', 'command2']);

const link = screen.getByRole('link', { name: 'Basic' });

await fireEvent.click(link);

const entryPoint = screen.getByRole('textbox', { name: 'Command:' });
expect(entryPoint).toBeInTheDocument();
expect((entryPoint as HTMLInputElement).value).toBe('command1 command2');
});

test('Expect that entrypoint is sent to API', async () => {
await createRunImage('entrypoint', []);

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

await fireEvent.click(button);

expect(window.createAndStartContainer).toHaveBeenCalledWith(
'engineid',
expect.objectContaining({ Entrypoint: ['entrypoint'] }),
);
});

test('Expect that single array entrypoint is sent to API', async () => {
await createRunImage(['entrypoint'], []);

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

await fireEvent.click(button);

expect(window.createAndStartContainer).toHaveBeenCalledWith(
'engineid',
expect.objectContaining({ Entrypoint: ['entrypoint'] }),
);
});

test('Expect that two elements array entrypoint is sent to API', async () => {
await createRunImage(['entrypoint1', 'entrypoint2'], []);

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

await fireEvent.click(button);

expect(window.createAndStartContainer).toHaveBeenCalledWith(
'engineid',
expect.objectContaining({ Entrypoint: ['entrypoint1', 'entrypoint2'] }),
);
});

test('Expect that single array command is sent to API', async () => {
await createRunImage([], ['command']);

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

await fireEvent.click(button);

expect(window.createAndStartContainer).toHaveBeenCalledWith(
'engineid',
expect.objectContaining({ Cmd: ['command'] }),
);
});

test('Expect that two elements array command is sent to API', async () => {
await createRunImage([], ['command1', 'command2']);

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

await fireEvent.click(button);

expect(window.createAndStartContainer).toHaveBeenCalledWith(
'engineid',
expect.objectContaining({ Cmd: ['command1', 'command2'] }),
);
});
});
45 changes: 41 additions & 4 deletions packages/renderer/src/lib/image/RunImage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import type { ContainerInfoUI } from '../container/ContainerInfoUI';
import { ContainerUtils } from '../container/container-utils';
import { containersInfos } from '../../stores/containers';
import ErrorMessage from '../ui/ErrorMessage.svelte';
import { splitSpacesHandlingDoubleQuotes } from '../string/string';
let image: ImageInfoUI;
let imageInspectInfo: ImageInspectInfo;
let containerName = '';
let containerNameError = '';
let command = '';
let entrypoint = '';
let invalidFields = false;
let containerPortMapping: string[];
Expand Down Expand Up @@ -96,6 +101,16 @@ onMount(async () => {
imageInspectInfo = await window.getImageInspect(image.engineId, image.id);
exposedPorts = Array.from(Object.keys(imageInspectInfo?.Config?.ExposedPorts || {}));
command = imageInspectInfo.Config.Cmd.join(' ');
if (imageInspectInfo.Config.Entrypoint) {
if (typeof imageInspectInfo.Config.Entrypoint === 'string') {
entrypoint = imageInspectInfo.Config.Entrypoint;
} else {
entrypoint = imageInspectInfo.Config.Entrypoint.join(' ');
}
}
// auto-assign ports from available free port
containerPortMapping = new Array<string>(exposedPorts.length);
await Promise.all(
Expand Down Expand Up @@ -273,6 +288,12 @@ async function startContainer() {
ExposedPorts,
Tty,
};
if (command.trim().length > 0) {
options.Cmd = splitSpacesHandlingDoubleQuotes(command);
}
if (entrypoint.trim().length > 0) {
options.Entrypoint = splitSpacesHandlingDoubleQuotes(entrypoint);
}
if (runUser) {
options.User = runUser;
Expand Down Expand Up @@ -400,7 +421,7 @@ function checkContainerName(event: any) {
<ul class="pf-c-tabs__list">
<li class="pf-c-tabs__item" class:pf-m-current="{meta.url === '/images/run/basic'}">
<a
href="/images/run/basic"
href="{meta.match}/basic"
class="pf-c-tabs__link"
aria-controls="open-tabs-example-tabs-list-details-panel"
id="open-tabs-example-tabs-list-details-link">
Expand All @@ -409,7 +430,7 @@ function checkContainerName(event: any) {
</li>
<li class="pf-c-tabs__item" class:pf-m-current="{meta.url === '/images/run/advanced'}">
<a
href="/images/run/advanced"
href="{meta.match}/advanced"
class="pf-c-tabs__link"
aria-controls="open-tabs-example-tabs-list-details-panel"
id="open-tabs-example-tabs-list-details-link">
Expand All @@ -418,7 +439,7 @@ function checkContainerName(event: any) {
</li>
<li class="pf-c-tabs__item" class:pf-m-current="{meta.url === '/images/run/networking'}">
<a
href="/images/run/networking"
href="{meta.match}/networking"
class="pf-c-tabs__link"
aria-controls="open-tabs-example-tabs-list-yaml-panel"
id="open-tabs-example-tabs-list-yaml-link">
Expand All @@ -427,7 +448,7 @@ function checkContainerName(event: any) {
</li>
<li class="pf-c-tabs__item" class:pf-m-current="{meta.url === '/images/run/security'}">
<a
href="/images/run/security"
href="{meta.pattern}/security"
class="pf-c-tabs__link"
aria-controls="open-tabs-example-tabs-list-yaml-panel"
id="open-tabs-example-tabs-list-yaml-link">
Expand All @@ -454,6 +475,22 @@ function checkContainerName(event: any) {
? 'border-red-500'
: 'border-charcoal-800'}" />
<ErrorMessage class="h-1 text-sm" error="{containerNameError}" />
<label for="modalEntrypoint" class="block mb-2 text-sm font-medium text-gray-400 dark:text-gray-400"
>Entrypoint:</label>
<input
type="text"
bind:value="{entrypoint}"
name="modalEntrypoint"
id="modalEntrypoint"
class="w-full p-2 outline-none text-sm bg-charcoal-800 rounded-sm text-gray-700 placeholder-gray-700 border border-charcoal-800" />
<label for="modalCommand" class="block mb-2 text-sm font-medium text-gray-400 dark:text-gray-400"
>Command:</label>
<input
type="text"
bind:value="{command}"
name="modalCommand"
id="modalCommand"
class="w-full p-2 outline-none text-sm bg-charcoal-800 rounded-sm text-gray-700 placeholder-gray-700 border border-charcoal-800" />
<label for="volumes" class="pt-4 block mb-2 text-sm font-medium text-gray-400 dark:text-gray-400"
>Volumes:</label>
<!-- Display the list of volumes -->
Expand Down

0 comments on commit 6bb51e8

Please sign in to comment.