diff --git a/src/crc-setup.ts b/src/crc-setup.ts index cd8e46e..7509de4 100644 --- a/src/crc-setup.ts +++ b/src/crc-setup.ts @@ -25,12 +25,12 @@ export async function needSetup(): Promise { await execPromise(getCrcCli(), ['setup', '--check-only']); return false; } catch (e) { + console.log(e); return true; } } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function setUpCrc(logger: extensionApi.Logger, askForPreset = false): Promise { +export async function setUpCrc(askForPreset = false): Promise { if (askForPreset) { const preset = await extensionApi.window.showInformationMessage( 'Which preset bundle would you like to use with OpenShift Local. MicroShift, provides a lightweight and optimized environment with a limited set of services. OpenShift, provides a single node OpenShift cluster with a fuller set of services, including a web console (requires more resources).', diff --git a/src/crc-start.spec.ts b/src/crc-start.spec.ts index 3d5521b..80416fa 100644 --- a/src/crc-start.spec.ts +++ b/src/crc-start.spec.ts @@ -24,6 +24,7 @@ import { startCrc } from './crc-start.js'; import * as logProvider from './log-provider.js'; import * as daemon from './daemon-commander.js'; import type { StartInfo } from './types.js'; +import { getLoggerCallback } from './util.js'; vi.mock('@podman-desktop/api', async () => { return { @@ -44,7 +45,7 @@ test('setUpCRC is skipped if already setup, it just perform the daemon start com { updateStatus: vi.fn(), } as unknown as extensionApi.Provider, - {} as extensionApi.Logger, + getLoggerCallback(), { logUsage: vi.fn() } as unknown as extensionApi.TelemetryLogger, ); expect(setUpMock).not.toBeCalled(); @@ -65,7 +66,7 @@ test('set up CRC and then start the daemon', async () => { { updateStatus: vi.fn(), } as unknown as extensionApi.Provider, - {} as extensionApi.Logger, + getLoggerCallback(), { logUsage: vi.fn() } as unknown as extensionApi.TelemetryLogger, ); expect(setUpMock).toBeCalled(); diff --git a/src/crc-start.ts b/src/crc-start.ts index dfdc3f0..18aedd3 100644 --- a/src/crc-start.ts +++ b/src/crc-start.ts @@ -37,7 +37,7 @@ const missingPullSecret = 'Failed to ask for pull secret'; export async function startCrc( provider: extensionApi.Provider, - logger: extensionApi.Logger, + loggerCallback: (data: string) => void, telemetryLogger: extensionApi.TelemetryLogger, ): Promise { telemetryLogger.logUsage('crc.start', { @@ -49,16 +49,15 @@ export async function startCrc( if (isNeedSetup) { try { crcStatus.setSetupRunning(true); - await setUpCrc(logger); + await setUpCrc(); } catch (error) { - logger.error(error); provider.updateStatus('stopped'); - return; + throw error; } finally { crcStatus.setSetupRunning(false); } } - await crcLogProvider.startSendingLogs(logger); + await crcLogProvider.startSendingLogs(loggerCallback); const result = await commander.start(); if (result.Status === 'Running') { provider.updateStatus('started'); @@ -72,11 +71,11 @@ export async function startCrc( // check that crc missing pull secret if (err.message.startsWith(missingPullSecret)) { // ask user to provide pull secret - if (await askAndStorePullSecret(logger)) { + if (await askAndStorePullSecret()) { // if pull secret provided try to start again - return startCrc(provider, logger, telemetryLogger); + return startCrc(provider, loggerCallback, telemetryLogger); } else { - throw new Error('Could not start without pullsecret!'); + throw new Error(`${productName} start error: VM cannot be started without the pullsecret`); } } else if (err.name === 'RequestError' && err.code === 'ECONNRESET') { // look like crc start normally, but we receive empty response from socket, so 'got' generate an error @@ -84,14 +83,14 @@ export async function startCrc( return true; } } - await extensionApi.window.showErrorMessage(`${productName} start error: ${err}`); console.error(err); provider.updateStatus('stopped'); + throw new Error(`${productName} start error: ${err}`); } return false; } -async function askAndStorePullSecret(logger: extensionApi.Logger): Promise { +async function askAndStorePullSecret(): Promise { let pullSecret: string; const authSession: extensionApi.AuthenticationSession | undefined = await extensionApi.authentication.getSession( 'redhat.authentication-provider', @@ -147,7 +146,6 @@ async function askAndStorePullSecret(logger: extensionApi.Logger): Promise { - await createCrcVm(provider, extensionContext, telemetryLogger, defaultLogger); + await initializeCrc(provider, extensionContext, telemetryLogger); + justInitialized = true; }, create: async (params, logger) => { await presetChanged(provider, extensionContext, telemetryLogger); await saveConfig(params); - if (params['crc.factory.start.now']) { + if (params['crc.factory.start.now'] || justInitialized) { + justInitialized = false; + await connectToCrc(); await createCrcVm(provider, extensionContext, telemetryLogger, logger); } }, @@ -248,18 +251,11 @@ async function createCrcVm( logger: extensionApi.Logger, ): Promise { // we already have an instance - if (crcStatus.status.CrcStatus !== 'No Cluster' && !isNeedSetup()) { + if (crcStatus.status.CrcStatus !== 'No Cluster') { return; } - if (isNeedSetup()) { - const initResult = await initializeCrc(provider, extensionContext, telemetryLogger, logger); - if (!initResult) { - throw new Error(`${productName} not initialized.`); - } - } - - const hasStarted = await startCrc(provider, logger, telemetryLogger); + const hasStarted = await startCrc(provider, getLoggerCallback(undefined, logger), telemetryLogger); if (!connectionDisposable && hasStarted) { addCommands(telemetryLogger); await presetChanged(provider, extensionContext, telemetryLogger); @@ -270,29 +266,43 @@ async function initializeCrc( provider: extensionApi.Provider, extensionContext: extensionApi.ExtensionContext, telemetryLogger: extensionApi.TelemetryLogger, - logger: extensionApi.Logger, -): Promise { - const hasSetupFinished = await setUpCrc(logger, true); - if (hasSetupFinished) { - await needSetup(); - await connectToCrc(); - await presetChanged(provider, extensionContext, telemetryLogger); - addCommands(telemetryLogger); - await syncPreferences(provider, extensionContext, telemetryLogger); +): Promise { + const hasToBeSetup = await needSetup(); + if (hasToBeSetup) { + crcStatus.setSetupRunning(true); + let hasSetupFinished = false; + try { + hasSetupFinished = await setUpCrc(true); + } catch (e) { + console.log(String(e)); + } finally { + crcStatus.setSetupRunning(false); + } + if (!hasSetupFinished) { + throw new Error(`Failed at initializing ${productName}`); + } } - return hasSetupFinished; + + addCommands(telemetryLogger); + await syncPreferences(provider, extensionContext, telemetryLogger); + await presetChanged(provider, extensionContext, telemetryLogger); + provider.updateStatus('configured'); } function addCommands(telemetryLogger: extensionApi.TelemetryLogger): void { - registerOpenTerminalCommand(); - registerOpenConsoleCommand(); - registerLogInCommands(); - registerDeleteCommand(); - - commandManager.addCommand(CRC_PUSH_IMAGE_TO_CLUSTER, image => { - telemetryLogger.logUsage('pushImage'); - return pushImageToCrcCluster(image); - }); + try { + registerOpenTerminalCommand(); + registerOpenConsoleCommand(); + registerLogInCommands(); + registerDeleteCommand(); + + commandManager.addCommand(CRC_PUSH_IMAGE_TO_CLUSTER, image => { + telemetryLogger.logUsage('pushImage'); + pushImageToCrcCluster(image).catch((e: unknown) => console.error(String(e))); + }); + } catch (e) { + // do nothing + } } function deleteCommands(): void { @@ -338,14 +348,13 @@ function registerOpenShiftLocalCluster( extensionContext: extensionApi.ExtensionContext, telemetryLogger: extensionApi.TelemetryLogger, ): void { - const status = () => crcStatus.getConnectionStatus(); const apiURL = 'https://api.crc.testing:6443'; const kubernetesProviderConnection: extensionApi.KubernetesProviderConnection = { name, endpoint: { apiURL, }, - status, + status: () => crcStatus.getConnectionStatus(), }; connectionDisposable = provider.registerKubernetesProviderConnection(kubernetesProviderConnection); @@ -353,9 +362,14 @@ function registerOpenShiftLocalCluster( delete: () => { return handleDelete(provider, extensionContext, telemetryLogger); }, - start: async ctx => { + start: async (ctx, logger) => { provider.updateStatus('starting'); - await startCrc(provider, ctx.log, telemetryLogger); + try { + await startCrc(provider, getLoggerCallback(ctx, logger), telemetryLogger); + } catch (e) { + logger.error(e); + throw e; + } }, stop: () => { provider.updateStatus('stopping'); diff --git a/src/install/crc-install.ts b/src/install/crc-install.ts index d747f5b..eb8196a 100644 --- a/src/install/crc-install.ts +++ b/src/install/crc-install.ts @@ -136,7 +136,7 @@ export class CrcInstall { provider.updateVersion(newInstalledCrc.version); let setupResult = false; if (await needSetup()) { - setupResult = await setUpCrc(logger, true); + setupResult = await setUpCrc(true); } installFinishedFn(setupResult, newInstalledCrc); } diff --git a/src/log-provider.ts b/src/log-provider.ts index 6f9056e..5b499c9 100644 --- a/src/log-provider.ts +++ b/src/log-provider.ts @@ -16,7 +16,6 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ -import type { Logger } from '@podman-desktop/api'; import type { DaemonCommander } from './daemon-commander.js'; import { commander } from './daemon-commander.js'; @@ -24,14 +23,14 @@ export class LogProvider { private timeout: NodeJS.Timeout; constructor(private readonly commander: DaemonCommander) {} - async startSendingLogs(logger: Logger): Promise { + async startSendingLogs(loggerCallback: (data: string) => void): Promise { let lastLogLine = 0; this.timeout = setInterval(async () => { try { const logs = await this.commander.logs(); const logsDiff: string[] = logs.Messages.slice(lastLogLine, logs.Messages.length - 1); lastLogLine = logs.Messages.length; - logger.log(logsDiff.join('\n')); + loggerCallback(logsDiff.join('\n')); } catch (e) { console.log('Logs tick: ' + e); } diff --git a/src/preferences.ts b/src/preferences.ts index b477779..e125162 100644 --- a/src/preferences.ts +++ b/src/preferences.ts @@ -19,7 +19,7 @@ import * as extensionApi from '@podman-desktop/api'; import type { Configuration, Preset } from './types.js'; import { commander } from './daemon-commander.js'; -import { isEmpty, productName } from './util.js'; +import { getLoggerCallback, isEmpty, productName } from './util.js'; import { crcStatus } from './crc-status.js'; import { stopCrc } from './crc-stop.js'; import { deleteCrc } from './crc-delete.js'; @@ -337,7 +337,7 @@ async function handleRecreate( } else if (result === 'Delete and Restart') { await stopCrc(telemetryLogger); await deleteCrc(); - await startCrc(provider, defaultLogger, telemetryLogger); + await startCrc(provider, getLoggerCallback(undefined, defaultLogger), telemetryLogger); return true; } else if (result === 'Delete') { await deleteCrc(); diff --git a/src/util.spec.ts b/src/util.spec.ts new file mode 100644 index 0000000..a01153c --- /dev/null +++ b/src/util.spec.ts @@ -0,0 +1,43 @@ +/********************************************************************** + * Copyright (C) 2024 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 + ***********************************************************************/ + +import type * as extensionApi from '@podman-desktop/api'; +import { expect, test, vi } from 'vitest'; +import { getLoggerCallback } from './util'; + +test('check logger passed to getLoggerCallback is actually called with data', async () => { + const logMock = vi.fn(); + const logger = { + log: logMock, + } as unknown as extensionApi.Logger; + const callback = getLoggerCallback(undefined, logger); + callback('data'); + expect(logMock).toBeCalledWith('data'); +}); + +test('check logger passed to getLoggerCallback is actually called with data', async () => { + const logMock = vi.fn(); + const context = { + log: { + log: logMock, + }, + } as unknown as extensionApi.LifecycleContext; + const callback = getLoggerCallback(context); + callback('data2'); + expect(logMock).toBeCalledWith('data2'); +}); diff --git a/src/util.ts b/src/util.ts index 7598f8d..9859e73 100644 --- a/src/util.ts +++ b/src/util.ts @@ -20,6 +20,7 @@ import * as os from 'node:os'; import { spawn } from 'node:child_process'; import * as fs from 'node:fs/promises'; import type { Preset } from './types.js'; +import type { LifecycleContext, Logger } from '@podman-desktop/api'; export const productName = 'OpenShift Local'; export const defaultPreset: Preset = 'openshift'; @@ -113,3 +114,12 @@ export function getPresetLabel(preset: Preset): string { return defaultPresetLabel; } } + +export function getLoggerCallback(context?: LifecycleContext, logger?: Logger): (data: string) => void { + return (data: string): void => { + if (data) { + context?.log?.log(data); + logger?.log(data); + } + }; +}