Skip to content

Commit

Permalink
feat: add crc cli detection checks
Browse files Browse the repository at this point in the history
Signed-off-by: Yevhen Vydolob <[email protected]>
  • Loading branch information
evidolob committed Mar 8, 2023
1 parent ecf65e7 commit 3d82c40
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 64 deletions.
170 changes: 170 additions & 0 deletions src/crc-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**********************************************************************
* 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 type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
import { isMac, isWindows } from './util';

import type { Logger } from '@tmpwip/extension-api';

const macosExtraPath = '/usr/local/bin:/opt/local/bin';
const crcWindowsInstallPath = 'c:\\Program Files\\Red Hat OpenShift Local';

let daemonProcess: ChildProcess;

export function getInstallationPath(): string {
const env = process.env;
if (isWindows()) {
return `${crcWindowsInstallPath};${env.PATH}`;
} else if (isMac()) {
if (!env.PATH) {
return macosExtraPath;
} else {
return env.PATH.concat(':').concat(macosExtraPath);
}
} else {
return env.PATH;
}
}

export function getCrcCli(): string {
if (isWindows()) {
return 'crc.exe';
}
return 'crc';
}

export interface ExecOptions {
logger?: Logger;
env: NodeJS.ProcessEnv | undefined;
}

export function execPromise(command: string, args?: string[], options?: ExecOptions): Promise<string> {
let env = Object.assign({}, process.env); // clone original env object

// In production mode, applications don't have access to the 'user' path like brew
if (isMac() || isWindows()) {
env.PATH = getInstallationPath();
} else if (env.FLATPAK_ID) {
// need to execute the command on the host
args = ['--host', command, ...args];
command = 'flatpak-spawn';
}

if (options?.env) {
env = Object.assign(env, options.env);
}
return new Promise((resolve, reject) => {
let stdOut = '';
let stdErr = '';

const process = spawn(command, args, { env });
process.stdout.setEncoding('utf8');
process.stderr.setEncoding('utf8');

process.stdout.on('data', data => {
stdOut += data;
options?.logger?.log(data);
});

process.stderr.on('data', data => {
stdErr += data;
options?.logger?.error(data);
});

process.on('error', error => {
let content = '';
if (stdOut && stdOut !== '') {
content += stdOut + '\n';
}
if (stdErr && stdErr !== '') {
content += stdErr + '\n';
}
reject(content + error);
});

process.on('close', exitCode => {
let content = '';
if (stdOut && stdOut !== '') {
content += stdOut + '\n';
}
if (stdErr && stdErr !== '') {
content += stdErr + '\n';
}

if (exitCode !== 0) {
reject(content);
}
resolve(stdOut.trim());
});
});
}

export interface CrcVersion {
version: string;
openshiftVersion: string;
podmanVersion: string;
}

export async function getCrcVersion(): Promise<CrcVersion | undefined> {
try {
const versionOut = await execPromise(getCrcCli(), ['version', '-o', 'json']);
if (versionOut) {
return JSON.parse(versionOut);
}
} catch (err) {
// no crc binary or we cant parse output
}

return undefined;
}

export async function daemonStart(): Promise<boolean> {
let command = getCrcCli();
let args = ['daemon', '--watchdog'];

const env = Object.assign({}, process.env); // clone original env object

// In production mode, applications don't have access to the 'user' path like brew
if (isMac() || isWindows()) {
env.PATH = getInstallationPath();
} else if (env.FLATPAK_ID) {
// need to execute the command on the host
args = ['--host', command, ...args];
command = 'flatpak-spawn';
}

// launching the daemon
daemonProcess = spawn(command, args, {
detached: true,
windowsHide: true,
env,
});

daemonProcess.on('error', err => {
const msg = `CRC daemon failure, daemon failed to start: ${err}`;
console.error('CRC failure', msg);
});

return true;
}

export function daemonStop() {
if (daemonProcess && daemonProcess.exitCode !== null) {
daemonProcess.kill();
}
}
40 changes: 40 additions & 0 deletions src/detection-checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**********************************************************************
* 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 type { ProviderDetectionCheck } from '@tmpwip/extension-api';
import type { CrcVersion } from './crc-cli';
import { getInstallationPath } from './crc-cli';

