开发 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 将会:
- 搜索并加载扩展目录中
package.json
文件里main
条目指定的JavaScript
文件(通常是extension.js
)。 - 运行导出的
activate
函数。
停用
停用扩展时,Podman Desktop 将会:
- 运行可选导出的
deactivate
函数。 - 释放已添加到
extensionContext.subscriptions
的任何资源,请参阅 extension-loader.ts 中的deactivateExtension
。
示例样板代码
这是一个 extensions/foobar/src/extensions.ts
文件的示例,其中包含基本的 activate
和 deactivate
功能,前提是您已经创建了 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 中。
创建并运行您的扩展
您可以通过执行以下端到端任务来创建和运行扩展:
扩展您的扩展
以下是可以帮助您扩展扩展的文档和/或“样板”代码。
使用 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.ts 和 tray-menu.ts
extension-loader.ts
: 尝试加载扩展并相应地设置状态(started
、stopped
、starting
或stopping
)。如果发生未知错误,状态将设置为unknown
。extension-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”。
- 在命名空间下,将新函数添加到
/packages/extension-api/src/extension-api.d.ts
。这将使其在您的扩展中调用时可在 API 内访问。
export namespace foobar {
// ...
export function hello(input: string): void;
}
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
};
- 在我们创建类之前,上面的代码将无法工作!因此,让我们创建一个具有该功能的
packages/main/src/plugin/foobar-client.ts
文件。
export class FoobarClient {
hello(input: string) {
console.log('hello ' + input);
}
}
- 需要在
packages/main/src/plugin/index.ts
中创建此类的实例并将其传递给ExtensionLoader
的构造函数。
const foobarClient = new FoobarClient();
this.extensionLoader = new ExtensionLoader(
/* ... */
foobarClient,
);
- 在 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)"
},
- 最后一步!从您的扩展中调用您正在实现的新 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');
}