From 9fed2ad5358294a50e68e214203f24202e9ed2ca Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Thu, 20 Jul 2023 14:15:43 -0400 Subject: [PATCH] feat: add "deploy to kubernetes" label for compose group ### What does this PR do? * Adds button to Deploy to Kubernetes for a Compose group * Utilizes DeployPodToKube same code, but added a "compose" type * Adds a new function for listSimpleContainersByLabel for an easier way to find containers by label that you only need the name / id of. ### Screenshot/screencast of this PR ### What issues does this PR fix or reference? Closes https://github.com/containers/podman-desktop/issues/3104 ### How to test this PR? 1. Deploy a Docker Compose example 2. Click on the "hamburger" button to bring up the Deploy To Kubernetes button. 3. Deploy to Kubernetes and make sure it's deployed correctly. Signed-off-by: Charlie Drage --- .../src/plugin/container-registry.spec.ts | 26 +++++++++++ .../main/src/plugin/container-registry.ts | 12 +++++ packages/main/src/plugin/index.ts | 8 ++++ packages/preload/src/index.ts | 13 +++++- packages/renderer/src/App.svelte | 10 ++++- .../src/lib/compose/ComposeActions.svelte | 13 +++++- .../src/lib/pod/DeployPodToKube.spec.ts | 44 +++++++++++++++++++ .../src/lib/pod/DeployPodToKube.svelte | 14 ++++-- 8 files changed, 134 insertions(+), 6 deletions(-) diff --git a/packages/main/src/plugin/container-registry.spec.ts b/packages/main/src/plugin/container-registry.spec.ts index 7d19b5bd42a15..5628615d6fb62 100644 --- a/packages/main/src/plugin/container-registry.spec.ts +++ b/packages/main/src/plugin/container-registry.spec.ts @@ -278,3 +278,29 @@ test('deleteContainersByLabel should succeed successfully if project name is pro // Expect deleteContainer tohave been called 3 times expect(deleteContainer).toHaveBeenCalledTimes(3); }); + +test('test listSimpleContainersByLabel with compose label', async () => { + const engine = { + // Fake that we have 3 containers of the same project + listSimpleContainers: vi + .fn() + .mockResolvedValue([ + fakeContainerWithComposeProject, + fakeContainerWithComposeProject, + fakeContainerWithComposeProject, + fakeContainer, + ]), + listPods: vi.fn().mockResolvedValue([]), + }; + vi.spyOn(containerRegistry, 'getMatchingEngine').mockReturnValue(engine as unknown as Dockerode); + vi.spyOn(containerRegistry, 'listSimpleContainers').mockReturnValue(engine.listSimpleContainers()); + + // List all containers with the label 'com.docker.compose.project' and value 'project1' + const result = await containerRegistry.listSimpleContainersByLabel( + 'com.docker.compose.project', + 'project1', + ); + + // We expect ONLY to return 3 since the last container does not have the correct label. + expect(result).toHaveLength(3); +}); \ No newline at end of file diff --git a/packages/main/src/plugin/container-registry.ts b/packages/main/src/plugin/container-registry.ts index c6947fc2f0021..67149ec29dfff 100644 --- a/packages/main/src/plugin/container-registry.ts +++ b/packages/main/src/plugin/container-registry.ts @@ -265,6 +265,18 @@ export class ContainerProviderRegistry { return flatttenedContainers; } + // listSimpleContainers by matching label and key + async listSimpleContainersByLabel(label: string, key: string): Promise { + // Get all the containers using listSimpleContainers + const containers = await this.listSimpleContainers(); + + // Find all the containers that match the label + key + return containers.filter(container => { + const labels = container.Labels; + return labels && labels[label] === key; + }); + } + async listContainers(): Promise { let telemetryOptions = {}; const containers = await Promise.all( diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index 68c86e7233ba7..8de5637e15662 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -684,6 +684,14 @@ export class PluginSystem { this.ipcHandle('container-provider-registry:listContainers', async (): Promise => { return containerProviderRegistry.listContainers(); }); + + this.ipcHandle( + 'container-provider-registry:listSimpleContainersByLabel', + async (_listener, label: string, key: string): Promise => { + return containerProviderRegistry.listSimpleContainersByLabel(label, key); + }, + ); + this.ipcHandle('container-provider-registry:listSimpleContainers', async (): Promise => { return containerProviderRegistry.listSimpleContainers(); }); diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index d899856a8e974..80638219609d9 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -23,7 +23,11 @@ import type * as containerDesktopAPI from '@podman-desktop/api'; import { contextBridge, ipcRenderer } from 'electron'; import EventEmitter from 'events'; -import type { ContainerCreateOptions, ContainerInfo } from '../../main/src/plugin/api/container-info'; +import type { + ContainerCreateOptions, + ContainerInfo, + SimpleContainerInfo, +} from '../../main/src/plugin/api/container-info'; import type { ContributionInfo } from '../../main/src/plugin/api/contribution-info'; import type { ImageInfo } from '../../main/src/plugin/api/image-info'; import type { VolumeInspectInfo, VolumeListInfo } from '../../main/src/plugin/api/volume-info'; @@ -177,6 +181,13 @@ function initExposure(): void { return ipcInvoke('container-provider-registry:listContainers'); }); + contextBridge.exposeInMainWorld( + 'listSimpleContainersByLabel', + async (label: string, key: string): Promise => { + return ipcInvoke('container-provider-registry:listSimpleContainersByLabel', label, key); + }, + ); + contextBridge.exposeInMainWorld('listImages', async (): Promise => { return ipcInvoke('container-provider-registry:listImages'); }); diff --git a/packages/renderer/src/App.svelte b/packages/renderer/src/App.svelte index 9cfac05127c94..4eb6faa3d92e4 100644 --- a/packages/renderer/src/App.svelte +++ b/packages/renderer/src/App.svelte @@ -126,7 +126,15 @@ window.events?.receive('display-troubleshooting', () => { + engineId="{decodeURI(meta.params.engineId)}" + type="container" /> + + + + -import { faTrash, faPlay, faStop, faArrowsRotate } from '@fortawesome/free-solid-svg-icons'; +import { faTrash, faPlay, faStop, faArrowsRotate, faRocket } from '@fortawesome/free-solid-svg-icons'; import type { ComposeInfoUI } from './ComposeInfoUI'; import ListItemButtonIcon from '../ui/ListItemButtonIcon.svelte'; import DropdownMenu from '../ui/DropdownMenu.svelte'; import FlatMenu from '../ui/FlatMenu.svelte'; +import { router } from 'tinro'; export let compose: ComposeInfoUI; export let dropdownMenu = false; @@ -57,6 +58,10 @@ async function restartCompose(composeInfoUI: ComposeInfoUI) { } } +function deployToKubernetes(): void { + router.goto(`/compose/deploy-to-kube/${compose.name}/${compose.engineId}`); +} + // If dropdownMenu = true, we'll change style to the imported dropdownMenu style // otherwise, leave blank. let actionsStyle; @@ -93,6 +98,12 @@ if (dropdownMenu) { + { Object.defineProperty(window, 'generatePodmanKube', { @@ -69,6 +70,9 @@ beforeEach(() => { Object.defineProperty(window, 'telemetryTrack', { value: telemetryTrackMock, }); + Object.defineProperty(window, 'listSimpleContainersByLabel', { + value: listSimpleContainersByLabelMock, + }); // podYaml with volumes const podYaml = { @@ -97,6 +101,19 @@ beforeEach(() => { }, }; generatePodmanKubeMock.mockResolvedValue(jsYaml.dump(podYaml)); + + // Mock listSimpleContainersByLabel with a SimpleContainerInfo[] array of 1 container + const simpleContainerInfo = { + id: '1234', + name: 'hello', + image: 'hello-world', + status: 'running', + labels: { + 'com.docker.compose.project': 'hello', + }, + }; + listSimpleContainersByLabelMock.mockResolvedValue([simpleContainerInfo]); + }); afterEach(() => { @@ -198,6 +215,33 @@ test('When deploying a pod, volumes should not be added (they are deleted by pod ); }); +// Test deploying a compose group of containers +test('Test deploying a group of compose containers with type compose still functions the same as normal deploy', async () => { + await waitRender({ type: 'compose' }); + const createButton = screen.getByRole('button', { name: 'Deploy' }); + expect(createButton).toBeInTheDocument(); + expect(createButton).toBeEnabled(); + + // Press the deploy button + await fireEvent.click(createButton); + + // Expect to return the correct create pod yaml + await waitFor(() => + expect(kubernetesCreatePodMock).toBeCalledWith('default', { + metadata: { name: 'hello' }, + spec: { + containers: [ + { + name: 'hello', + image: 'hello-world', + imagePullPolicy: 'IfNotPresent', + }, + ], + }, + }), + ); +}); + // After modifying the pod name, metadata.apps.label should also have been changed test('When modifying the pod name, metadata.apps.label should also have been changed', async () => { await waitRender({}); diff --git a/packages/renderer/src/lib/pod/DeployPodToKube.svelte b/packages/renderer/src/lib/pod/DeployPodToKube.svelte index fa8097e69e88f..896135ddfb3f6 100644 --- a/packages/renderer/src/lib/pod/DeployPodToKube.svelte +++ b/packages/renderer/src/lib/pod/DeployPodToKube.svelte @@ -11,6 +11,7 @@ import { ensureRestrictedSecurityContext } from '/@/lib/pod/pod-utils'; export let resourceId: string; export let engineId: string; +export let type: string; let kubeDetails: string; @@ -38,9 +39,16 @@ let containerPortArray: string[] = []; let createdRoutes: V1Route[] = []; onMount(async () => { - // grab kube result from the pod - const kubeResult = await window.generatePodmanKube(engineId, [resourceId]); - kubeDetails = kubeResult; + // If type = compose + // we will grab the containers by using the label com.docker.compose.project that matches the resourceId + // we can then pass the array of containers to generatePodmanKube rather than the singular pod id + if (type === 'compose') { + const containers = await window.listSimpleContainersByLabel('com.docker.compose.project', resourceId); + const containerIds = containers.map(container => container.Id); + kubeDetails = await window.generatePodmanKube(engineId, containerIds); + } else { + kubeDetails = await window.generatePodmanKube(engineId, [resourceId]); + } // parse yaml bodyPod = jsYaml.load(kubeDetails) as any;