开发 Podman Desktop 扩展
Podman Desktop 经过精心设计,以便您可以以“扩展”的形式模块化地添加新功能,以及相应的扩展 API。这使您可以与 Podman Desktop 进行通信,而无需了解其内部机制。您只需要找到 API 调用,Podman Desktop 将完成剩下的工作。
建议使用 **TypeScript** 编写扩展,以进行类型检查,但扩展也可以使用 **JavaScript** 编写。
大多数扩展是外部加载的,但是,我们也对自己的 API 进行内部测试,将其作为 内部扩展 加载,这些扩展使用相同的 API。这些内部维护的扩展可以作为构建外部加载扩展的示例和基础。
创建新扩展的概述
我们尽力通过利用 `package.json` 并简化扩展内部的激活,通过仅提供两个入口点来简化扩展创建:`activate()` 和 `deactivate()`。
与 Podman Desktop 的所有功能也完全通过 `extension-api` 进行通信,它被加载为 `import * as extensionApi from '@podman-desktop/api';`。API 代码位于 此处,而该代码的网站表示位于 此处。
激活
激活扩展时,Podman Desktop 将
- 搜索并加载扩展目录(通常为 `extension.js`)中 `package.json` 文件的 `main` 入口中指定的 JavaScript 文件。
- 运行导出的 `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 中传播。
创建扩展
初始化扩展
我们尽可能地使用 `package.json`。我们从编写第一个 `package.json` 开始。
先决条件
- JavaScript 或 TypeScript
步骤
-
初始化 `package.json` 文件。
{}
-
将 TypeScript 和 Podman Desktop API 添加到开发依赖项中
"devDependencies": {
"@podman-desktop/api": "latest",
"typescript": "latest",
"vite": "latest"
}, -
添加必要的元数据
"name": "my-extension",
"displayName": "My Hello World extension",
"description": "How to write my first extension",
"version": "0.0.1",
"icon": "icon.png",
"publisher": "benoitf", -
添加可能运行此扩展的 Podman Desktop 版本
"engines": {
"podman-desktop": "latest"
}, -
添加主要入口点
"main": "./dist/extension.js"
-
添加一个 Hello World 命令贡献
"contributes": {
"commands": [
{
"command": "my.first.command",
"title": "My First Extension: Hello World"
}
]
} -
将 `icon.png` 文件添加到项目中。
验证
-
`package.json` 示例完整内容
{
"devDependencies": {
"@podman-desktop/api": "latest",
"typescript": "latest",
"vite": "latest"
},
"name": "my-extension",
"displayName": "My Hello World extension",
"description": "How to write my first extension",
"version": "0.0.1",
"icon": "icon.png",
"publisher": "benoitf",
"engines": {
"podman-desktop": "latest"
},
"scripts": {
"build": "vite build",
"test": "vitest run --coverage",
"test:watch": "vitest watch --coverage",
"watch": "vite build --watch"
},
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "my.first.command",
"title": "My First Extension: Hello World"
}
]
}
}
编写扩展入口点
编写扩展功能。
先决条件
- JavaScript 或 TypeScript
步骤
-
创建并编辑 `src/extension.ts` 文件。
-
导入 Podman Desktop API
import * as podmanDesktopAPI from '@podman-desktop/api';
-
导出 `activate` 函数,以便在激活时调用。
函数的签名可以是
-
同步的
export function activate(): void;
-
异步的
export async function activate(): Promise<void>;
-
-
(可选)将扩展上下文添加到 `activate` 函数中,使扩展能够注册可处置资源
export async function activate(extensionContext: podmanDesktopAPI.ExtensionContext): Promise<void> {}
-
注册命令和回调
import * as podmanDesktopAPI from '@podman-desktop/api';
export async function activate(extensionContext: podmanDesktopAPI.ExtensionContext): Promise<void> {
// register the command referenced in package.json file
const myFirstCommand = podmanDesktopAPI.commands.registerCommand('my.first.command', async () => {
// display a choice to the user for selecting some values
const result = await podmanDesktopAPI.window.showQuickPick(['un', 'deux', 'trois'], {
canPickMany: true, // user can select more than one choice
});
// display an information message with the user choice
await podmanDesktopAPI.window.showInformationMessage(`The choice was: ${result}`);
});
// create an item in the status bar to run our command
// it will stick on the left of the status bar
const item = podmanDesktopAPI.window.createStatusBarItem(podmanDesktopAPI.StatusBarAlignLeft, 100);
item.text = 'My first command';
item.command = 'my.first.command';
item.show();
// register disposable resources to it's removed when we deactivte the extension
extensionContext.subscriptions.push(myFirstCommand);
extensionContext.subscriptions.push(item);
} -
(可选)导出 `deactivate` 函数,以便在停用时调用。
函数的签名可以是
-
同步的
export function deactivate(): void;
-
异步的
export async function deactivate(): Promise<void>;
-
请记住,上面的示例并未完全表示扩展可以使用的所有功能。例如,还可以实现创建新的提供者、新的命令、扩展 Podman Desktop 内部功能。请参阅我们的 API 文档 以了解更多信息。
构建依赖项
此示例使用 TypeScript 和 Vite 进行构建,以下文件应位于您的扩展的根目录中。
创建一个名为 `tsconfig.json` 的文件,其内容如下
{
"compilerOptions": {
"module": "esnext",
"lib": ["ES2017"],
"sourceMap": true,
"rootDir": "src",
"outDir": "dist",
"target": "esnext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src", "types/*.d.ts"]
}
创建一个名为 `vite.config.js` 的文件,其内容如下
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { join } from 'path';
import { builtinModules } from 'module';
const PACKAGE_ROOT = __dirname;
/**
* @type {import('vite').UserConfig}
* @see https://vite.vuejs.ac.cn/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: process.cwd(),
resolve: {
alias: {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
build: {
sourcemap: 'inline',
target: 'esnext',
outDir: 'dist',
assetsDir: '.',
minify: process.env.MODE === 'production' ? 'esbuild' : false,
lib: {
entry: 'src/extension.ts',
formats: ['cjs'],
},
rollupOptions: {
external: ['@podman-desktop/api', ...builtinModules.flatMap(p => [p, `node:${p}`])],
output: {
entryFileNames: '[name].js',
},
},
emptyOutDir: true,
reportCompressedSize: false,
},
};
export default config;
验证
-
扩展会编译并生成位于 `dist` 文件夹中的输出。
-
所有运行时依赖项都包含在最终二进制文件中。
运行和调试扩展
先决条件
- JavaScript 或 TypeScript
- 这是 Podman Desktop 仓库的克隆。
步骤
要在加载扩展的情况下启动 Podman Desktop,请从 Podman Desktop 仓库的克隆中运行以下命令:
pnpm watch --extension-folder /path/to/your/extension
如果您创建了 Webview,可以通过以下方式调试/访问扩展的控制台:
- 点击侧边栏中的扩展图标。
- 右键单击并选择**打开 Webview 的开发工具**。
扩展您的扩展
以下是可帮助扩展您的扩展的文档和/或“样板”代码。
使用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
将反映为“已启动”。
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 中保持可持续性和一致性。在编写代码之前,在问题中进行讨论将很有帮助。
在此示例中,我们将添加一个新函数来在控制台中简单地显示:“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
命名空间常量下添加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');
}