export function getCrcDetectionChecks(version?: CrcVersion): ProviderDetectionCheck[] {
const detectionChecks: ProviderDetectionCheck[] = [];

if (version && version.version) {
detectionChecks.push({
name: `CRC ${version.version} found`,
status: true,
});
} else {
detectionChecks.push({
name: 'CRC cli was not found in the PATH',
details: `Current path is ${getInstallationPath()}`,
status: false,
});
}

return detectionChecks;
}
102 changes: 38 additions & 64 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import * as extensionApi from '@tmpwip/extension-api';
import * as path from 'node:path';
import * as os from 'node:os';
import * as fs from 'node:fs';
import * as childProcess from 'node:child_process';
import type { Status } from './daemon-commander';
import { DaemonCommander } from './daemon-commander';
import { LogProvider } from './log-provider';
import { isMac, isWindows } from './util';
import { isWindows } from './util';
import { daemonStart, daemonStop, getCrcVersion } from './crc-cli';
import { getCrcDetectionChecks } from './detection-checks';

const commander = new DaemonCommander();
let daemonProcess: childProcess.ChildProcess;
let statusFetchTimer: NodeJS.Timer;

let crcStatus: Status;
Expand All @@ -37,17 +37,43 @@ const crcLogProvider = new LogProvider(commander);
const defaultStatus = { CrcStatus: 'Unknown', Preset: 'Unknown' };

export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
// crc is installed or not ?
if (!fs.existsSync(crcBinary())) {
console.warn('Can not find CRC binary!');
return;
const crcVersion = await getCrcVersion();

const detectionChecks: extensionApi.ProviderDetectionCheck[] = [];
let status: extensionApi.ProviderStatus = 'not-installed';
let preset = 'unknown';
if (crcVersion) {
status = 'installed';

const daemonStarted = await daemonStart();
if (!daemonStarted) {
//TODO handle this
return;
}

try {
// initial status
crcStatus = await commander.status();
} catch (err) {
console.error('error in CRC extension', err);
crcStatus = defaultStatus;
}

// detect preset of CRC
preset = readPreset(crcStatus);

startStatusUpdateTimer();
}

detectionChecks.push(...getCrcDetectionChecks(crcVersion));

// create CRC provider
const provider = extensionApi.provider.createProvider({
name: 'CRC',
id: 'crc',
status: 'unknown',
version: crcVersion?.version,
status: status,
detectionChecks: detectionChecks,
images: {
icon: './icon.png',
logo: './icon.png',
Expand All @@ -60,17 +86,6 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
return;
}

try {
// initial status
crcStatus = await commander.status();
} catch (err) {
console.error('error in CRC extension', err);
crcStatus = defaultStatus;
}

// detect preset of CRC
const preset = readPreset(crcStatus);

const providerLifecycle: extensionApi.ProviderLifecycle = {
status: () => convertToProviderStatus(crcStatus?.CrcStatus),

Expand Down Expand Up @@ -101,47 +116,6 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
// OpenShift
registerOpenShiftLocalCluster(provider, extensionContext);
}

startStatusUpdateTimer();
}

function crcBinary(): string {
if (isWindows()) {
// This returns `crc` as located in c:\Program Files\OpenShift Local\
return path.join('C:\\Program Files\\Red Hat OpenShift Local\\crc.exe');
}

if (isMac()) {
// This returns `crc` as located in /usr/local/bin/crc
return '/usr/local/bin/crc';
}

// No path provided
return 'crc';
}

async function daemonStart(): Promise<boolean> {
// launching the daemon
daemonProcess = childProcess.spawn(crcBinary(), ['daemon', '--watchdog'], {
detached: true,
windowsHide: true,
});

daemonProcess.on('error', err => {
const msg = `Backend failure, Backend failed to start: ${err}`;
// TODO: show error on UI!
console.error('Backend failure', msg);
});

daemonProcess.stdout.on('date', () => {
// noop
});

daemonProcess.stderr.on('data', () => {
// noop
});

return true;
}

function registerPodmanConnection(provider: extensionApi.Provider, extensionContext: extensionApi.ExtensionContext) {
Expand Down Expand Up @@ -174,9 +148,9 @@ function registerPodmanConnection(provider: extensionApi.Provider, extensionCont

export function deactivate(): void {
console.log('stopping crc extension');
if (daemonProcess) {
daemonProcess.kill();
}

daemonStop();

if (statusFetchTimer) {
clearInterval(statusFetchTimer);
}
Expand Down Expand Up @@ -229,7 +203,7 @@ function convertToProviderStatus(crcStatus: string): extensionApi.ProviderStatus
case 'No Cluster':
return 'configured';
default:
return 'unknown';
return 'not-installed';
}
}

Expand Down

0 comments on commit 3d82c40

Please sign in to comment.