Higress私有化部署访问 - Higress Plugin Server

217 阅读4分钟

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: "