From e25b846f399cea80a27daef2ac90f93f3654d008 Mon Sep 17 00:00:00 2001 From: Florent Benoit Date: Wed, 14 Sep 2022 17:57:43 +0200 Subject: [PATCH] feat: Adds support of "podman play kube" through REST API fixes https://github.com/containers/podman-desktop/issues/203 Change-Id: Id68146bb7f9d871765de7e130eaacde6de1d0834 Signed-off-by: Florent Benoit --- packages/main/src/mainWindow.ts | 5 +- packages/main/src/plugin/api/provider-info.ts | 1 + .../main/src/plugin/container-registry.ts | 21 ++- .../src/plugin/dockerode/libpod-dockerode.ts | 41 +++++ packages/main/src/plugin/index.ts | 12 ++ packages/main/src/plugin/provider-registry.ts | 1 + packages/preload/src/index.ts | 52 ++++-- packages/renderer/src/App.svelte | 5 + .../renderer/src/lib/ContainerList.svelte | 42 +++-- .../container/ContainerPlayKubefile.svelte | 163 ++++++++++++++++++ .../src/lib/container/KubePlayIcon.svelte | 16 ++ 11 files changed, 327 insertions(+), 32 deletions(-) create mode 100644 packages/renderer/src/lib/container/ContainerPlayKubefile.svelte create mode 100644 packages/renderer/src/lib/container/KubePlayIcon.svelte diff --git a/packages/main/src/mainWindow.ts b/packages/main/src/mainWindow.ts index 5d00416746d..1d00e04ae88 100644 --- a/packages/main/src/mainWindow.ts +++ b/packages/main/src/mainWindow.ts @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ -import type { BrowserWindowConstructorOptions } from 'electron'; +import type { BrowserWindowConstructorOptions, FileFilter } from 'electron'; import { BrowserWindow, ipcMain, app, dialog, screen } from 'electron'; import contextMenu from 'electron-context-menu'; import { join } from 'path'; @@ -80,9 +80,10 @@ async function createWindow() { }); // select a file using native widget - ipcMain.on('dialog:openFile', async (_, param: { dialogId: string; message: string }) => { + ipcMain.on('dialog:openFile', async (_, param: { dialogId: string; message: string; filter: FileFilter }) => { const response = await dialog.showOpenDialog(browserWindow, { properties: ['openFile'], + filters: [param.filter], message: param.message, }); // send the response back diff --git a/packages/main/src/plugin/api/provider-info.ts b/packages/main/src/plugin/api/provider-info.ts index 91f161e178b..9f9cd22fa8b 100644 --- a/packages/main/src/plugin/api/provider-info.ts +++ b/packages/main/src/plugin/api/provider-info.ts @@ -35,6 +35,7 @@ export interface ProviderContainerConnectionInfo { socketPath: string; }; lifecycleMethods?: LifecycleMethod[]; + type: 'docker' | 'podman'; } export interface ProviderKubernetesConnectionInfo { diff --git a/packages/main/src/plugin/container-registry.ts b/packages/main/src/plugin/container-registry.ts index a5f72ebff21..c2a9051f0ee 100644 --- a/packages/main/src/plugin/container-registry.ts +++ b/packages/main/src/plugin/container-registry.ts @@ -34,7 +34,7 @@ const tar: { pack: (dir: string) => NodeJS.ReadableStream } = require('tar-fs'); import { EventEmitter } from 'node:events'; import type { ContainerInspectInfo } from './api/container-inspect-info'; import type { HistoryInfo } from './api/history-info'; -import type { LibPod, PodInfo as LibpodPodInfo } from './dockerode/libpod-dockerode'; +import type { LibPod, PlayKubeInfo, PodInfo as LibpodPodInfo } from './dockerode/libpod-dockerode'; import { LibpodDockerode } from './dockerode/libpod-dockerode'; import type { ContainerStatsInfo } from './api/container-stats-info'; import type { VolumeInfo, VolumeInspectInfo, VolumeListInfo } from './api/volume-info'; @@ -788,6 +788,21 @@ export class ContainerProviderRegistry { return this.statsConsumerId; } + async playKube( + kubernetesYamlFilePath: string, + selectedProvider: ProviderContainerConnectionInfo, + ): Promise { + this.telemetryService.track('playKube'); + // grab all connections + const matchingContainerProvider = Array.from(this.internalProviders.values()).find( + containerProvider => containerProvider.connection.endpoint.socketPath === selectedProvider.endpoint.socketPath, + ); + if (!matchingContainerProvider || !matchingContainerProvider.libpodApi) { + throw new Error('No provider with a running engine'); + } + return matchingContainerProvider.libpodApi.playKube(kubernetesYamlFilePath); + } + async buildImage( containerBuildContextDirectory: string, relativeContainerfilePath: string, @@ -798,7 +813,9 @@ export class ContainerProviderRegistry { this.telemetryService.track('buildImage'); // grab all connections const matchingContainerProvider = Array.from(this.internalProviders.values()).find( - containerProvider => containerProvider.connection.endpoint.socketPath === selectedProvider.endpoint.socketPath, + containerProvider => + containerProvider.connection.endpoint.socketPath === selectedProvider.endpoint.socketPath && + selectedProvider.status === 'started', ); if (!matchingContainerProvider || !matchingContainerProvider.api) { throw new Error('No provider with a running engine'); diff --git a/packages/main/src/plugin/dockerode/libpod-dockerode.ts b/packages/main/src/plugin/dockerode/libpod-dockerode.ts index 5d56905200b..159c8fb931c 100644 --- a/packages/main/src/plugin/dockerode/libpod-dockerode.ts +++ b/packages/main/src/plugin/dockerode/libpod-dockerode.ts @@ -37,6 +37,22 @@ export interface PodInfo { Status: string; } +export interface PlayKubePodInfo { + ContainerErrors: string[]; + Containers: string[]; + Id: string; + InitContainers: string[]; + Logs: string[]; +} + +export interface PlayKubeInfo { + Pods: PlayKubePodInfo[]; + RmReport: { Err: string; Id: string }[]; + Secrets: { CreateReport: { ID: string } }[]; + StopReport: { Err: string; Id: string }[]; + Volumes: { Name: string }[]; +} + // API of libpod that we want to expose on our side export interface LibPod { listPods(): Promise; @@ -45,6 +61,7 @@ export interface LibPod { removePod(podId: string): Promise; restartPod(podId: string): Promise; generateKube(names: string[]): Promise; + playKube(yamlContentFilePath: string): Promise; } // tweak Dockerode by adding the support of libpod API @@ -209,5 +226,29 @@ export class LibpodDockerode { }); }); }; + + // add playKube + prototypeOfDockerode.playKube = function (yamlContentFilePath: string) { + const optsf = { + path: '/v4.2.0/libpod/play/kube', + method: 'POST', + file: yamlContentFilePath, + statusCodes: { + 200: true, + 204: true, + 500: 'server error', + }, + options: {}, + }; + + return new Promise((resolve, reject) => { + this.modem.dial(optsf, (err: unknown, data: unknown) => { + if (err) { + return reject(err); + } + resolve(data); + }); + }); + }; } } diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index 9d515788281..36333ea59dc 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -63,6 +63,7 @@ import type { HistoryInfo } from './api/history-info'; import type { PodInfo } from './api/pod-info'; import type { VolumeInspectInfo, VolumeListInfo } from './api/volume-info'; import type { ContainerStatsInfo } from './api/container-stats-info'; +import type { PlayKubeInfo } from './dockerode/libpod-dockerode'; type LogType = 'log' | 'warn' | 'trace' | 'debug' | 'error'; export class PluginSystem { @@ -281,6 +282,17 @@ export class PluginSystem { return containerProviderRegistry.generatePodmanKube(engine, names); }, ); + + this.ipcHandle( + 'container-provider-registry:playKube', + async ( + _listener, + yamlFilePath: string, + selectedProvider: ProviderContainerConnectionInfo, + ): Promise => { + return containerProviderRegistry.playKube(yamlFilePath, selectedProvider); + }, + ); this.ipcHandle( 'container-provider-registry:startContainer', async (_listener, engine: string, containerId: string): Promise => { diff --git a/packages/main/src/plugin/provider-registry.ts b/packages/main/src/plugin/provider-registry.ts index a8592ec3fdd..03a6f70a38d 100644 --- a/packages/main/src/plugin/provider-registry.ts +++ b/packages/main/src/plugin/provider-registry.ts @@ -377,6 +377,7 @@ export class ProviderRegistry { const containerProviderConnection: ProviderContainerConnectionInfo = { name: connection.name, status: connection.status(), + type: connection.type, endpoint: { socketPath: connection.endpoint.socketPath, }, diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index a7b9a7093f7..0781d615407 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -43,6 +43,7 @@ import type { IConfigurationPropertyRecordedSchema } from '../../main/src/plugin import type { PullEvent } from '../../main/src/plugin/api/pull-event'; import { Deferred } from './util/deferred'; import type { StatusBarEntryDescriptor } from '../../main/src/plugin/statusbar/statusbar-registry'; +import type { PlayKubeInfo } from '../../main/src/plugin/dockerode/libpod-dockerode'; export type DialogResultCallback = (openDialogReturnValue: Electron.OpenDialogReturnValue) => void; @@ -142,6 +143,17 @@ function initExposure(): void { contextBridge.exposeInMainWorld('generatePodmanKube', async (engine: string, names: string[]): Promise => { return ipcInvoke('container-provider-registry:generatePodmanKube', engine, names); }); + + contextBridge.exposeInMainWorld( + 'playKube', + async ( + relativeContainerfilePath: string, + selectedProvider: ProviderContainerConnectionInfo, + ): Promise => { + return ipcInvoke('container-provider-registry:playKube', relativeContainerfilePath, selectedProvider); + }, + ); + contextBridge.exposeInMainWorld('stopPod', async (engine: string, podId: string): Promise => { return ipcInvoke('container-provider-registry:stopPod', engine, podId); }); @@ -650,28 +662,32 @@ function initExposure(): void { const dialogResponses = new Map(); - contextBridge.exposeInMainWorld('openFileDialog', async (message: string) => { - // generate id - const dialogId = idDialog; - idDialog++; + contextBridge.exposeInMainWorld( + 'openFileDialog', + async (message: string, filter?: { extensions: string[]; name: string }) => { + // generate id + const dialogId = idDialog; + idDialog++; - // create defer object - const defer = new Deferred(); + // create defer object + const defer = new Deferred(); - // store the dialogID - dialogResponses.set(`${dialogId}`, (result: Electron.OpenDialogReturnValue) => { - defer.resolve(result); - }); + // store the dialogID + dialogResponses.set(`${dialogId}`, (result: Electron.OpenDialogReturnValue) => { + defer.resolve(result); + }); - // ask to open file dialog - ipcRenderer.send('dialog:openFile', { - dialogId: `${dialogId}`, - message, - }); + // ask to open file dialog + ipcRenderer.send('dialog:openFile', { + dialogId: `${dialogId}`, + message, + filter, + }); - // wait for response - return defer.promise; - }); + // wait for response + return defer.promise; + }, + ); contextBridge.exposeInMainWorld('openFolderDialog', async (message: string) => { // generate id diff --git a/packages/renderer/src/App.svelte b/packages/renderer/src/App.svelte index bdde78a9b9f..05fac3ab344 100644 --- a/packages/renderer/src/App.svelte +++ b/packages/renderer/src/App.svelte @@ -33,6 +33,7 @@ import ImageDetails from './lib/image/ImageDetails.svelte'; import PodsList from './lib/pod/PodsList.svelte'; import VolumesList from './lib/volume/VolumesList.svelte'; import VolumeDetails from './lib/volume/VolumeDetails.svelte'; +import ContainerPlayKubefile from './lib/container/ContainerPlayKubefile.svelte'; let containersCountValue; router.mode.hash(); @@ -378,6 +379,10 @@ window.events?.receive('display-help', () => { + + + + diff --git a/packages/renderer/src/lib/ContainerList.svelte b/packages/renderer/src/lib/ContainerList.svelte index 5273b9fb575..00eae9afca2 100644 --- a/packages/renderer/src/lib/ContainerList.svelte +++ b/packages/renderer/src/lib/ContainerList.svelte @@ -18,6 +18,7 @@ import NavPage from './ui/NavPage.svelte'; import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import Fa from 'svelte-fa/src/fa.svelte'; import ContainerGroupIcon from './container/ContainerGroupIcon.svelte'; +import KubePlayIcon from './container/KubePlayIcon.svelte'; const containerUtils = new ContainerUtils(); let openChoiceModal = false; @@ -39,6 +40,13 @@ $: providerConnections = $providerInfos .flat() .filter(providerContainerConnection => providerContainerConnection.status === 'started'); +$: providerPodmanConnections = $providerInfos + .map(provider => provider.containerConnections) + .flat() + // keep only podman providers as it is not supported by docker + .filter(providerContainerConnection => providerContainerConnection.type === 'podman') + .filter(providerContainerConnection => providerContainerConnection.status === 'started'); + // number of selected items in the list $: selectedItemsNumber = containerGroups.reduce( @@ -212,6 +220,10 @@ function toggleCreateContainer(): void { openChoiceModal = !openChoiceModal; } +function runContainerYaml(): void { + router.goto('/containers/play'); +} + function fromDockerfile(): void { openChoiceModal = false; router.goto('/images/build'); @@ -237,16 +249,26 @@ function toggleAllContainerGroups(value: boolean) { bind:searchTerm title="containers" subtitle="Hover over a container to view action buttons; click to open up full details."> - +
+ + {#if providerPodmanConnections.length > 0} + + {/if} +
{#if selectedItemsNumber > 0} + {/if} + {#if runStarted} +
+ Please wait during the Play Kube and do not change screen. This process may take a few minutes to complete... +
+ {/if} + {#if runError} +
{runError}
+ {/if} + {#if runFinished} + + {/if} +
+ {#if playKubeResultRaw} +
+ +
+ {/if} +{/if} diff --git a/packages/renderer/src/lib/container/KubePlayIcon.svelte b/packages/renderer/src/lib/container/KubePlayIcon.svelte new file mode 100644 index 00000000000..97865ecc3fa --- /dev/null +++ b/packages/renderer/src/lib/container/KubePlayIcon.svelte @@ -0,0 +1,16 @@ + + + + + + + + + + + +