Skip to content

Commit

Permalink
RDX: Support hooks to run native code on install/uninstall
Browse files Browse the repository at this point in the history
This lets extensions define hooks to be run on install or uninstall.  Each
extension can run one command on install (after everything else is up), and
one command on uninstall (before anything is torn down).  Any errors are
logged but otherwise ignored.  Note that extensions get re-installed on
startup, so both scripts should be idempotent.

Signed-off-by: Mark Yen <mark.yen@suse.com>
  • Loading branch information
mook-as committed Sep 25, 2024
1 parent 0efb3bd commit 3c50a1e
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 14 deletions.
75 changes: 63 additions & 12 deletions pkg/rancher-desktop/main/extensions/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

import type { ContainerEngineClient } from '@pkg/backend/containerClient';
import mainEvents from '@pkg/main/mainEvents';
import { spawnFile } from '@pkg/utils/childProcess';
import { parseImageReference } from '@pkg/utils/dockerUtils';
import Logging from '@pkg/utils/logging';
import paths from '@pkg/utils/paths';
Expand Down Expand Up @@ -218,6 +219,33 @@ export class ExtensionImpl implements Extension {
throw new ExtensionErrorImpl(code, `${ prefix } Image is not allowed`);
}

/**
* Determine the post-install or pre-uninstall script to run, if any.
* Returns the script executable plus arguments; the executable path is always
* absolute.
*/
protected getScriptArgs(metadata: ExtensionMetadata, key: 'x-rd-install' | 'x-rd-uninstall'): string[] | undefined {
const scriptData = metadata.host?.[key]?.[this.platform];

if (!scriptData) {
return;
}

const [scriptName, ...scriptArgs] = Array.isArray(scriptData) ? scriptData : [scriptData];
const description = {
'x-rd-install': 'Post-install',
'x-rd-uninstall': 'Pre-uninstall',
}[key];
const binDir = path.join(this.dir, 'bin');
const scriptPath = path.normalize(path.resolve(binDir, scriptName));

if (/^\.+[/\\]/.test(path.relative(binDir, scriptPath))) {
throw new Error(`${ description } script for ${ this.id } (${ scriptName }) not inside binaries directory`);
}

return [scriptPath, ...scriptArgs];
}

async install(allowedImages: readonly string[] | undefined): Promise<boolean> {
const metadata = await this.metadata;

Expand All @@ -242,6 +270,17 @@ export class ExtensionImpl implements Extension {

mainEvents.emit('settings-write', { application: { extensions: { installed: { [this.id]: this.version } } } });

try {
const [scriptPath, ...scriptArgs] = this.getScriptArgs(metadata, 'x-rd-install') ?? [];

if (scriptPath) {
console.log(`Running ${ this.id } post-install script: ${ scriptPath } ${ scriptArgs.join(' ') }...`);
await spawnFile(scriptPath, scriptArgs, { stdio: console, cwd: path.dirname(scriptPath) });
}
} catch (ex) {
console.error(`Ignoring error running ${ this.id } post-install script: ${ ex }`);
}

console.debug(`Install ${ this.id }: install complete.`);

return true;
Expand Down Expand Up @@ -303,23 +342,24 @@ export class ExtensionImpl implements Extension {
}));
}

protected get platform() {
switch (process.platform) {
case 'win32':
return 'windows';
case 'linux':
case 'darwin':
return process.platform;
default:
throw new Error(`Platform ${ process.platform } is not supported`);
}
}

protected async installHostExecutables(workDir: string, metadata: ExtensionMetadata): Promise<void> {
const plat = (() => {
switch (process.platform) {
case 'win32':
return 'windows';
case 'linux':
case 'darwin':
return process.platform;
default:
throw new Error(`Platform ${ process.platform } is not supported`);
}
})();
const binDir = path.join(workDir, 'bin');

await fs.promises.mkdir(binDir, { recursive: true });
const binaries = metadata.host?.binaries ?? [];
const paths = binaries.flatMap(p => p[plat]).map(b => b?.path).filter(defined);
const paths = binaries.flatMap(p => p[this.platform]).map(b => b?.path).filter(defined);

await Promise.all(paths.map(async(p) => {
try {
Expand Down Expand Up @@ -472,6 +512,17 @@ export class ExtensionImpl implements Extension {
return false;
}

try {
const [scriptPath, ...scriptArgs] = this.getScriptArgs(await this.metadata, 'x-rd-uninstall') ?? [];

if (scriptPath) {
console.log(`Running ${ this.id } pre-uninstall script: ${ scriptPath } ${ scriptArgs.join(' ') }...`);
await spawnFile(scriptPath, scriptArgs, { stdio: console, cwd: path.dirname(scriptPath) });
}
} catch (ex) {
console.error(`Ignoring error running ${ this.id } pre-uninstall script: ${ ex }`);
}

try {
await this.uninstallContainers();
} catch (ex) {
Expand Down
20 changes: 18 additions & 2 deletions pkg/rancher-desktop/main/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { ContainerEngineClient } from '@pkg/backend/containerClient';
import type { Settings } from '@pkg/config/settings';
import type { RecursiveReadonly } from '@pkg/utils/typeUtils';

type PlatformSpecific<T> = Record<'darwin' | 'windows' | 'linux', T>;

export type ExtensionMetadata = {
/** Icon for the extension, as a path in the image. */
icon: string;
Expand Down Expand Up @@ -33,8 +35,22 @@ export type ExtensionMetadata = {
socket: string;
}
};
/** Files to copy to the host. */
host?: { binaries: Record<'darwin' | 'windows' | 'linux', { path: string }[]>[] };
host?: {
/** Files to copy to the host. */
binaries: PlatformSpecific<{ path: string }[]>[],
/**
* Rancher Desktop extension: this will be run after the extension is
* installed (possibly as an upgrade). This file should be listed in
* `binaries`. Errors will be ignored.
*/
'x-rd-install'?: PlatformSpecific<string|string[]>,
/**
* Rancher Desktop extension: this will be run before the extension is
* uninstalled (possibly as an upgrade). This file should be listed in
* `binaries`. Errors will be ignored.
*/
'x-rd-uninstall'?: PlatformSpecific<string|string[]>,
};
};

/**
Expand Down

0 comments on commit 3c50a1e

Please sign in to comment.