1. 项目结构设计
目录结构:
higress-plugin-server/
├── Dockerfile
├── nginx.conf
├── scripts/
│ ├── pull_plugins.py
│ ├── generate_metadata.py
│ └── build.sh
├── plugins/
│ └── (插件文件将通过脚本下载)
├── deploy/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── configmap.yaml
├── helm/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ └── _helpers.tpl
└── .github/workflows/
├── build-and-push.yaml
└── sync-plugins.yaml
2. 核心代码实现
2.1 Dockerfile
FROM nginx:alpine
# 安装必要工具
RUN apk add --no-cache python3 py3-pip curl
# 复制配置文件
COPY nginx.conf /etc/nginx/nginx.conf
COPY scripts/ /scripts/
# 创建插件目录
RUN mkdir -p /usr/share/nginx/html/plugins
# 复制插件文件(如果存在)
COPY plugins/ /usr/share/nginx/html/plugins/
# 设置权限
RUN chmod +x /scripts/*.sh /scripts/*.py
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/plugins/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
2.2 Nginx 配置文件
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'"$request_time" "$upstream_response_time"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# 启用 gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
application/javascript
application/json
application/wasm
text/css
text/plain
text/xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
# 隐藏版本号
server_tokens off;
# 启用目录浏览
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
# 插件文件路径
location /plugins/ {
alias /usr/share/nginx/html/plugins/;
# 设置 WASM 文件的 MIME 类型
location ~* \.wasm$ {
add_header Content-Type application/wasm;
add_header Cache-Control "public, max-age=3600";
add_header Access-Control-Allow-Origin "*";
}
# 元数据文件
location ~* \.txt$ {
add_header Content-Type text/plain;
add_header Cache-Control "public, max-age=300";
}
}
# 健康检查端点
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# API 端点 - 插件列表
location /api/plugins {
default_type application/json;
content_by_lua_block {
-- 这里可以添加动态插件列表生成逻辑
ngx.say('{"status": "ok", "plugins": []}')
}
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
2.3 插件下载脚本
#!/usr/bin/env python3
# scripts/pull_plugins.py
import os
import sys
import json
import hashlib
import requests
import subprocess
from datetime import datetime
from pathlib import Path
class PluginDownloader:
def __init__(self, config_file="config.json"):
self.config = self.load_config(config_file)
self.plugins_dir = Path("/usr/share/nginx/html/plugins")
self.plugins_dir.mkdir(parents=True, exist_ok=True)
def load_config(self, config_file):
"""加载配置文件"""
default_config = {
"registry": "higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins",
"plugins": [
{"name": "ai-proxy", "versions": ["1.0.0", "1.1.0"]},
{"name": "basic-auth", "versions": ["1.0.0"]},
{"name": "key-auth", "versions": ["1.0.0"]},
{"name": "jwt-auth", "versions": ["1.0.0"]},
{"name": "bot-detect", "versions": ["1.0.0"]},
{"name": "custom-response", "versions": ["1.0.0"]},
{"name": "key-rate-limit", "versions": ["1.0.0"]},
{"name": "request-block", "versions": ["1.0.0"]}
]
}
if os.path.exists(config_file):
with open(config_file, 'r') as f:
return json.load(f)
return default_config
def download_from_oci(self, plugin_name, version):
"""从 OCI 仓库下载插件"""
try:
# 使用 crane 或 docker 命令下载 OCI 镜像
image_url = f"{self.config['registry']}/{plugin_name}:{version}"
temp_dir = f"/tmp/{plugin_name}-{version}"
# 创建临时目录
os.makedirs(temp_dir, exist_ok=True)
# 下载镜像内容
cmd = [
"docker", "run", "--rm", "-v", f"{temp_dir}:/output",
image_url, "cp", "/plugin.wasm", "/output/"
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Failed to download {plugin_name}:{version} - {result.stderr}")
return None
wasm_file = os.path.join(temp_dir, "plugin.wasm")
if os.path.exists(wasm_file):
return wasm_file
except Exception as e:
print(f"Error downloading {plugin_name}:{version}: {e}")
return None
def calculate_md5(self, file_path):
"""计算文件 MD5"""
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def generate_metadata(self, plugin_name, version, wasm_file):
"""生成插件元数据"""
file_stat = os.stat(wasm_file)
md5_hash = self.calculate_md5(wasm_file)
metadata = {
"plugin_name": plugin_name,
"version": version,
"size": file_stat.st_size,
"last_modified": datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
"created": datetime.now().isoformat(),
"md5": md5_hash,
"download_url": f"/plugins/{plugin_name}/{version}/plugin.wasm"
}
return metadata
def save_plugin(self, plugin_name, version, wasm_file):
"""保存插件到目标目录"""
plugin_dir = self.plugins_dir / plugin_name / version
plugin_dir.mkdir(parents=True, exist_ok=True)
# 复制 WASM 文件
target_wasm = plugin_dir / "plugin.wasm"
subprocess.run(["cp", wasm_file, str(target_wasm)])
# 生成元数据
metadata = self.generate_metadata(plugin_name, version, str(target_wasm))
# 保存元数据文件
metadata_file = plugin_dir / "metadata.json"
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
# 保存文本格式元数据(兼容性)
metadata_txt = plugin_dir / "metadata.txt"
with open(metadata_txt, 'w') as f:
f.write(f"Plugin Name: {metadata['plugin_name']}\n")
f.write(f"Version: {metadata['version']}\n")
f.write(f"Size: {metadata['size']} bytes\n")
f.write(f"Last Modified: {metadata['last_modified']}\n")
f.write(f"Created: {metadata['created']}\n")
f.write(f"MD5: {metadata['md5']}\n")
print(f"Saved {plugin_name}:{version} to {plugin_dir}")
return True
def download_all_plugins(self):
"""下载所有配置的插件"""
success_count = 0
total_count = 0
for plugin_config in self.config["plugins"]:
plugin_name = plugin_config["name"]
versions = plugin_config["versions"]
for version in versions:
total_count += 1
print(f"Downloading {plugin_name}:{version}...")
wasm_file = self.download_from_oci(plugin_name, version)
if wasm_file and self.save_plugin(plugin_name, version, wasm_file):
success_count += 1
# 清理临时文件
os.remove(wasm_file)
print(f"Download completed: {success_count}/{total_count} plugins downloaded successfully")
return success_count == total_count
if __name__ == "__main__":
downloader = PluginDownloader()
success = downloader.download_all_plugins()
sys.exit(0 if success else 1)
2.4 元数据生成脚本
#!/usr/bin/env python3
# scripts/generate_metadata.py
import os
import json
import hashlib
from datetime import datetime
from pathlib import Path
def calculate_md5(file_path):
"""计算文件 MD5"""
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def generate_plugin_metadata(plugins_dir="/usr/share/nginx/html/plugins"):
"""为所有插件生成元数据"""
plugins_path = Path(plugins_dir)
all_plugins = []
for plugin_dir in plugins_path.iterdir():
if not plugin_dir.is_dir():
continue
plugin_name = plugin_dir.name
plugin_versions = []
for version_dir in plugin_dir.iterdir():
if not version_dir.is_dir():
continue
version = version_dir.name
wasm_file = version_dir / "plugin.wasm"
if wasm_file.exists():
file_stat = wasm_file.stat()
md5_hash = calculate_md5(str(wasm_file))
version_metadata = {
"version": version,
"size": file_stat.st_size,
"last_modified": datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
"md5": md5_hash,
"download_url": f"/plugins/{plugin_name}/{version}/plugin.wasm"
}
plugin_versions.append(version_metadata)
# 保存单个版本的元数据
metadata_file = version_dir / "metadata.json"
with open(metadata_file, 'w') as f:
json.dump(version_metadata, f, indent=2)
if plugin_versions:
plugin_metadata = {
"name": plugin_name,
"versions": plugin_versions,
"latest_version": max(plugin_versions, key=lambda x: x["version"])["version"]
}
all_plugins.append(plugin_metadata)
# 生成全局插件索引
global_metadata = {
"generated_at": datetime.now().isoformat(),
"total_plugins": len(all_plugins),
"plugins": all_plugins
}
index_file = plugins_path / "index.json"
with open(index_file, 'w') as f:
json.dump(global_metadata, f, indent=2)
print(f"Generated metadata for {len(all_plugins)} plugins")
return global_metadata
if __name__ == "__main__":
generate_plugin_metadata()
3. Helm Chart 集成
3.1 在 Higress 主 Chart 中添加 Plugin Server 配置
在 中添加 plugin-server 配置:
# 在 global 部分添加
global:
# ... 现有配置 ...
# -- Whether to enable Plugin Server for Higress, default is false.
enablePluginServer: false
# -- Plugin Server configuration
pluginServer:
# -- Plugin Server URL, if empty will use internal service
url: ""
# -- Default plugin pull policy
imagePullPolicy: "