跳转到主内容

开发 Podman Desktop 扩展

Podman Desktop 的组织结构允许您以“扩展”以及相应的 extension-api 的形式模块化地添加新功能。这使您能够与 Podman Desktop 通信,而无需了解其内部工作原理。您只需查找 API 调用,Podman Desktop 就会完成其余的工作。

为了进行类型检查,建议使用 TypeScript 编写扩展,但也可以使用 JavaScript 编写。

大多数扩展是外部加载的;但是,我们也会通过将它们作为使用相同 API 的内部扩展来加载,从而对我们自己的 API 进行“dogfooding”(内部测试)。这些内部维护的扩展可以作为如何构建外部加载扩展的示例和基础。

创建新扩展的概述

我们尝试通过利用 package.json 并在扩展内部保持激活过程的简单性,仅提供两个入口点:activate()deactivate(),来尽可能简化扩展的创建。

所有 Podman Desktop 的功能都完全通过 API 进行通信。您创建的扩展通过 @podman-desktop/api 包与 Podman Desktop API 交互。API 代码位于此处,而代码的网站表示形式可以在此处找到。

激活

激活扩展时,Podman Desktop 将会:

  1. 搜索并加载扩展目录中 package.json 文件里 main 条目指定的 JavaScript 文件(通常是 extension.js)。
  2. 运行导出的 activate 函数。

停用

停用扩展时,Podman Desktop 将会:

  1. 运行可选导出的 deactivate 函数。
  2. 释放已添加到 extensionContext.subscriptions 的任何资源,请参阅 extension-loader.ts 中的 deactivateExtension

示例样板代码

这是一个 extensions/foobar/src/extensions.ts 文件的示例,其中包含基本的 activatedeactivate 功能,前提是您已经创建了 package.json 文件。

import * as extensionApi from '@podman-desktop/api';

// Activate the extension asynchronously
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
// Create a provider with an example name, ID and icon
const provider = extensionApi.provider.createProvider({
name: 'FooBar',
id: 'foobar',
status: 'unknown',
images: {
icon: './icon.png',
logo: './icon.png',
},
});

// Push the new provider to Podman Desktop
extensionContext.subscriptions.push(provider);
}

// Deactivate the extension
export function deactivate(): void {
console.log('stopping FooBar extension');
}

与 UI 交互

扩展通过多种方式“挂钩”到 Podman Desktop UI,包括:

  • 将扩展注册为特定的提供程序(身份验证、注册表、Kubernetes、容器、CLI 工具或其他)。
  • 注册特定事件(以 onDid... 开头的函数)。
  • 向菜单添加条目(托盘菜单、状态栏和其他类型的菜单)。
  • 向配置面板添加字段。
  • 监视文件系统中的文件。

当通过这些不同的注册访问扩展代码时,扩展可以使用 API 提供的实用函数来:

  • 获取配置字段的值。
  • 通过输入框和快速选择与用户交互。
  • 向用户显示信息、警告、错误消息和通知。
  • 获取有关环境的信息(操作系统、遥测、系统剪贴板)。
  • 在系统中执行进程。
  • 向遥测发送数据。
  • 在上下文中设置数据,这些数据会传播到 UI 中。

创建并运行您的扩展

您可以通过执行以下端到端任务来创建和运行扩展:

  1. 初始化扩展
  2. 编写扩展的功能
  3. 构建依赖项
  4. 运行扩展
  5. 验证扩展的功能

扩展您的扩展

以下是可以帮助您扩展扩展的文档和/或“样板”代码。

使用 ProviderStatus

Podman Desktop 通过来自 extension-api 的一系列状态来运行每个提供程序。

export type ProviderStatus =
| 'not-installed'
| 'installed'
| 'configured'
| 'ready'
| 'started'
| 'stopped'
| 'starting'
| 'stopping'
| 'error'
| 'unknown';

ProviderStatus 向主提供程序页面提供信息,详细说明该提供程序是否已安装、就绪、已启动、已停止等。

这可以在您的扩展中通过调用例如 provider.updateStatus('installed') 来更新。Podman Desktop 将在主屏幕上显示状态。

注意: ProviderStatus 仅用于提供信息,并可以在扩展内部用于跟踪 activate()deactivate() 是否正常工作。

使用 ProviderConnectionStatus

export type ProviderConnectionStatus = 'started' | 'stopped' | 'starting' | 'stopping' | 'unknown';

注意: unknown 状态是唯一的,因为它不会显示在 Podman Desktop 的扩展部分,也无法通过 API 调用访问。未知状态通常在 Podman Desktop 无法加载扩展时发生。

ProviderConnectionStatus 是您扩展的主要“生命周期”。该状态由 Podman Desktop 自动更新,并反映在提供程序中。

