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:
Yevhen Vydolob 2022-08-01 15:17:19 +03:00 committed by GitHub
parent d675ac6dd7
commit b54cda7f41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 184 additions and 8 deletions

View file

@ -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>;
}

View file

@ -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;
}

View file

@ -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);
});

View file

@ -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);

View file

@ -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[]> => {

View file

@ -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">

View file

@ -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>