mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-05-24 10:18:53 +00:00
feat: add preflight check api (#363)
* feat: add preflight check api Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * fix review comments Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * Use 'ipcInvoke' instead of 'ipcRenderer.invoke' Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com>
This commit is contained in:
parent
d675ac6dd7
commit
b54cda7f41
7 changed files with 184 additions and 8 deletions
11
packages/extension-api/src/extension-api.d.ts
vendored
11
packages/extension-api/src/extension-api.d.ts
vendored
|
|
@ -202,7 +202,18 @@ declare module '@tmpwip/extension-api' {
|
|||
create(params: { [key: string]: any }): Promise<void>;
|
||||
}
|
||||
|
||||
export interface CheckResult {
|
||||
successful: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface InstallCheck {
|
||||
title: string;
|
||||
execute(): Promise<CheckResult>;
|
||||
}
|
||||
|
||||
export interface ProviderInstallation {
|
||||
preflightChecks?(): InstallCheck[];
|
||||
// ask to install the provider
|
||||
install(logger: Logger): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,3 +73,19 @@ export interface ProviderInfo {
|
|||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PreflightChecksCallback {
|
||||
startCheck: (status: CheckStatus) => void;
|
||||
endCheck: (status: CheckStatus) => void;
|
||||
}
|
||||
|
||||
export interface CheckStatus {
|
||||
name: string;
|
||||
successful?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface PreflightCheckEvent {
|
||||
type: 'start' | 'stop';
|
||||
status: CheckStatus;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,12 @@ import { ConfigurationRegistry } from './configuration-registry';
|
|||
import { TerminalInit } from './terminal-init';
|
||||
import { ImageRegistry } from './image-registry';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { ProviderContainerConnectionInfo, ProviderInfo } from './api/provider-info';
|
||||
import type {
|
||||
PreflightCheckEvent,
|
||||
PreflightChecksCallback,
|
||||
ProviderContainerConnectionInfo,
|
||||
ProviderInfo,
|
||||
} from './api/provider-info';
|
||||
import type { WebContents } from 'electron';
|
||||
import { ipcMain, BrowserWindow } from 'electron';
|
||||
import type { ContainerCreateOptions, ContainerInfo } from './api/container-info';
|
||||
|
|
@ -331,6 +336,27 @@ export class PluginSystem {
|
|||
return providerRegistry.installProvider(providerInternalId);
|
||||
});
|
||||
|
||||
this.ipcHandle(
|
||||
'provider-registry:runInstallPreflightChecks',
|
||||
async (_, providerInternalId: string, callbackId: number): Promise<boolean> => {
|
||||
const callback: PreflightChecksCallback = {
|
||||
startCheck: status => {
|
||||
this.getWebContentsSender().send('provider-registry:installPreflightChecksUpdate', callbackId, {
|
||||
type: 'start',
|
||||
status,
|
||||
} as PreflightCheckEvent);
|
||||
},
|
||||
endCheck: status => {
|
||||
this.getWebContentsSender().send('provider-registry:installPreflightChecksUpdate', callbackId, {
|
||||
type: 'stop',
|
||||
status,
|
||||
} as PreflightCheckEvent);
|
||||
},
|
||||
};
|
||||
return providerRegistry.runPreflightChecks(providerInternalId, callback);
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcHandle('provider-registry:startProvider', async (_, providerInternalId: string): Promise<void> => {
|
||||
return providerRegistry.startProvider(providerInternalId);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import type {
|
|||
ProviderInfo,
|
||||
ProviderKubernetesConnectionInfo,
|
||||
LifecycleMethod,
|
||||
PreflightChecksCallback,
|
||||
} from './api/provider-info';
|
||||
import type { ContainerProviderRegistry } from './container-registry';
|
||||
import { LifecycleContextImpl, LoggerImpl } from './lifecycle-context';
|
||||
|
|
@ -244,6 +245,44 @@ export class ProviderRegistry {
|
|||
return provider.detectionChecks;
|
||||
}
|
||||
|
||||
async runPreflightChecks(providerInternalId: string, statusCallback: PreflightChecksCallback): Promise<boolean> {
|
||||
const provider = this.getMatchingProvider(providerInternalId);
|
||||
const providerInstall = this.providerInstallations.get(providerInternalId);
|
||||
if (!providerInstall) {
|
||||
throw new Error(`No matching installation for provider ${provider.internalId}`);
|
||||
}
|
||||
|
||||
if (!providerInstall.preflightChecks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checks = providerInstall.preflightChecks();
|
||||
for (const check of checks) {
|
||||
statusCallback.startCheck({ name: check.title });
|
||||
try {
|
||||
const checkResult = await check.execute();
|
||||
|
||||
statusCallback.endCheck({
|
||||
name: check.title,
|
||||
successful: checkResult.successful,
|
||||
description: checkResult.description,
|
||||
});
|
||||
|
||||
if (!checkResult.successful) {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
statusCallback.endCheck({
|
||||
name: check.title,
|
||||
successful: false,
|
||||
description: err instanceof Error ? err.message : typeof err === 'object' ? err?.toString() : 'unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async installProvider(providerInternalId: string): Promise<void> {
|
||||
const provider = this.getMatchingProvider(providerInternalId);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,12 @@ import type { ContributionInfo } from '../../main/src/plugin/api/contribution-in
|
|||
import type { ImageInfo } from '../../main/src/plugin/api/image-info';
|
||||
import type { ImageInspectInfo } from '../../main/src/plugin/api/image-inspect-info';
|
||||
import type { ExtensionInfo } from '../../main/src/plugin/api/extension-info';
|
||||
import type { ProviderContainerConnectionInfo, ProviderInfo } from '../../main/src/plugin/api/provider-info';
|
||||
import type {
|
||||
PreflightCheckEvent,
|
||||
PreflightChecksCallback,
|
||||
ProviderContainerConnectionInfo,
|
||||
ProviderInfo,
|
||||
} from '../../main/src/plugin/api/provider-info';
|
||||
import type { IConfigurationPropertyRecordedSchema } from '../../main/src/plugin/configuration-registry';
|
||||
import type { PullEvent } from '../../main/src/plugin/api/pull-event';
|
||||
import { Deferred } from './util/deferred';
|
||||
|
|
@ -260,6 +265,31 @@ function initExposure(): void {
|
|||
},
|
||||
);
|
||||
|
||||
const preflightChecksCallbacks = new Map<number, PreflightChecksCallback>();
|
||||
let checkCallbackId = 0;
|
||||
contextBridge.exposeInMainWorld(
|
||||
'runInstallPreflightChecks',
|
||||
async (providerId: string, callBack: PreflightChecksCallback) => {
|
||||
checkCallbackId++;
|
||||
preflightChecksCallbacks.set(checkCallbackId, callBack);
|
||||
return await ipcInvoke('provider-registry:runInstallPreflightChecks', providerId, checkCallbackId);
|
||||
},
|
||||
);
|
||||
|
||||
ipcRenderer.on('provider-registry:installPreflightChecksUpdate', (_, callbackId, data: PreflightCheckEvent) => {
|
||||
const callback = preflightChecksCallbacks.get(callbackId);
|
||||
if (callback) {
|
||||
switch (data.type) {
|
||||
case 'start':
|
||||
callback.startCheck(data.status);
|
||||
break;
|
||||
case 'stop':
|
||||
callback.endCheck(data.status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld(
|
||||
'updateProvider',
|
||||
async (providerId: string): Promise<containerDesktopAPI.ProviderDetectionCheck[]> => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,45 @@
|
|||
<script lang="ts">
|
||||
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
|
||||
import type { CheckStatus, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
|
||||
|
||||
export let provider: ProviderInfo;
|
||||
|
||||
export let onPreflightChecks: (status: CheckStatus[]) => void;
|
||||
|
||||
let installInProgress = false;
|
||||
|
||||
let checksStatus: CheckStatus[] = [];
|
||||
|
||||
let preflightChecksFailed = false;
|
||||
|
||||
async function performInstallation(provider: ProviderInfo) {
|
||||
installInProgress = true;
|
||||
|
||||
await window.installProvider(provider.internalId);
|
||||
checksStatus = [];
|
||||
let checkSuccess = false;
|
||||
let currentCheck: CheckStatus;
|
||||
try {
|
||||
checkSuccess = await window.runInstallPreflightChecks(provider.internalId, {
|
||||
endCheck: status => {
|
||||
if (currentCheck) {
|
||||
currentCheck = status;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
checksStatus.push(currentCheck);
|
||||
onPreflightChecks(checksStatus);
|
||||
},
|
||||
startCheck: status => {
|
||||
currentCheck = status;
|
||||
onPreflightChecks([...checksStatus, currentCheck]);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
if (checkSuccess) {
|
||||
await window.installProvider(provider.internalId);
|
||||
} else {
|
||||
preflightChecksFailed = true;
|
||||
}
|
||||
|
||||
installInProgress = false;
|
||||
}
|
||||
|
|
@ -15,7 +47,7 @@ async function performInstallation(provider: ProviderInfo) {
|
|||
|
||||
{#if provider.installationSupport}
|
||||
<button
|
||||
disabled="{installInProgress}"
|
||||
disabled="{installInProgress || preflightChecksFailed}"
|
||||
on:click="{() => performInstallation(provider)}"
|
||||
class="pf-c-button pf-m-primary"
|
||||
type="button">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { ProviderDetectionCheck } from '@tmpwip/extension-api';
|
||||
|
||||
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
|
||||
import type { CheckStatus, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
|
||||
import ProviderDetectionChecksButton from './ProviderDetectionChecksButton.svelte';
|
||||
import ProviderInstallationButton from './ProviderInstallationButton.svelte';
|
||||
import ProviderLinks from './ProviderLinks.svelte';
|
||||
|
|
@ -10,6 +10,7 @@ import ProviderLogo from './ProviderLogo.svelte';
|
|||
export let provider: ProviderInfo;
|
||||
|
||||
let detectionChecks: ProviderDetectionCheck[] = [];
|
||||
let preflightChecks: CheckStatus[] = [];
|
||||
</script>
|
||||
|
||||
<div class="p-2 flex flex-col bg-zinc-700 rounded-lg">
|
||||
|
|
@ -24,7 +25,7 @@ let detectionChecks: ProviderDetectionCheck[] = [];
|
|||
</div>
|
||||
<div class="mt-10 mb-1 w-full flex justify-around">
|
||||
<ProviderDetectionChecksButton onDetectionChecks="{checks => (detectionChecks = checks)}" provider="{provider}" />
|
||||
<ProviderInstallationButton provider="{provider}" />
|
||||
<ProviderInstallationButton onPreflightChecks="{checks => (preflightChecks = checks)}" provider="{provider}" />
|
||||
</div>
|
||||
{#if detectionChecks.length > 0}
|
||||
<div class="flex flex-col w-full mt-5 px-5 pt-5 pb-0 rounded-lg bg-zinc-600">
|
||||
|
|
@ -38,5 +39,26 @@ let detectionChecks: ProviderDetectionCheck[] = [];
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if preflightChecks.length > 0}
|
||||
<div class="flex flex-col w-full mt-5 px-5 pt-5 pb-0 rounded-lg bg-zinc-600">
|
||||
{#each preflightChecks as preCheck}
|
||||
<div class="flex flex-col">
|
||||
<p class="mb-4 items-center list-inside">
|
||||
{#if preCheck.successful === undefined}
|
||||
<svg class="pf-c-spinner pf-m-sm" role="progressbar" viewBox="0 0 100 100" aria-label="Checkin...">
|
||||
<circle class="pf-c-spinner__path" cx="50" cy="50" r="45" fill="none"></circle>
|
||||
</svg>
|
||||
{:else}
|
||||
{preCheck.successful ? '✅' : '❌'}
|
||||
{/if}
|
||||
{preCheck.name}
|
||||
</p>
|
||||
{#if preCheck.description}
|
||||
Details: <p class="text-gray-300 w-full break-all">{preCheck.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<ProviderLinks provider="{provider}" />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue