diff --git a/.gitignore b/.gitignore index 32b1709..e73d0b6 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,5 @@ cython_debug/ # PyPI configuration file .pypirc .idea +/plugins/* +!/plugins/.gitkeep diff --git a/Dockerfile b/Dockerfile index e2b019d..0ef48bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,9 @@ ARG PYTHON_IMAGE=higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/python:3.11-alpine ARG NGINX_IMAGE=higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/nginx:alpine ARG ALPINE_MIRROR="" +ARG USE_LOCAL_PLUGINS=false -FROM $PYTHON_IMAGE AS builder +FROM $PYTHON_IMAGE AS builder-base # 配置 Alpine 镜像源(可选,本地构建时可指定国内镜像加速) ARG ALPINE_MIRROR @@ -26,14 +27,28 @@ RUN set -eux; \ && rm -rf /tmp/oras.tar.gz oras \ && oras version -# 创建工作目录 WORKDIR /workspace -# 复制脚本 +# 复制脚本和公共文件 COPY pull_plugins.py plugins.properties ./ -# 执行构建操作 -RUN python3 pull_plugins.py --download-v2 +FROM builder-base AS local-true +# 启用本地模式:复制本地插件源 +COPY plugins/ ./plugins/ + +FROM builder-base AS local-false +# 不启用本地模式:创建空的 plugins 目录 +RUN mkdir -p plugins + +FROM local-${USE_LOCAL_PLUGINS:-false} AS builder + +# 根据 USE_LOCAL_PLUGINS 决定是否启用本地 WASM 文件覆盖 +ARG USE_LOCAL_PLUGINS +RUN if [ "$USE_LOCAL_PLUGINS" = "true" ]; then \ + python3 pull_plugins.py --download-v2 --use-local; \ + else \ + python3 pull_plugins.py --download-v2; \ + fi # 运行阶段:最终镜像 FROM $NGINX_IMAGE diff --git a/README.md b/README.md index 45b1b78..3c27167 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,52 @@ MCP_SERVER_WASM_IMAGE_URL=http://higress-plugin-server.higress-system.svc/plugin > `higress-plugin-server.higress-system.svc` 替换为 Higress Gateway 可以用来访问插件服务器的地址。 +## 使用本地 WASM 文件 + +不想把 WASM 推送到 OCI 仓库时(自定义插件,或用自编译版本覆盖官方插件),可直接把 `plugin.wasm` 放进仓库的 `plugins/` 目录。 + +> ⚠️ **默认行为**:`USE_LOCAL_PLUGINS` 默认为 `false`,构建时按 `plugins.properties` 从 OCI 仓库下载所有插件,`plugins/` 目录被忽略。 +> 要启用本地 WASM 覆盖,需在构建时指定 `--build-arg USE_LOCAL_PLUGINS=true`。 + +启用本地模式后,`plugins/<插件名>/<版本>/plugin.wasm` 会随构建上下文带入;与本地同名同版本的 OCI 下载会被自动跳过,构建结束时为 `plugins/<插件名>/<版本>/` 下的 `plugin.wasm` 自动生成 `metadata.txt`。**无需修改 `plugins.properties`**——本地插件按目录约定自动识别。 + +### 启用本地 WASM 构建 + +```bash +docker build --build-arg USE_LOCAL_PLUGINS=true -t higress-plugin-server:1.0.0 -f Dockerfile . +``` + +也可与国内镜像加速参数组合使用: + +```bash +docker build \ + --build-arg ALPINE_MIRROR=mirrors.aliyun.com \ + --build-arg USE_LOCAL_PLUGINS=true \ + -t higress-plugin-server:1.0.0 \ + -f Dockerfile . +``` + +### 两种用法 + +1. **新增自定义插件**:在 `plugins/<插件名>/<版本>/` 下放置 `plugin.wasm`,不要写进 `plugins.properties`。它不会被下载,构建后自动生成 `metadata.txt` 并被 serve。需要哪些版本就放哪些版本目录。 + +2. **覆盖官方插件**:保留 `plugins.properties` 中该插件的 OCI 配置,同时把你的 `plugin.wasm` 放到同名同版本目录下。启用本地模式后脚本检测到本地副本会跳过下载。`Dockerfile` 构建时同时处理 `1.0.0` 和 `2.0.0` 两个版本,所以要覆盖哪个版本就把文件放进对应版本目录。 + +### 目录结构 + +按 `plugins/<插件名>/<版本>/plugin.wasm` 放置;`metadata.txt` 无需手动提供,构建时按实际文件自动(重)生成: + +``` +. ← 仓库根目录(构建上下文) +├── plugins.properties +└── plugins/ + └── my-plugin/ + ├── 1.0.0/ + │ └── plugin.wasm + └── 2.0.0/ + └── plugin.wasm +``` + ## 参考文档 - 如何修改内置插件镜像地址:https://higress.cn/docs/latest/ops/how-tos/builtin-plugin-url/ diff --git a/plugins/.gitkeep b/plugins/.gitkeep new file mode 100644 index 0000000..24a52c0 --- /dev/null +++ b/plugins/.gitkeep @@ -0,0 +1,4 @@ +# 此占位文件确保 plugins/ 目录被 git 跟踪,从而使 Dockerfile 的 +# "COPY plugins/ ./plugins/" 在全新克隆后仍有源目录可用。 +# 本地/自定义插件以 plugins/<插件名>/<版本>/plugin.wasm 形式放入此目录; +# 下载产物与 metadata.txt 由构建生成,不纳入版本控制(见 .gitignore)。 diff --git a/pull_plugins.py b/pull_plugins.py index 381ff51..b3fa733 100644 --- a/pull_plugins.py +++ b/pull_plugins.py @@ -93,54 +93,34 @@ def generate_metadata(plugin_dir, plugin_name): except Exception as e: print(f"生成元数据失败: {e}") -def main(): - parser = argparse.ArgumentParser(description='处理插件配置文件') - parser.add_argument('properties_path', nargs='?', default=None, - help='properties文件路径(默认:脚本所在目录下的plugins.properties)') - parser.add_argument('--download-v2', action='store_true', - help='是否下载 2.0.0 版本插件') - args = parser.parse_args() - - # 用户未提供路径时,使用默认逻辑 - if args.properties_path is None: - script_dir = os.path.dirname(os.path.abspath(__file__)) - default_path = os.path.join(script_dir, 'plugins.properties') - - args.properties_path = default_path - - base_path = os.path.dirname(args.properties_path) - properties = read_properties(args.properties_path) - - if not properties: - print("未找到有效的插件配置") - return - - failed_plugins = [] - - for plugin_name, plugin_url in properties.items(): - print(f"\n正在处理插件: {plugin_name}") - # 处理原始版本(1.0.0) - success = process_plugin(base_path, plugin_name, plugin_url, "1.0.0") - if not success: - failed_plugins.append(f"{plugin_name}:1.0.0") - - # 如果指定了 --download-v2 参数,则额外处理 2.0.0 版本 - if args.download_v2: - v2_url = plugin_url.replace(":1.0.0", ":2.0.0") - print(f"\n正在处理插件 {plugin_name} 的 2.0.0 版本") - success = process_plugin(base_path, plugin_name, v2_url, "2.0.0") - if not success: - failed_plugins.append(f"{plugin_name}:2.0.0") - - if failed_plugins: - print("\n以下插件未成功处理:") - for plugin in failed_plugins: - print(f"- {plugin}") - sys.exit(1) - -def process_plugin(base_path, plugin_name, plugin_url, version): +def generate_all_metadata(base_path): + """ + 统一生成 metadata.txt:扫描 plugins///plugin.wasm, + 覆盖 properties 中下载/跳过的插件、本地预置插件,以及未在 properties + 中定义的自定义插件。临时目录(__temp)与失败目录 + 因不匹配该路径模板,不会被遍历。 + """ + plugins_base_path = os.path.join(base_path, 'plugins') + if not os.path.isdir(plugins_base_path): + return 0 + count = 0 + for plugin_name in sorted(os.listdir(plugins_base_path)): + name_dir = os.path.join(plugins_base_path, plugin_name) + if not os.path.isdir(name_dir): + continue + for version in sorted(os.listdir(name_dir)): + plugin_dir = os.path.join(name_dir, version) + if os.path.isfile(os.path.join(plugin_dir, 'plugin.wasm')): + generate_metadata(plugin_dir, plugin_name) + count += 1 + print(f"共为 {count} 个插件目录生成 metadata.txt") + return count + +def process_plugin(base_path, plugin_name, plugin_url, version, use_local=False): """ - 处理单个插件下载和信息获取 + 处理单个 OCI 插件:把 plugin.wasm 放到 plugins///。 + 本地已预置(通过 Dockerfile 的 COPY plugins/)则跳过下载。 + metadata.txt 不在此处生成,由 generate_all_metadata() 统一处理。 """ plugins_base_path = os.path.join(base_path, 'plugins') os.makedirs(plugins_base_path, exist_ok=True) @@ -148,6 +128,11 @@ def process_plugin(base_path, plugin_name, plugin_url, version): plugin_dir = os.path.join(plugins_base_path, plugin_name, version) os.makedirs(plugin_dir, exist_ok=True) + # 只有 use_local=True 时才检查本地已预置的 plugin.wasm:跳过下载 + if use_local and os.path.isfile(os.path.join(plugin_dir, 'plugin.wasm')): + print(f"{plugin_name} ({version}) 检测到本地副本,跳过下载") + return True + temp_download_dir = os.path.join(plugins_base_path, f"{plugin_name}_{version}_temp") os.makedirs(temp_download_dir, exist_ok=True) @@ -191,13 +176,59 @@ def process_plugin(base_path, plugin_name, plugin_url, version): finally: shutil.rmtree(temp_download_dir, ignore_errors=True) - if wasm_found: - generate_metadata(plugin_dir, plugin_name) - else: + if not wasm_found: print(f"{plugin_name} ({version}) 未找到 .wasm 文件") shutil.rmtree(plugin_dir, ignore_errors=True) return wasm_found +def main(): + parser = argparse.ArgumentParser(description='处理插件配置文件') + parser.add_argument('properties_path', nargs='?', default=None, + help='properties文件路径(默认:脚本所在目录下的plugins.properties)') + parser.add_argument('--download-v2', action='store_true', + help='是否下载 2.0.0 版本插件') + parser.add_argument('--use-local', action='store_true', + help='启用本地 WASM 文件:plugins///plugin.wasm 存在时跳过 OCI 下载') + args = parser.parse_args() + + # 用户未提供路径时,使用默认逻辑 + if args.properties_path is None: + script_dir = os.path.dirname(os.path.abspath(__file__)) + args.properties_path = os.path.join(script_dir, 'plugins.properties') + + base_path = os.path.dirname(args.properties_path) + properties = read_properties(args.properties_path) + + if not properties: + print("未找到有效的插件配置") + return + + failed_plugins = [] + + for plugin_name, plugin_url in properties.items(): + print(f"\n正在处理插件: {plugin_name}") + # 处理原始版本(1.0.0) + success = process_plugin(base_path, plugin_name, plugin_url, "1.0.0", use_local=args.use_local) + if not success: + failed_plugins.append(f"{plugin_name}:1.0.0") + + # 如果指定了 --download-v2 参数,则额外处理 2.0.0 版本 + if args.download_v2: + v2_url = plugin_url.replace(":1.0.0", ":2.0.0") + print(f"\n正在处理插件 {plugin_name} 的 2.0.0 版本") + success = process_plugin(base_path, plugin_name, v2_url, "2.0.0", use_local=args.use_local) + if not success: + failed_plugins.append(f"{plugin_name}:2.0.0") + + # 统一生成 metadata.txt:覆盖下载的、本地预置的、以及未在 properties 中定义的自定义插件 + generate_all_metadata(base_path) + + if failed_plugins: + print("\n以下插件未成功处理:") + for plugin in failed_plugins: + print(f"- {plugin}") + sys.exit(1) + if __name__ == '__main__': - main() \ No newline at end of file + main()