跳至主要内容

开发 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 将

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

停用

停用扩展时,Podman Desktop 将

  1. 运行(可选)导出的 `deactivate` 函数。
  2. 处理已添加到 `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

步骤

  1. 初始化 `package.json` 文件。

    {}
  2. 将 TypeScript 和 Podman Desktop API 添加到开发依赖项中

     "devDependencies": {
    "@podman-desktop/api": "latest",
    "typescript": "latest",
    "vite": "latest"
    },
  3. 添加必要的元数据

      "name": "my-extension",
    "displayName": "My Hello World extension",
    "description": "How to write my first extension",
    "version": "0.0.1",
    "icon": "icon.png",
    "publisher": "benoitf",
  4. 添加可能运行此扩展的 Podman Desktop 版本

      "engines": {
    "podman-desktop": "latest"
    },
  5. 添加主要入口点

     "main": "./dist/extension.js"
  6. 添加一个 Hello World 命令贡献

      "contributes": {
    "commands": [
    {
    "command": "my.first.command",
    "title": "My First Extension: Hello World"
    }
    ]
    }
  7. 将 `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

步骤

  1. 创建并编辑 `src/extension.ts` 文件。

  2. 导入 Podman Desktop API

    import * as podmanDesktopAPI from '@podman-desktop/api';
  3. 导出 `activate` 函数,以便在激活时调用。

    函数的签名可以是

    • 同步的

      export function activate(): void;
    • 异步的

      export async function activate(): Promise<void>;
  4. (可选)将扩展上下文添加到 `activate` 函数中,使扩展能够注册可处置资源

    export async function activate(extensionContext: podmanDesktopAPI.ExtensionContext): Promise<void> {}
  5. 注册命令和回调

    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);
    }
  6. (可选)导出 `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` 文件夹中的输出。

  • 所有运行时依赖项都包含在最终二进制文件中。

运行和调试扩展

先决条件

步骤

要在加载扩展的情况下启动 Podman Desktop,请从 Podman Desktop 仓库的克隆中运行以下命令:

pnpm watch --extension-folder /path/to/your/extension

如果您创建了 Webview,可以通过以下方式调试/访问扩展的控制台:

  1. 点击侧边栏中的扩展图标。
  2. 右键单击并选择**打开 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.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 中保持可持续性和一致性。在编写代码之前,在问题中进行讨论将很有帮助。

在此示例中,我们将添加一个新函数来在控制台中简单地显示:“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 命名空间常量下添加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.tsExtensionLoader 的构造函数。
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 之类的打包程序来缩小工件的大小。

下一步