diff --git a/scripts/run.mjs b/scripts/run.mjs index 523c91c..20fefd3 100644 --- a/scripts/run.mjs +++ b/scripts/run.mjs @@ -64,30 +64,10 @@ async function checkAndCloneDesktopRepo() { } } -function removeCrcExt() { - const crcExtPath = path.join(desktopPath, 'extensions', 'crc'); - if(fs.existsSync(crcExtPath)){ - console.log('Deleting old crc extension'); - fs.rmSync(crcExtPath, {recursive: true}); - } -} - -async function patchPDBuild() { - console.log('Removing crc build script from package.json...'); - const filePath = path.resolve(desktopPath, 'package.json'); - const content = fs.readFileSync(filePath); - const packageObj = JSON.parse(content.toString('utf8')); - packageObj.scripts['build:extensions:crc'] = ''; - fs.writeFileSync(filePath, JSON.stringify(packageObj, undefined, ' ')); -} - async function prepareDev() { await checkAndCloneDesktopRepo(); - removeCrcExt(); - await patchPDBuild(); await exec('yarn',undefined, {cwd: desktopPath }); - await buildPD(); await exec('yarn',[], {cwd: path.join(__dirname, '..')}); } @@ -112,14 +92,7 @@ async function build() { async function run() { await buildCrc(); - - if(os.platform() === 'darwin') { - await exec('open', ['-W', '-a', path.resolve(desktopPath, 'dist', 'mac', 'Podman Desktop.app')]); - } else if(os.platform() === 'win32') { - await exec(path.resolve(desktopPath, 'dist', 'win-unpacked', '"Podman Desktop.exe"')); - } else { - throw new Error('Cannot launch Podman Desktop on ' + os.platform()); - } + await exec('yarn', ['watch'], {cwd: desktopPath}); } const firstArg = process.argv[2]; diff --git a/src/crc-cli.ts b/src/crc-cli.ts index e0560db..c1f6baa 100644 --- a/src/crc-cli.ts +++ b/src/crc-cli.ts @@ -168,3 +168,12 @@ export function daemonStop() { daemonProcess.kill(); } } + +export async function needSetup(): Promise { + try { + await execPromise(getCrcCli(), ['setup', '--check-only']); + return false; + } catch (e) { + return true; + } +} diff --git a/src/crc-setup.ts b/src/crc-setup.ts new file mode 100644 index 0000000..7937929 --- /dev/null +++ b/src/crc-setup.ts @@ -0,0 +1,108 @@ +/********************************************************************** + * Copyright (C) 2023 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 * as extensionApi from '@podman-desktop/api'; +import { execPromise, getCrcCli } from './crc-cli'; +import type { Preset } from './daemon-commander'; +import { productName } from './util'; + +interface PresetQuickPickItem extends extensionApi.QuickPickItem { + data: Preset; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function setUpCrc(logger: extensionApi.Logger, askForPreset = false): Promise { + if (askForPreset) { + const preset = await extensionApi.window.showQuickPick(createPresetItems(), { + canPickMany: false, + title: `Select ${productName} Preset`, + placeHolder: `Select ${productName} Preset`, + }); + if (!preset) { + extensionApi.window.showNotification({ + title: productName, + body: 'Default preset will be used.', + }); + } else { + await execPromise(getCrcCli(), ['config', 'set', 'preset', preset.data]); + } + } + + const setupBar = extensionApi.window.createStatusBarItem('RIGHT', 2000); + try { + setupBar.text = `Configuring ${productName}...`; + setupBar.show(); + await execPromise(getCrcCli(), ['setup'], { + logger: { + error: (data: string) => { + if (!data.startsWith('level=')) { + const downloadMsg = 'Downloading bundle: ' + data.substring(data.lastIndexOf(']') + 1, data.length).trim(); + setupBar.text = downloadMsg; + setupBar.tooltip = + 'Downloading bundle: ' + + data.substring(0, data.indexOf('[')).trim() + + ' ' + + data.substring(data.lastIndexOf(']') + 1, data.length).trim(); + } else { + const msg = data.substring(data.indexOf('msg="') + 5, data.length - 1); + setupBar.text = msg; + } + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + warn: (data: string) => { + //ignore + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + log: (data: string) => { + //ignore + }, + }, + env: undefined, + }); + + setupBar.text = 'All done.'; + } catch (err) { + console.error(err); + extensionApi.window.showErrorMessage(`${productName} configuration failed:\n${err}`); + return false; + } finally { + setupBar.hide(); + setupBar.dispose(); + } + + return true; +} + +function createPresetItems(): PresetQuickPickItem[] { + return [ + { + data: 'openshift', + label: 'openshift', + description: + 'Run a full OpenShift cluster environment as a single node, providing a registry and access to Operator Hub', + detail: + 'Run a full OpenShift cluster environment as a single node, providing a registry and access to Operator Hub', + }, + { + data: 'microshift', + label: 'microshift', + description: 'MicroShift is an optimized OpenShift Kubernetes for small form factor and edge computing.', + detail: 'MicroShift is an optimized OpenShift Kubernetes for small form factor and edge computing.', + }, + ]; +} diff --git a/src/daemon-commander.ts b/src/daemon-commander.ts index 0c0dbbb..5ea4300 100644 --- a/src/daemon-commander.ts +++ b/src/daemon-commander.ts @@ -19,8 +19,10 @@ import got from 'got'; import { isWindows } from './util'; +export type CrcStatus = 'Running' | 'Starting' | 'Stopping' | 'Stopped' | 'No Cluster' | 'Error' | 'Unknown'; + export interface Status { - readonly CrcStatus: string; + readonly CrcStatus: CrcStatus; readonly Preset?: string; readonly OpenshiftStatus?: string; readonly OpenshiftVersion?: string; @@ -29,6 +31,21 @@ export interface Status { readonly DiskSize?: number; } +export type Preset = 'openshift' | 'microshift' | 'podman'; + +export interface Configuration { + preset: Preset; + cpus: number; + memory: number; + 'disk-size'?: number; + 'consent-telemetry'?: string; + 'http-proxy'?: string; + 'https-proxy'?: string; + 'no-proxy'?: string; + 'proxy-ca-file'?: string; + [key: string]: string | number; +} + export class DaemonCommander { private apiPath: string; @@ -87,11 +104,24 @@ export class DaemonCommander { return body; } - async configGet() { + async configGet(): Promise { const url = this.apiPath + '/config'; const { body } = await got(url); - return JSON.parse(body); + return JSON.parse(body).Configs; + } + + async configSet(values: Configuration): Promise { + const url = this.apiPath + '/config'; + + const result = await got.post(url, { + json: { properties: values }, + throwHttpErrors: false, + // body: values, + }); + if (result.statusCode !== 200) { + throw new Error(result.body); + } } async consoleUrl() { @@ -117,3 +147,20 @@ export class DaemonCommander { return body; } } + +export const commander = new DaemonCommander(); + +export async function isPullSecretMissing(): Promise { + let result = true; + + await commander + .pullSecretAvailable() + .then(() => { + result = true; + }) + .catch(() => { + result = false; + }); + + return result; +} diff --git a/src/extension.ts b/src/extension.ts index e7100eb..f17515d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,22 +20,26 @@ import * as extensionApi from '@podman-desktop/api'; import * as path from 'node:path'; import * as os from 'node:os'; import * as fs from 'node:fs'; -import type { Status } from './daemon-commander'; -import { DaemonCommander } from './daemon-commander'; +import type { CrcStatus, Status } from './daemon-commander'; +import { commander } from './daemon-commander'; import { LogProvider } from './log-provider'; -import { isWindows } from './util'; -import { daemonStart, daemonStop, getCrcVersion } from './crc-cli'; +import { isWindows, productName } from './util'; +import { daemonStart, daemonStop, getCrcVersion, needSetup } from './crc-cli'; import { getCrcDetectionChecks } from './detection-checks'; import { CrcInstall } from './install/crc-install'; +import { setUpCrc } from './crc-setup'; -const commander = new DaemonCommander(); let statusFetchTimer: NodeJS.Timer; let crcStatus: Status; const crcLogProvider = new LogProvider(commander); -const defaultStatus = { CrcStatus: 'Unknown', Preset: 'Unknown' }; +const defaultStatus: Status = { CrcStatus: 'Unknown', Preset: 'Unknown' }; +const errorStatus: Status = { CrcStatus: 'Error', Preset: 'Unknown' }; + +let isNeedSetup = false; +let isSetupGoing = false; export async function activate(extensionContext: extensionApi.ExtensionContext): Promise { const crcInstaller = new CrcInstall(); @@ -47,6 +51,11 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): if (crcVersion) { status = 'installed'; + } + crcStatus = defaultStatus; + + isNeedSetup = await needSetup(); + if (crcVersion) { connectToCrc(); } @@ -54,7 +63,7 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): // create CRC provider const provider = extensionApi.provider.createProvider({ - name: 'CRC', + name: productName, id: 'crc', version: crcVersion?.version, status: status, @@ -67,35 +76,29 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): extensionContext.subscriptions.push(provider); const daemonStarted = await daemonStart(); - if (!daemonStarted) { - return; - } const providerLifecycle: extensionApi.ProviderLifecycle = { status: () => convertToProviderStatus(crcStatus?.CrcStatus), - start: async context => { - try { - crcLogProvider.startSendingLogs(context.log); - await commander.start(); - } catch (err) { - console.error(err); - } + start: context => { + return startCrc(context.log); }, - stop: async () => { - console.log('extension:crc: receive the call stop'); - try { - await commander.stop(); - crcLogProvider.stopSendingLogs(); - } catch (err) { - console.error(err); - } + stop: () => { + return stopCrc(); }, }; - provider.registerLifecycle(providerLifecycle); - // initial preset check - presetChanged(provider, extensionContext); + extensionContext.subscriptions.push(provider.registerLifecycle(providerLifecycle)); + + if (!daemonStarted) { + crcStatus = errorStatus; + return; + } + + if (!isNeedSetup) { + // initial preset check + presetChanged(provider, extensionContext); + } if (crcInstaller.isAbleToInstall()) { const installationDisposable = provider.registerInstallation({ @@ -103,7 +106,10 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): return crcInstaller.getInstallChecks(); }, install: (logger: extensionApi.Logger) => { - return crcInstaller.doInstallCrc(provider, logger, async () => { + return crcInstaller.doInstallCrc(provider, logger, async (setupResult: boolean) => { + if (!setupResult) { + return; + } await connectToCrc(); presetChanged(provider, extensionContext); }); @@ -146,9 +152,7 @@ export function deactivate(): void { daemonStop(); - if (statusFetchTimer) { - clearInterval(statusFetchTimer); - } + stopUpdateTimer(); } async function registerOpenShiftLocalCluster( @@ -196,15 +200,25 @@ function convertToProviderStatus(crcStatus: string): extensionApi.ProviderStatus case 'Stopped': return 'stopped'; case 'No Cluster': - return 'configured'; + return 'stopped'; + case 'Error': + return 'error'; default: return 'not-installed'; } } async function startStatusUpdateTimer(): Promise { + if (statusFetchTimer) { + return; // we already set timer + } statusFetchTimer = setInterval(async () => { try { + // we don't need to update status while setup is going + if (isSetupGoing) { + crcStatus = createStatus('Starting', crcStatus.Preset); + return; + } crcStatus = await commander.status(); } catch (e) { console.error('CRC Status tick: ' + e); @@ -213,13 +227,21 @@ async function startStatusUpdateTimer(): Promise { }, 1000); } -function readPreset(crcStatus: Status): 'Podman' | 'OpenShift' | 'unknown' { +function stopUpdateTimer(): void { + if (statusFetchTimer) { + clearInterval(statusFetchTimer); + } +} + +function readPreset(crcStatus: Status): 'Podman' | 'OpenShift' | 'MicroShift' | 'unknown' { try { switch (crcStatus.Preset) { case 'podman': return 'Podman'; case 'openshift': return 'OpenShift'; + case 'microshift': + return 'MicroShift'; default: return 'unknown'; } @@ -230,12 +252,6 @@ function readPreset(crcStatus: Status): 'Podman' | 'OpenShift' | 'unknown' { } async function connectToCrc(): Promise { - const daemonStarted = await daemonStart(); - if (!daemonStarted) { - //TODO handle this - return; - } - try { // initial status crcStatus = await commander.status(); @@ -260,3 +276,43 @@ function presetChanged(provider: extensionApi.Provider, extensionContext: extens registerOpenShiftLocalCluster(provider, extensionContext); } } + +async function startCrc(logger: extensionApi.Logger): Promise { + try { + // call crc setup to prepare bundle, before start + if (isNeedSetup) { + try { + isSetupGoing = true; + crcStatus = createStatus('Starting', crcStatus.Preset); + await setUpCrc(logger); + isNeedSetup = false; + } catch (error) { + logger.error(error); + return; + } finally { + isSetupGoing = false; + } + } + crcLogProvider.startSendingLogs(logger); + await commander.start(); + } catch (err) { + console.error(err); + } +} + +async function stopCrc(): Promise { + console.log('extension:crc: receive the call stop'); + try { + await commander.stop(); + crcLogProvider.stopSendingLogs(); + } catch (err) { + console.error(err); + } +} + +function createStatus(crcStatus: CrcStatus, preset: string): Status { + return { + CrcStatus: crcStatus, + Preset: preset, + }; +} diff --git a/src/install/base-install.ts b/src/install/base-install.ts index 13dab46..1c6f309 100644 --- a/src/install/base-install.ts +++ b/src/install/base-install.ts @@ -100,16 +100,15 @@ export abstract class BaseInstaller implements Installer { } async downloadCrcInstaller(installerUrl: string, destinationPath: string, fileSha: string): Promise { - const lastProgressStr = ''; + const lastProgressStr = 'Downloading: 0%'; - this.statusBarItem.text = 'Downloading'; + this.statusBarItem.text = lastProgressStr; const downloadStream = got.stream(installerUrl); downloadStream.on('downloadProgress', progress => { const progressStr = /* progress.transferred + ' of ' + progress.total + ' ' + */ Math.round(progress.percent * 100) + '%'; if (lastProgressStr !== progressStr) { - //TODO: show progress on UI! this.statusBarItem.text = 'Downloading: ' + progressStr; } }); diff --git a/src/install/crc-install.ts b/src/install/crc-install.ts index af67ccb..a8b275b 100644 --- a/src/install/crc-install.ts +++ b/src/install/crc-install.ts @@ -23,9 +23,10 @@ import * as os from 'node:os'; import type { CrcReleaseInfo, Installer } from './base-install'; import { WinInstall } from './win-install'; -import { getCrcVersion } from '../crc-cli'; +import { getCrcVersion, needSetup } from '../crc-cli'; import { getCrcDetectionChecks } from '../detection-checks'; import { MacOsInstall } from './mac-install'; +import { setUpCrc } from '../crc-setup'; const crcLatestReleaseUrl = 'https://developers.redhat.com/content-gateway/rest/mirror/pub/openshift-v4/clients/crc/latest/release-info.json'; @@ -57,7 +58,7 @@ export class CrcInstall { public async doInstallCrc( provider: extensionApi.Provider, logger: extensionApi.Logger, - installFinishedFn: () => void, + installFinishedFn: (isSetUpFinished: boolean) => void, ): Promise { const latestRelease = await this.downloadLatestReleaseInfo(); @@ -77,7 +78,11 @@ export class CrcInstall { // update detections checks provider.updateDetectionChecks(getCrcDetectionChecks(newInstalledCrc)); - installFinishedFn(); + let setupResult = false; + if (await needSetup()) { + setupResult = await setUpCrc(logger, true); + } + installFinishedFn(setupResult); } } else { return; diff --git a/src/install/win-install.ts b/src/install/win-install.ts index d4db029..d463f88 100644 --- a/src/install/win-install.ts +++ b/src/install/win-install.ts @@ -24,7 +24,7 @@ import * as zipper from 'zip-local'; import * as extensionApi from '@podman-desktop/api'; import type { CrcReleaseInfo } from './base-install'; import { BaseCheck, BaseInstaller } from './base-install'; -import { isFileExists, runCliCommand } from '../util'; +import { isFileExists, productName, runCliCommand } from '../util'; const winInstallerName = 'crc-windows-installer.zip'; @@ -42,13 +42,12 @@ export class WinInstall extends BaseInstaller { progress.report({ increment: 10 }); const runResult = await runCliCommand('msiexec.exe', ['/i', msiPath, '/qr', '/norestart']); - if (runResult.exitCode !== 0) { // installed successfully, but reboot required if (runResult.exitCode === 3010) { progress.report({ increment: 99 }); extensionApi.window.showInformationMessage( - 'CRC is successfully installed. Reboot required to finalize system changes.', + `${productName} is successfully installed. Reboot required to finalize system changes.`, 'OK', ); return true; @@ -56,19 +55,24 @@ export class WinInstall extends BaseInstaller { // user cancel installation return false; } else { - throw new Error(runResult.stdErr); + throw new Error( + `${productName} installation failed with unexpected code: ${runResult.exitCode}. StdOut: ${runResult.stdOut}. StdErr: ${runResult.stdErr}`, + ); } } progress.report({ increment: 80 }); - extensionApi.window.showNotification({ body: 'CRC is successfully installed.' }); + extensionApi.window.showNotification({ body: `${productName} is successfully installed.` }); return true; } else { - throw new Error(`Can't find CRC setup package! Path: ${setupPath} doesn't exists.`); + throw new Error(`Can't find ${productName} setup package! Path: ${setupPath} doesn't exists.`); } } catch (err) { console.error('Error during CRC install!'); console.error(err); - await extensionApi.window.showErrorMessage('Unexpected error, during CRC installation: ' + err, 'OK'); + await extensionApi.window.showErrorMessage( + `Unexpected error, during ${productName} installation: + ${err}`, + 'OK', + ); return false; } finally { progress.report({ increment: -1 }); @@ -92,21 +96,25 @@ export class WinInstall extends BaseInstaller { } private async extractMsiFromZip(zipPath: string): Promise { - const outPath = path.join(os.tmpdir(), 'crc'); + const outPath = path.join(os.tmpdir(), 'crc-extension'); if (!(await isFileExists(outPath))) { await fs.mkdir(outPath); } - zipper.unzip(zipPath, (err, res) => { - if (err) { - throw err; - } else { - res.save(outPath, saveErr => { - if (saveErr) { - throw saveErr; - } - }); - } + await new Promise((resolve, reject) => { + zipper.unzip(zipPath, (err, res) => { + if (err) { + reject(err); + } else { + res.save(outPath, saveErr => { + if (saveErr) { + reject(saveErr); + } else { + resolve(); + } + }); + } + }); }); return path.join(outPath, 'crc-windows-amd64.msi'); } diff --git a/src/util.ts b/src/util.ts index 5b6fc49..9ed1099 100644 --- a/src/util.ts +++ b/src/util.ts @@ -17,10 +17,11 @@ ***********************************************************************/ import * as os from 'node:os'; -import * as path from 'node:path'; import { spawn } from 'node:child_process'; import * as fs from 'node:fs/promises'; +export const productName = 'OpenShift Local'; + const windows = os.platform() === 'win32'; export function isWindows(): boolean { return windows; @@ -34,19 +35,6 @@ export function isLinux(): boolean { return linux; } -/** - * @returns true if app running in dev mode - */ -export function isDev(): boolean { - const isEnvSet = 'ELECTRON_IS_DEV' in process.env; - const envSet = Number.parseInt(process.env.ELECTRON_IS_DEV, 10) === 1; - return isEnvSet ? envSet : false; -} - -export function getAssetsFolder(): string { - return path.resolve(__dirname, '..', 'assets'); -} - export interface SpawnResult { exitCode: number; stdOut: string;