通过扩展中的 activate 函数成功启动后,ProviderConnectionStatus 将反映为“started”。

ProviderConnectionStatus 状态用于两个区域:extension-loader.tstray-menu.ts

  • extension-loader.ts: 尝试加载扩展并相应地设置状态(startedstoppedstartingstopping)。如果发生未知错误,状态将设置为 unknownextension-loader.ts 还会向 Podman Desktop 发送 API 调用以更新扩展的 UI。
  • tray-menu.ts: 如果使用了 extensionApi.tray.registerMenuItem(item); API 调用,将创建扩展的托盘菜单。创建后,Podman Desktop 将使用 ProviderConnectionStatus 在托盘菜单中指示状态。

添加命令

命令

使用 package.json 文件的 contributes 部分声明命令。

 "contributes": {
"commands": [
{
"command": "my.command",
"title": "This is my command",
"category": "Optional category to prefix title",
"enablement": "myProperty === myValue"
},
],
}

如果可选的 enablement 属性评估为 false,命令面板将不显示此命令。

要注册命令的回调,请使用以下代码:

import * as extensionApi from '@podman-desktop/api';

extensionContext.subscriptions.push(extensionApi.commands.registerCommand('my.command', async () => {
// callback of your command
await extensionApi.window.showInformationMessage('Clicked on my command');
});
);

扩展 extension-api API

有时,您需要向 API 添加新功能,以便在 Podman Desktop 内部进行更改。例如,在渲染器中发生新的 UI/UX 组件,您需要扩展 API 才能对 Podman Desktop 的内部工作进行该更改。

请注意,API 贡献需要经过批准,因为我们希望保持 API 的可持续性/一致性。在编写代码之前,在 issue 中进行讨论将是有益的。

在此示例中,我们将添加一个新函数,以简单地在控制台中显示:“hello world”。

  1. 在命名空间下,将新函数添加到 /packages/extension-api/src/extension-api.d.ts。这将使其在您的扩展中调用时可在 API 内访问。
export namespace foobar {
// ...
export function hello(input: string): void;
}
  1. packages/main/src/plugin/extension-loader.ts 充当扩展加载器,定义了 API 所需的所有操作。修改它以在 foobar 命名空间 const 下添加 hello() 的主要功能。
// It's recommended you define a class that you retrieve from a separate file
// see Podman and Kubernetes examples for implementation.

// Add the class to the constructor of the extension loader
import type { FoobarClient } from './foobar';

export class ExtensionLoader {
// ...
constructor(
private foobarClient: FoobarClient,
// ...
) {}
// ..
}

// Initialize the 'foobar' client
const foobarClient = this.foobarClient;

// The "containerDesktopAPI.foobar" call is the namespace you previously defined within `extension-api.d.ts`
const foobar: typeof containerDesktopAPI.foobar = {

// Define the function that you are implementing and call the function from the class you created.
hello(input: string): void => {
return foobarClient.hello(input);
},
};

// Add 'foobar' to the list of configurations being returned by `return <typeof containerDesktopAPI>`
return <typeof containerDesktopAPI>{
foobar
};
  1. 在我们创建类之前,上面的代码将无法工作!因此,让我们创建一个具有该功能的 packages/main/src/plugin/foobar-client.ts 文件。
export class FoobarClient {
hello(input: string) {
console.log('hello ' + input);
}
}
  1. 需要在 packages/main/src/plugin/index.ts 中创建此类的实例并将其传递给 ExtensionLoader 的构造函数。
const foobarClient = new FoobarClient();
this.extensionLoader = new ExtensionLoader(
/* ... */
foobarClient,
);
  1. 在 package.json 中,您可以通过配置设置属性注册一些设置。

例如,如果您贡献一个名为 podman.binary.path 的属性,它将在 Podman Desktop UI 设置中显示 Path,如果您将其更改为 podman.binary.pathToBinary,它在标题中将变为 Path To Binary


"configuration": {
"title": "Podman",
"properties": {
"podman.binary.path": {
"name": "Path to Podman Binary",
"type": "string",
"format": "file",
"default": "",
"description": "Custom path to Podman binary (Default is blank)"
},
  1. 最后一步!从您的扩展中调用您正在实现的新 API 调用。
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
// Define the provider
const provider = extensionApi.provider.createProvider({
name: 'FooBar',
id: 'foobar',
status: 'unknown',
images: {
icon: './icon.png',
logo: './icon.png',
},
});

// Push the new provider to Podman Desktop
extensionContext.subscriptions.push(provider);

// Call the "hello world" function that'll output to the console
extensionContext.foobar.hello('world');
}

其他资源

  • 考虑使用像 RollupWebpack 这样的打包器来缩小构建产物的大小。

后续步骤