Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement CRC CLI update #162

Merged
merged 2 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
gbraad marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to still install this update?
People might ignore it, meaning ignore for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They have Cancel button.
I could rename Ignore to Ignore %{version}.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does make sense if expressed this way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's put this in a new issue... but not implement this yet.
as I see this is also in the answer === check

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