Skip to content

Commit

Permalink
Implement CRC CLI update (#162)
Browse files Browse the repository at this point in the history
* Implement CRC update

Signed-off-by: Yevhen Vydolob <[email protected]>

* Rename update method, add telemetry

Signed-off-by: Yevhen Vydolob <[email protected]>

---------

Signed-off-by: Yevhen Vydolob <[email protected]>
  • Loading branch information
evidolob authored Jul 13, 2023
1 parent 891ff0c commit 9a19118
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 37 deletions.
24 changes: 23 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ let connectionDisposable: extensionApi.Disposable;
let crcVersion: CrcVersion | undefined;

export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
const crcInstaller = new CrcInstall();
const crcInstaller = new CrcInstall(extensionContext.storagePath);
extensionApi.configuration.getConfiguration();
crcVersion = await getCrcVersion();
const telemetryLogger = extensionApi.env.createTelemetryLogger();
Expand Down Expand Up @@ -165,6 +165,10 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
extensionContext.subscriptions.push(installationDisposable);
}

if (crcVersion) {
await registerCrcUpdate(crcVersion, crcInstaller, provider, telemetryLogger);
}

extensionContext.subscriptions.push(
presetChangedEvent(() => {
presetChanged(provider, extensionContext, telemetryLogger);
Expand All @@ -178,6 +182,24 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
);
}

async function registerCrcUpdate(
crcVersion: CrcVersion,
crcInstaller: CrcInstall,
provider: extensionApi.Provider,
telemetry: extensionApi.TelemetryLogger,
): Promise<void> {
const updateInfo = await crcInstaller.hasUpdate(crcVersion);
if (updateInfo.hasUpdate) {
provider.registerUpdate({
version: updateInfo.newVersion.version.crcVersion,
update: logger => {
return crcInstaller.askForUpdate(provider, updateInfo, logger, telemetry);
},
preflightChecks: () => crcInstaller.getUpdatePreflightChecks(),
});
}
}

function registerProviderLifecycleAndFactory(
provider: extensionApi.Provider,
extensionContext: extensionApi.ExtensionContext,
Expand Down
27 changes: 3 additions & 24 deletions src/install/base-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import path from 'node:path';
import stream from 'node:stream/promises';
import * as os from 'node:os';
import { isFileExists, productName } from '../util';
import type { CrcReleaseInfo } from '../types';

export abstract class BaseCheck implements extensionApi.InstallCheck {
abstract title: string;
Expand All @@ -43,35 +44,19 @@ export abstract class BaseCheck implements extensionApi.InstallCheck {
}
}

export interface CrcReleaseInfo {
version: {
crcVersion: string;
gitSha: string;
openshiftVersion: string;
podmanVersion: string;
};

links: {
linux: string;
darwin: string;
windows: string;
};
}

export interface Installer {
getPreflightChecks(): extensionApi.InstallCheck[] | undefined;
getUpdatePreflightChecks(): extensionApi.InstallCheck[] | undefined;
install(releaseInfo: CrcReleaseInfo, logger?: extensionApi.Logger): Promise<boolean>;
requireUpdate(installedVersion: string): boolean;
update(): Promise<boolean>;
update(releaseInfo: CrcReleaseInfo, logger?: extensionApi.Logger): Promise<boolean>;
}

export abstract class BaseInstaller implements Installer {
protected statusBarItem: extensionApi.StatusBarItem | undefined;

abstract install(releaseInfo: CrcReleaseInfo, logger?: extensionApi.Logger): Promise<boolean>;

abstract update(): Promise<boolean>;
abstract update(releaseInfo: CrcReleaseInfo, logger?: extensionApi.Logger): Promise<boolean>;

abstract getUpdatePreflightChecks(): extensionApi.InstallCheck[];

Expand Down Expand Up @@ -147,12 +132,6 @@ export abstract class BaseInstaller implements Installer {
return installerPath;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
requireUpdate(installedVersion: string): boolean {
// return compare(installedVersion, getBundledPodmanVersion(), '<');
throw new Error('requireUpdate is not implemented yet');
}

private createStatusBar(): void {
this.statusBarItem = extensionApi.window.createStatusBarItem('RIGHT', 1000);
this.statusBarItem.show();
Expand Down
112 changes: 110 additions & 2 deletions src/install/crc-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,81 @@
import * as extensionApi from '@podman-desktop/api';
import got from 'got';
import * as os from 'node:os';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';

import type { CrcReleaseInfo, Installer } from './base-install';
import type { Installer } from './base-install';
import { WinInstall } from './win-install';

import type { CrcVersion } from '../crc-cli';
import { getCrcVersion } from '../crc-cli';
import { getCrcDetectionChecks } from '../detection-checks';
import { MacOsInstall } from './mac-install';
import { needSetup, setUpCrc } from '../crc-setup';
import type { CrcReleaseInfo, CrcUpdateInfo } from '../types';

import { compare } from 'compare-versions';
import { isFileExists, productName } from '../util';

const crcLatestReleaseUrl =
'https://developers.redhat.com/content-gateway/rest/mirror/pub/openshift-v4/clients/crc/latest/release-info.json';

export interface CrcCliInfo {
ignoreVersionUpdate?: string;
}
class CrcCliInfoStorage {
private crcInfo: CrcCliInfo;

constructor(private readonly storagePath: string) {}

get ignoreVersionUpdate(): string {
return this.crcInfo.ignoreVersionUpdate;
}

set ignoreVersionUpdate(version: string) {
if (this.crcInfo.ignoreVersionUpdate !== version) {
this.crcInfo.ignoreVersionUpdate = version;
this.writeInfo().catch((err: unknown) => console.error(`Unable to write ${productName} Version`, err));
}
}

private async writeInfo(): Promise<void> {
try {
const podmanInfoPath = path.resolve(this.storagePath, 'crc-ext.json');
await fs.writeFile(podmanInfoPath, JSON.stringify(this.crcInfo));
} catch (err) {
console.error(err);
}
}

async loadInfo(): Promise<void> {
const podmanInfoPath = path.resolve(this.storagePath, 'crc-ext.json');
if (!(await isFileExists(this.storagePath))) {
await fs.mkdir(this.storagePath);
}

if (!(await isFileExists(podmanInfoPath))) {
this.crcInfo = {} as CrcCliInfo;
return;
}

try {
const infoBuffer = await fs.readFile(podmanInfoPath);
const crcInfo = JSON.parse(infoBuffer.toString('utf8')) as CrcCliInfo;
this.crcInfo = crcInfo;
return;
} catch (err) {
console.error(err);
}
this.crcInfo = {} as CrcCliInfo;
}
}

export class CrcInstall {
private installers = new Map<NodeJS.Platform, Installer>();
private crcCliInfo: CrcCliInfoStorage;

constructor() {
constructor(private readonly storagePath: string) {
this.installers.set('win32', new WinInstall());
this.installers.set('darwin', new MacOsInstall());
}
Expand Down Expand Up @@ -87,6 +145,56 @@ export class CrcInstall {
}
}

async hasUpdate(version: CrcVersion): Promise<CrcUpdateInfo> {
const latestRelease = await this.downloadLatestReleaseInfo();
this.crcCliInfo = new CrcCliInfoStorage(this.storagePath);
await this.crcCliInfo.loadInfo();
if (
compare(latestRelease.version.crcVersion, version.version, '>') &&
this.crcCliInfo.ignoreVersionUpdate !== latestRelease.version.crcVersion
) {
return { hasUpdate: true, newVersion: latestRelease, currentVersion: version.version };
}

return { hasUpdate: false, currentVersion: version.version };
}

getUpdatePreflightChecks(): extensionApi.InstallCheck[] | undefined {
const installer = this.getInstaller();
if (installer) {
return installer.getUpdatePreflightChecks();
}
return undefined;
}

async askForUpdate(
provider: extensionApi.Provider,
updateInfo: CrcUpdateInfo,
logger: extensionApi.Logger,
telemetry: extensionApi.TelemetryLogger,
): Promise<void> {
const newVersion = updateInfo.newVersion.version.crcVersion;
const answer = await extensionApi.window.showInformationMessage(
`You have ${productName} ${updateInfo.currentVersion}.\nDo you want to update to ${newVersion}?`,
'Yes',
'No',
'Ignore',
);
if (answer === 'Yes') {
telemetry.logUsage('crc.update.start', { version: newVersion });
await this.getInstaller().update(updateInfo.newVersion, logger);
const crcVersion = await getCrcVersion();
provider.updateDetectionChecks(getCrcDetectionChecks(crcVersion));
provider.updateVersion(crcVersion.version);
this.crcCliInfo.ignoreVersionUpdate = undefined;
} else if (answer === 'Ignore') {
telemetry.logUsage('crc.update.ignored', { version: newVersion });
this.crcCliInfo.ignoreVersionUpdate = updateInfo.newVersion.version.crcVersion;
} else {
telemetry.logUsage('crc.update.canceled', { version: newVersion });
}
}

private async installCrc(releaseInfo: CrcReleaseInfo, logger: extensionApi.Logger): Promise<boolean> {
const installer = this.getInstaller();
if (installer) {
Expand Down
12 changes: 6 additions & 6 deletions src/install/mac-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import * as os from 'node:os';

import * as extensionApi from '@podman-desktop/api';
import { compare } from 'compare-versions';
import type { CrcReleaseInfo } from './base-install';
import { BaseCheck, BaseInstaller } from './base-install';
import { isFileExists, runCliCommand } from '../util';
import type { CrcReleaseInfo } from '../types';

const macosInstallerFineName = 'crc-macos-installer.pkg';

export class MacOsInstall extends BaseInstaller {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
install(releaseInfo: CrcReleaseInfo, logger?: extensionApi.Logger): Promise<boolean> {
install(releaseInfo: CrcReleaseInfo, logger?: extensionApi.Logger, isUpdate = false): Promise<boolean> {
return extensionApi.window.withProgress({ location: extensionApi.ProgressLocation.APP_ICON }, async progress => {
progress.report({ increment: 5 });

Expand All @@ -43,7 +43,7 @@ export class MacOsInstall extends BaseInstaller {
progress.report({ increment: 80 });
// we cannot rely on exit code, as installer could be closed and it return '0' exit code
// so just check that crc bin file exist.
if (await isFileExists('/usr/local/bin/crc')) {
if ((await isFileExists('/usr/local/bin/crc')) && !isUpdate) {
extensionApi.window.showNotification({ body: 'OpenShift Local is successfully installed.' });
return true;
} else {
Expand All @@ -64,11 +64,11 @@ export class MacOsInstall extends BaseInstaller {
}
});
}
update(): Promise<boolean> {
throw new Error('Method not implemented.');
update(releaseInfo: CrcReleaseInfo, logger: extensionApi.Logger): Promise<boolean> {
return this.install(releaseInfo, logger, true);
}
getUpdatePreflightChecks(): extensionApi.InstallCheck[] {
throw new Error('Method not implemented.');
return [];
}
getPreflightChecks(): extensionApi.InstallCheck[] {
return [new MacCPUCheck(), new MacMemoryCheck(), new MacVersionCheck()];
Expand Down
8 changes: 4 additions & 4 deletions src/install/win-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import * as fs from 'node:fs/promises';
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, productName, runCliCommand } from '../util';
import type { CrcReleaseInfo } from '../types';

const winInstallerName = 'crc-windows-installer.zip';

Expand Down Expand Up @@ -88,11 +88,11 @@ export class WinInstall extends BaseInstaller {
return [new WinBitCheck(), new CpuCoreCheck(), new WinVersionCheck(), new WinMemoryCheck()];
}

update(): Promise<boolean> {
throw new Error('Method not implemented.');
update(releaseInfo: CrcReleaseInfo, logger: extensionApi.Logger): Promise<boolean> {
return this.install(releaseInfo, logger);
}
getUpdatePreflightChecks(): extensionApi.InstallCheck[] {
throw new Error('Method not implemented.');
return [];
}

private async extractMsiFromZip(zipPath: string): Promise<string> {
Expand Down
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,24 @@ export interface StartInfo {
ClusterConfig: ClusterConfig;
KubeletStarted: boolean;
}

export interface CrcReleaseInfo {
version: {
crcVersion: string;
gitSha: string;
openshiftVersion: string;
podmanVersion: string;
};

links: {
linux: string;
darwin: string;
windows: string;
};
}

export interface CrcUpdateInfo {
newVersion?: CrcReleaseInfo;
currentVersion: string;
hasUpdate: boolean;
}

0 comments on commit 9a19118

Please sign in to comment.