Files
agent_management/k8s_manager.py
T

1316 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Kubernetes管理模块 - 负责与Kubernetes集群交互
"""
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from typing import Dict, List, Optional
import logging
import os
import re
import requests
import time
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def sanitize_k8s_name(name: str, max_length: int = 63) -> str:
"""将名称转换为 DNS-1035 合规格式
Kubernetes Service、Deployment 等资源名称必须符合 DNS-1035 标准:
- 只能包含小写字母、数字和连字符 '-'
- 必须以字母开头
- 必须以字母或数字结尾
- 最长 63 个字符
Args:
name: 原始名称
max_length: 最大长度(默认 63)
Returns:
合规的 K8s 资源名称
"""
# 转小写
name = name.lower()
# 将下划线和空格替换为连字符
name = name.replace("_", "-").replace(" ", "-")
# 移除非字母、数字、连字符的字符
name = re.sub(r'[^a-z0-9-]', '', name)
# 合并连续的连字符
name = re.sub(r'-+', '-', name)
# 如果以数字开头,添加 'a' 前缀
if name and name[0].isdigit():
name = 'a' + name
# 如果以连字符开头,去掉
name = name.lstrip('-')
# 截断到最大长度
name = name[:max_length]
# 去掉末尾的连字符
name = name.rstrip('-')
# 最终兜底:如果名称为空
if not name:
name = 'agent'
return name
class K8sManager:
"""Kubernetes资源管理器"""
# Azure DNS 配置(从环境变量读取)
AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID", "263c3ff6-1be5-4141-8308-b188464fb297")
AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID", "c5ba26db-f180-425f-bac3-93708d853988")
AZURE_CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET", "ydt8Q~DIOfKXDUOJkzfgaNVNh2d1VrbeIaYuKbpD")
AZURE_SUBSCRIPTION_ID = os.getenv("AZURE_SUBSCRIPTION_ID", "c6c47e4c-f5f4-49f8-b26f-7728862c17d6")
AZURE_RESOURCE_GROUP = os.getenv("AZURE_RESOURCE_GROUP", "taiji-ai-v0")
AZURE_DNS_ZONE = os.getenv("AZURE_DNS_ZONE", "taijiagnet.com")
def __init__(self, namespace: str = "ai-agents", kubeconfig_path: str = None):
"""
初始化Kubernetes管理器
Args:
namespace: AI Agent部署的命名空间
kubeconfig_path: kubeconfig文件路径(可选)
"""
self.namespace = namespace
self._load_kube_config(kubeconfig_path)
self.v1 = client.CoreV1Api()
self.apps_v1 = client.AppsV1Api()
self.networking_v1 = client.NetworkingV1Api()
# 确保命名空间存在
self._ensure_namespace()
def _load_kube_config(self, kubeconfig_path: str = None):
"""
加载Kubernetes配置
优先级:
1. 集群内ServiceAccount(推荐用于生产环境)
2. 指定的kubeconfig文件路径
3. 默认的kubeconfig路径 (~/.kube/config)
"""
try:
# 方式1: 尝试加载集群内配置(当服务运行在K8s中且有ServiceAccount时)
config.load_incluster_config()
logger.info("✅ 使用集群内ServiceAccount配置")
except config.ConfigException:
try:
if kubeconfig_path and os.path.exists(kubeconfig_path):
# 方式2: 使用指定的kubeconfig文件
config.load_kube_config(config_file=kubeconfig_path)
logger.info(f"✅ 使用指定的kubeconfig: {kubeconfig_path}")
else:
# 方式3: 使用默认kubeconfig(开发环境)
config.load_kube_config()
logger.info("✅ 使用默认kubeconfig (~/.kube/config)")
except Exception as e:
logger.error(f"❌ 无法加载Kubernetes配置: {e}")
raise Exception(f"Kubernetes配置加载失败: {e}")
def _ensure_namespace(self):
"""确保AI Agent命名空间存在"""
try:
self.v1.read_namespace(name=self.namespace)
logger.info(f"命名空间 {self.namespace} 已存在")
except ApiException as e:
if e.status == 404:
# 创建命名空间
namespace_manifest = client.V1Namespace(
metadata=client.V1ObjectMeta(name=self.namespace)
)
self.v1.create_namespace(body=namespace_manifest)
logger.info(f"创建命名空间 {self.namespace}")
else:
raise
def create_agent_namespace(self, agent_name: str, owner_id: str = None) -> str:
"""为 Agent 创建独立的命名空间
Args:
agent_name: Agent 名称
owner_id: 所有者 ID
Returns:
创建的命名空间名称
"""
# 生成命名空间名称(使用 agent-{sanitized_name} 格式,确保 DNS 合规)
sanitized_name = sanitize_k8s_name(agent_name)
namespace_name = f"agent-{sanitized_name}"[:63].rstrip('-')
try:
# 检查命名空间是否已存在
self.v1.read_namespace(name=namespace_name)
logger.info(f"命名空间 {namespace_name} 已存在")
return namespace_name
except ApiException as e:
if e.status == 404:
# 创建命名空间
labels = {
"managed-by": "agent-manager",
"agent-name": agent_name
}
if owner_id:
labels["owner-id"] = owner_id
namespace_manifest = client.V1Namespace(
metadata=client.V1ObjectMeta(
name=namespace_name,
labels=labels
)
)
self.v1.create_namespace(body=namespace_manifest)
logger.info(f"✅ 创建命名空间 {namespace_name}")
# 复制 ACR secret 到新命名空间
self._copy_acr_secret_to_namespace(namespace_name)
return namespace_name
else:
raise
def _copy_acr_secret_to_namespace(self, target_namespace: str):
"""复制所有 ACR secrets 到目标命名空间
Args:
target_namespace: 目标命名空间
"""
# 需要复制的 ACR secrets 列表
acr_secrets = ["acr-secret", "openclaw-acr-secret"]
for secret_name in acr_secrets:
try:
# 从 agent-manager 命名空间读取 secret
source_secret = self.v1.read_namespaced_secret(
name=secret_name,
namespace="agent-manager"
)
# 创建新的 secret(去除自动生成的字段)
new_secret = client.V1Secret(
metadata=client.V1ObjectMeta(
name=secret_name,
namespace=target_namespace
),
data=source_secret.data,
type=source_secret.type
)
# 在目标命名空间创建 secret
self.v1.create_namespaced_secret(
namespace=target_namespace,
body=new_secret
)
logger.info(f"✅ 已复制 {secret_name} 到命名空间 {target_namespace}")
except ApiException as e:
if e.status == 404:
logger.warning(f"⚠️ 源 {secret_name} 不存在,跳过复制")
elif e.status == 409:
logger.info(f"{secret_name} 已存在于命名空间 {target_namespace}")
else:
logger.error(f"❌ 复制 {secret_name} 失败: {e}")
def create_service(self, service_name: str, namespace: str, pod_selector: Dict[str, str],
service_port: int, target_port: int) -> Dict:
"""为 Agent Pod 创建 LoadBalancer Service
Args:
service_name: Service 名称
namespace: 命名空间
pod_selector: Pod 选择器标签
service_port: Service 端口
target_port: Pod 目标端口
Returns:
创建的 Service 信息(包含外网 IP)
"""
try:
service_manifest = client.V1Service(
metadata=client.V1ObjectMeta(
name=service_name,
namespace=namespace,
labels={"managed-by": "agent-manager"}
),
spec=client.V1ServiceSpec(
selector=pod_selector,
ports=[
client.V1ServicePort(
name="http",
protocol="TCP",
port=service_port,
target_port=target_port
)
],
type="LoadBalancer"
)
)
response = self.v1.create_namespaced_service(
namespace=namespace,
body=service_manifest
)
logger.info(f"✅ LoadBalancer Service {service_name} 创建成功 (namespace: {namespace})")
# 获取外网 IP(可能需要等待分配)
external_ip = None
if response.status.load_balancer.ingress:
lb_ingress = response.status.load_balancer.ingress[0]
external_ip = lb_ingress.ip or lb_ingress.hostname
return {
"name": response.metadata.name,
"namespace": response.metadata.namespace,
"cluster_ip": response.spec.cluster_ip,
"port": service_port,
"external_ip": external_ip,
"type": "LoadBalancer"
}
except ApiException as e:
logger.error(f"创建 Service 失败: {e}")
raise Exception(f"创建 Service 失败: {e.reason}")
def create_ingress(self, ingress_name: str, namespace: str, service_name: str,
service_port: int, host: str = None, path: str = "/") -> Dict:
"""为 Agent Service 创建 Ingress
Args:
ingress_name: Ingress 名称
namespace: 命名空间
service_name: 后端 Service 名称
service_port: Service 端口
host: 域名(可选,如果为 None 则使用默认)
path: 路径前缀
Returns:
创建的 Ingress 信息
"""
try:
# 构建路径规则
http_ingress_path = client.V1HTTPIngressPath(
path=path,
path_type="Prefix",
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
name=service_name,
port=client.V1ServiceBackendPort(number=service_port)
)
)
)
# 构建规则
ingress_rule = client.V1IngressRule(
http=client.V1HTTPIngressRuleValue(paths=[http_ingress_path])
)
# 如果指定了 host,添加到规则中
if host:
ingress_rule.host = host
# 创建 Ingress manifest
ingress_manifest = client.V1Ingress(
metadata=client.V1ObjectMeta(
name=ingress_name,
namespace=namespace,
labels={"managed-by": "agent-manager"},
annotations={
"nginx.ingress.kubernetes.io/rewrite-target": "/",
"nginx.ingress.kubernetes.io/ssl-redirect": "false"
}
),
spec=client.V1IngressSpec(
ingress_class_name="nginx", # 使用 nginx ingress controller
rules=[ingress_rule]
)
)
response = self.networking_v1.create_namespaced_ingress(
namespace=namespace,
body=ingress_manifest
)
logger.info(f"✅ Ingress {ingress_name} 创建成功 (namespace: {namespace})")
# 获取 Ingress IP/域名
ingress_url = None
if response.status.load_balancer.ingress:
lb_ingress = response.status.load_balancer.ingress[0]
if lb_ingress.ip:
ingress_url = f"http://{lb_ingress.ip}{path}"
elif lb_ingress.hostname:
ingress_url = f"http://{lb_ingress.hostname}{path}"
return {
"name": response.metadata.name,
"namespace": response.metadata.namespace,
"host": host,
"path": path,
"url": ingress_url,
"note": "Ingress URL 将在负载均衡器配置完成后可用"
}
except ApiException as e:
logger.error(f"创建 Ingress 失败: {e}")
raise Exception(f"创建 Ingress 失败: {e.reason}")
# 模板端口映射
TEMPLATE_PORTS = {
"echo_agent": 8000,
"search_agent": 8080,
"search_agent_a2a": 8080,
"search_agent_mcp": 8080,
"mysql_agent": 8000,
"postgresql_agent": 8000,
"jina_search_agent": 8000,
"azure_blob_agent": 8000,
"azure_blob_agent_mcp": 8000,
"azure_blob_agent_a2a": 8000,
"a2a_litellm_agent": 8000,
"code_ai_agent": 8000,
"facebook_agent": 8000,
"media_downloader": 8000,
"content_analyzer": 8000,
"huoke": 8000,
"microsoft_learn_agent": 8000,
"aws_docs_mcp": 8000,
"google_mcp": 8000,
}
# 模板所需环境变量说明
TEMPLATE_ENV_INFO = {
"jina_search_agent": {
"required": {
"JINA_API_KEY": "Jina API密钥,从 https://jina.ai/ 获取"
},
"optional": {
"SERVICE_PORT": "HTTP服务端口,默认8000",
"SERVICE_HOST": "HTTP服务监听地址,默认0.0.0.0"
}
},
"mysql_agent": {
"required": {
"MYSQL_HOST": "MySQL数据库主机地址",
"MYSQL_USER": "MySQL用户名",
"MYSQL_PASSWORD": "MySQL密码",
"MYSQL_DATABASE": "MySQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"MYSQL_PORT": "MySQL端口,默认3306"
}
},
"postgresql_agent": {
"required": {
"POSTGRES_HOST": "PostgreSQL数据库主机地址",
"POSTGRES_USER": "PostgreSQL用户名",
"POSTGRES_PASSWORD": "PostgreSQL密码",
"POSTGRES_DATABASE": "PostgreSQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"POSTGRES_PORT": "PostgreSQL端口,默认5432"
}
},
"azure_blob_agent": {
"required": {
"LITELLM_API_BASE": "LiteLLM服务地址,如 http://litellm-service:4000",
"LITELLM_MODEL": "使用的LLM模型,如 gpt-3.5-turbo",
"LITELLM_API_KEY": "LiteLLM API密钥"
},
"optional": {
"AZURE_STORAGE_CONNECTION_STRING": "Azure Storage连接字符串(可选,也可通过 /connect API 动态传入)",
"SERVICE_PORT": "HTTP服务端口,默认8000",
"SERVICE_HOST": "HTTP服务监听地址,默认0.0.0.0"
}
},
"azure_blob_agent_mcp": {
"required": {
"MODEL_PROVIDER": "模型提供商,如 openai, azure-openai",
"MODEL_NAME": "使用的模型名称,如 gpt-4",
"MODEL_API_KEY": "模型 API 密钥"
},
"optional": {
"AZURE_STORAGE_CONNECTION_STRING": "Azure Storage连接字符串",
"TOOLS_CONFIG": "工具配置 JSON",
"TOOL_ENDPOINT": "外部工具端点",
"TOOL_API_KEY": "工具 API 密钥",
"MODEL_ENDPOINT": "模型 API 端点",
"STORAGE_ACCOUNT_NAME": "存储账户名称",
"USER_ID": "用户标识",
"TENANT_ID": "租户标识",
"NAMESPACE": "Kubernetes 命名空间"
}
},
"azure_blob_agent_a2a": {
"required": {
"MODEL_PROVIDER": "模型提供商,如 openai, azure-openai",
"MODEL_NAME": "使用的模型名称,如 gpt-4",
"MODEL_API_KEY": "模型 API 密钥",
"AGENT_ID": "Agent 唯一标识",
"AGENT_ROLE": "Agent 角色"
},
"optional": {
"AZURE_STORAGE_CONNECTION_STRING": "Azure Storage连接字符串",
"TOOLS_CONFIG": "工具配置 JSON",
"TOOL_ENDPOINT": "外部工具端点",
"TOOL_API_KEY": "工具 API 密钥",
"MODEL_ENDPOINT": "模型 API 端点",
"STORAGE_ACCOUNT_NAME": "存储账户名称",
"AGENT_CAPABILITIES": "Agent 能力列表 JSON",
"USER_ID": "用户标识",
"TENANT_ID": "租户标识",
"NAMESPACE": "Kubernetes 命名空间"
}
},
"search_agent": {
"required": {
"LLM_BASE_URL": "LLM 服务地址,如 https://api.openai.com/v1",
"SERPER_API_KEY": "Serper 搜索 API 密钥,从 https://serper.dev 获取",
"JINA_API_KEY": "Jina Reader API 密钥,从 https://jina.ai 获取"
},
"optional": {
"LLM_API_KEY": "LLM API 密钥(可在搜索请求中传入)",
"MODEL_NAME": "LLM 模型名称,默认 gpt-4o-mini",
"MAX_ITERATIONS": "最大搜索迭代次数,默认 3",
"MAX_RESULTS_PER_QUERY": "每次搜索最大结果数,默认 10",
"CONTENT_MAX_LENGTH": "内容最大长度,默认 5000",
"TIMEOUT": "超时时间(秒),默认 30",
"SERVICE_PORT": "HTTP服务端口,默认 8080",
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
}
},
"search_agent_a2a": {
"required": {
"LLM_BASE_URL": "LLM 服务地址,如 https://api.openai.com/v1",
},
"optional": {
"LITELLM_API_KEY": "LiteLLM API 密钥(可在请求中传入)",
"LLM_API_KEY": "LLM API 密钥(备选,可在请求中传入)",
"MODEL_NAME": "LLM 模型名称(优先)",
"LLM_MODEL": "LLM 模型名称(备选)",
"LITELLM_MODEL": "LiteLLM 模型名称(备选)",
"SERPER_API_KEY": "Serper 搜索 API 密钥(已内置默认值)",
"JINA_API_KEY": "Jina Reader API 密钥(已内置默认值)",
"SERVICE_PORT": "HTTP服务端口,默认 8080",
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
},
"description": "A2A 协议搜索 Agent,支持通过请求动态传入 API key"
},
"search_agent_mcp": {
"required": {
"LLM_BASE_URL": "LLM 服务地址,如 https://api.openai.com/v1",
},
"optional": {
"LITELLM_API_KEY": "LiteLLM API 密钥(可在请求中传入)",
"LLM_API_KEY": "LLM API 密钥(备选,可在请求中传入)",
"MODEL_NAME": "LLM 模型名称(优先)",
"LLM_MODEL": "LLM 模型名称(备选)",
"LITELLM_MODEL": "LiteLLM 模型名称(备选)",
"SERPER_API_KEY": "Serper 搜索 API 密钥(已内置默认值)",
"JINA_API_KEY": "Jina Reader API 密钥(已内置默认值)",
"SERVICE_PORT": "HTTP服务端口,默认 8080",
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
},
"description": "MCP 协议搜索 Agent,支持通过请求动态传入 API key"
},
"a2a_litellm_agent": {
"required": {
"LITELLM_API_BASE": "LiteLLM 服务地址",
"LITELLM_MODEL": "LiteLLM 模型名称"
},
"optional": {
"LITELLM_API_KEY": "LiteLLM API 密钥(可在请求中传入)",
"AGENT_NAME": "Agent 名称",
"AGENT_DESCRIPTION": "Agent 描述",
"SERVICE_PORT": "HTTP服务端口,默认 8000",
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
}
},
"code_ai_agent": {
"required": {
"LLM_BASE_URL": "LLM 服务地址,如 https://api.openai.com/v1",
},
"optional": {
"LLM_API_KEY": "LLM API 密钥(可在请求中传入)",
"MODEL_NAME": "LLM 模型名称",
"API_HOST": "API 服务监听地址,默认 0.0.0.0",
"API_PORT": "API 服务端口,默认 8000",
"PROJECTS_DIR": "项目存储目录,默认 /tmp/projects"
},
"description": "代码助手 Agent,支持代码生成、分析和执行"
},
"facebook_agent": {
"required": {
"LITELLM_GATEWAY_URL": "LiteLLM Gateway 服务地址,如 https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/",
"FACEBOOK_API_KEY": "Facebook RapidAPI 密钥"
},
"optional": {
"LITELLM_API_KEY": "LiteLLM API 密钥(可在请求中传入)",
"LITELLM_MODEL": "LiteLLM 模型名称,默认 taiji/gpt-4o-mini",
"FACEBOOK_API_HOST": "Facebook API 主机,默认 facebook-scraper3.p.rapidapi.com",
"FACEBOOK_MCP_URL": "Facebook MCP URL,默认 https://mcp.rapidapi.com",
"MAX_RESULTS": "最大搜索结果数,默认 10",
"TIMEOUT": "超时时间(秒),默认 30",
"LOG_LEVEL": "日志级别,默认 INFO"
},
"description": "Facebook 搜索智能 Agent,支持 Facebook 内容搜索和 MCP 协议"
}
}
def get_template_info(self, template: str) -> Dict:
"""
获取模板信息(从数据库动态获取)
Args:
template: 模板类型
Returns:
模板信息(端口、所需环境变量等)
"""
from template_manager import template_manager as tm
template_data = tm.get_template(template)
if template_data:
return {
"template": template,
"name": template_data.get("name"),
"display_name": template_data.get("display_name"),
"description": template_data.get("description"),
"port": template_data.get("port", 8000),
"image": template_data.get("image"),
"agent_framework": template_data.get("agent_framework"),
"env_requirements": template_data.get("env_requirements", {}),
# 回退到硬编码的环境变量信息(兼容旧模板)
"env_info": self.TEMPLATE_ENV_INFO.get(template, template_data.get("env_requirements", {}))
}
# 回退到硬编码配置(兼容性)
return {
"template": template,
"port": self.TEMPLATE_PORTS.get(template),
"env_info": self.TEMPLATE_ENV_INFO.get(template, {})
}
def create_pod(self, pod_name: str, template: str, config_data: Dict) -> Dict:
"""
创建Pod
Args:
pod_name: Pod名称
template: 模板类型(echo_agent, chat_agent等)
config_data: 配置信息(replicas, resources等)
Returns:
创建的Pod信息,包含访问地址
"""
try:
# 生成Pod规格
pod_manifest = self._generate_pod_manifest(pod_name, template, config_data)
# 创建Pod
response = self.v1.create_namespaced_pod(
namespace=self.namespace,
body=pod_manifest
)
logger.info(f"Pod {pod_name} 创建成功")
# 获取服务端口(从数据库动态获取)
from template_manager import template_manager as tm
service_port = tm.get_port(template)
result = {
"name": response.metadata.name,
"namespace": response.metadata.namespace,
"status": response.status.phase,
"created_at": response.metadata.creation_timestamp.isoformat() if response.metadata.creation_timestamp else None,
"template": template
}
# 如果是HTTP服务类型的agent,添加访问信息
if service_port:
result["service_port"] = service_port
result["access_info"] = {
"note": "Pod IP将在Pod运行后可用,请通过 /agents/{name}/status 获取",
"port": service_port,
"endpoints": {
"root": f"http://<pod_ip>:{service_port}/",
"health": f"http://<pod_ip>:{service_port}/health"
}
}
return result
except ApiException as e:
logger.error(f"创建Pod失败: {e}")
raise Exception(f"创建Pod失败: {e.reason}")
def _generate_pod_manifest(self, pod_name: str, template: str, config_data: Dict) -> client.V1Pod:
"""生成Pod配置清单"""
# 默认资源配置
replicas = config_data.get("replicas", 1)
cpu_request = config_data.get("cpu_request", "100m")
cpu_limit = config_data.get("cpu_limit", "500m")
memory_request = config_data.get("memory_request", "128Mi")
memory_limit = config_data.get("memory_limit", "512Mi")
# 从数据库动态获取模板镜像(延迟导入避免循环依赖)
from template_manager import template_manager as tm
image = tm.get_image(template)
# 构建环境变量列表
env_vars = [
client.V1EnvVar(name="POD_NAME", value=pod_name),
client.V1EnvVar(name="TEMPLATE_TYPE", value=template)
]
# NEW: 添加 Agent 框架配置
agent_framework = config_data.get("agent_framework", "langchain")
env_vars.append(client.V1EnvVar(name="AGENT_FRAMEWORK", value=agent_framework))
# NEW: 添加工具配置
if "tools_config" in config_data:
import json
env_vars.append(client.V1EnvVar(
name="TOOLS_CONFIG",
value=json.dumps(config_data["tools_config"])
))
if "tool_endpoint" in config_data:
env_vars.append(client.V1EnvVar(name="TOOL_ENDPOINT", value=config_data["tool_endpoint"]))
if "tool_api_key" in config_data:
env_vars.append(client.V1EnvVar(name="TOOL_API_KEY", value=config_data["tool_api_key"]))
# NEW: 添加模型配置
if "model_provider" in config_data:
env_vars.append(client.V1EnvVar(name="MODEL_PROVIDER", value=config_data["model_provider"]))
if "model_name" in config_data:
env_vars.append(client.V1EnvVar(name="MODEL_NAME", value=config_data["model_name"]))
if "model_endpoint" in config_data:
env_vars.append(client.V1EnvVar(name="MODEL_ENDPOINT", value=config_data["model_endpoint"]))
if "model_api_key" in config_data:
env_vars.append(client.V1EnvVar(name="MODEL_API_KEY", value=config_data["model_api_key"]))
# NEW: 添加存储配置
if "storage_connection_string" in config_data:
env_vars.append(client.V1EnvVar(
name="AZURE_STORAGE_CONNECTION_STRING",
value=config_data["storage_connection_string"]
))
if "storage_account_name" in config_data:
env_vars.append(client.V1EnvVar(name="STORAGE_ACCOUNT_NAME", value=config_data["storage_account_name"]))
# NEW: 添加用户标识
if "user_id" in config_data:
env_vars.append(client.V1EnvVar(name="USER_ID", value=config_data["user_id"]))
if "tenant_id" in config_data:
env_vars.append(client.V1EnvVar(name="TENANT_ID", value=config_data["tenant_id"]))
# NEW: 添加命名空间信息
namespace = config_data.get("namespace", self.namespace)
env_vars.append(client.V1EnvVar(name="NAMESPACE", value=namespace))
# 为特定模板添加默认环境变量
template_defaults = {
"facebook_agent": {
"LITELLM_GATEWAY_URL": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io",
"LITELLM_MODEL": "taiji/gpt-4o-mini",
"FACEBOOK_API_KEY": "34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00"
},
"code_ai_agent": {
"OPENAI_BASE_URL": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1",
"LITELLM_MODEL": "taiji/gpt-4o-mini"
}
}
# 获取用户自定义环境变量
custom_env = config_data.get("env", {})
# 如果模板有默认值,先添加默认值(用户自定义值会覆盖)
if template in template_defaults:
defaults = template_defaults[template]
for key, value in defaults.items():
# 只有当用户没有提供该环境变量时才使用默认值
if key not in custom_env:
env_vars.append(client.V1EnvVar(name=key, value=str(value)))
logger.info(f"使用默认环境变量: {key}")
# 添加用户自定义环境变量(会覆盖默认值)
for key, value in custom_env.items():
env_vars.append(client.V1EnvVar(name=key, value=str(value)))
logger.info(f"Pod {pod_name} 环境变量数量: {len(env_vars)}")
# 设置容器端口(如果是HTTP服务类型的agent)
container_ports = None
if template in ["search_agent", "search_agent_a2a", "search_agent_mcp"]:
container_ports = [client.V1ContainerPort(container_port=8080)]
elif template in ["jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent", "code_ai_agent", "facebook_agent", "echo_agent", "mysql_agent", "postgresql_agent"]:
container_ports = [client.V1ContainerPort(container_port=8000)]
# 创建Pod规格
container = client.V1Container(
name=pod_name,
image=image,
resources=client.V1ResourceRequirements(
requests={"cpu": cpu_request, "memory": memory_request},
limits={"cpu": cpu_limit, "memory": memory_limit}
),
env=env_vars,
ports=container_ports
)
# 根据镜像来源选择合适的 imagePullSecrets
image_pull_secrets = [client.V1LocalObjectReference(name="acr-secret")]
if "openclawacr" in image:
image_pull_secrets.append(client.V1LocalObjectReference(name="openclaw-acr-secret"))
pod_spec = client.V1PodSpec(
containers=[container],
restart_policy="Always",
image_pull_secrets=image_pull_secrets
)
# 构建标签(合并默认标签和用户自定义标签)
labels = {
"app": "ai-agent",
"template": template,
"managed-by": "agent-manager",
"framework": agent_framework # NEW: 添加框架标签
}
# 添加用户自定义标签
if "labels" in config_data:
labels.update(config_data["labels"])
pod_manifest = client.V1Pod(
api_version="v1",
kind="Pod",
metadata=client.V1ObjectMeta(
name=pod_name,
labels=labels
),
spec=pod_spec
)
return pod_manifest
def delete_pod(self, pod_name: str) -> Dict:
"""
删除Pod
Args:
pod_name: Pod名称
Returns:
删除结果
"""
try:
self.v1.delete_namespaced_pod(
name=pod_name,
namespace=self.namespace,
body=client.V1DeleteOptions()
)
logger.info(f"Pod {pod_name} 删除成功")
return {"status": "success", "message": f"Pod {pod_name} 已删除"}
except ApiException as e:
if e.status == 404:
return {"status": "not_found", "message": f"Pod {pod_name} 不存在"}
logger.error(f"删除Pod失败: {e}")
raise Exception(f"删除Pod失败: {e.reason}")
def delete_agent_namespace(self, agent_name: str) -> Dict:
"""删除Agent的命名空间及其所有资源
Args:
agent_name: Agent 名称
Returns:
删除结果
"""
sanitized_name = sanitize_k8s_name(agent_name)
namespace_name = f"agent-{sanitized_name}"[:63].rstrip('-')
# region agent log
try:
import json, time
with open("/home/taiji/tools/agent-manager/.cursor/debug.log", "a") as _f:
_f.write(json.dumps({
"sessionId": "debug-session",
"runId": "pre-fix",
"hypothesisId": "H1",
"location": "k8s_manager.py:delete_agent_namespace:entry",
"message": "delete_agent_namespace computed namespace",
"data": {
"agent_name": agent_name,
"namespace_name": namespace_name,
"is_manager_namespace": namespace_name == "agent-manager"
},
"timestamp": int(time.time() * 1000)
}) + "\n")
except Exception:
pass
# endregion
# 保护机制:防止删除 agent-manager 命名空间
if namespace_name == "agent-manager":
logger.error(f"❌ 禁止删除 agent-manager 命名空间!agent_name={agent_name}, namespace_name={namespace_name}")
raise Exception(f"禁止删除 agent-manager 命名空间。这是系统保护命名空间,不能被删除。")
try:
# 删除命名空间会自动删除其中的所有资源(Pod、Service、Ingress等)
self.v1.delete_namespace(
name=namespace_name,
body=client.V1DeleteOptions()
)
logger.info(f"✅ 命名空间 {namespace_name} 及其所有资源删除成功")
return {
"status": "success",
"message": f"Agent {agent_name} 的命名空间 {namespace_name} 及所有相关资源已删除",
"namespace": namespace_name
}
except ApiException as e:
if e.status == 404:
return {
"status": "not_found",
"message": f"命名空间 {namespace_name} 不存在"
}
logger.error(f"删除命名空间失败: {e}")
raise Exception(f"删除命名空间失败: {e.reason}")
def _get_azure_access_token(self) -> str:
"""获取 Azure Access Token
Returns:
Azure 访问令牌
"""
try:
token_url = f"https://login.microsoftonline.com/{self.AZURE_TENANT_ID}/oauth2/v2.0/token"
data = {
"client_id": self.AZURE_CLIENT_ID,
"client_secret": self.AZURE_CLIENT_SECRET,
"grant_type": "client_credentials",
"scope": "https://management.azure.com/.default"
}
response = requests.post(token_url, data=data, timeout=10)
response.raise_for_status()
token = response.json().get('access_token')
if not token:
raise Exception("未能获取访问令牌")
return token
except Exception as e:
logger.error(f"获取 Azure 访问令牌失败: {e}")
raise
def create_dns_record(self, subdomain: str, ip_address: str, ttl: int = 300) -> Dict:
"""为 IP 地址创建 Azure DNS A 记录
Args:
subdomain: 子域名(不包括主域名)
ip_address: 要指向的 IP 地址
ttl: DNS TTL(秒),默认 300
Returns:
DNS 记录信息
"""
try:
# 获取访问令牌
token = self._get_azure_access_token()
# 构建 DNS API URL
dns_url = (
f"https://management.azure.com/subscriptions/{self.AZURE_SUBSCRIPTION_ID}/"
f"resourceGroups/{self.AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/"
f"dnsZones/{self.AZURE_DNS_ZONE}/A/{subdomain}?api-version=2018-05-01"
)
# DNS 记录数据
dns_data = {
"properties": {
"TTL": ttl,
"ARecords": [{"ipv4Address": ip_address}]
}
}
# 创建 DNS 记录
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.put(dns_url, json=dns_data, headers=headers, timeout=30)
response.raise_for_status()
full_domain = f"{subdomain}.{self.AZURE_DNS_ZONE}"
logger.info(f"✅ DNS 记录创建成功: {full_domain} -> {ip_address}")
return {
"subdomain": subdomain,
"domain": full_domain,
"ip_address": ip_address,
"ttl": ttl,
"status": "created"
}
except requests.exceptions.RequestException as e:
logger.error(f"创建 DNS 记录失败: {e}")
if hasattr(e.response, 'text'):
logger.error(f"错误详情: {e.response.text}")
raise Exception(f"创建 DNS 记录失败: {str(e)}")
def delete_dns_record(self, subdomain: str) -> Dict:
"""删除 Azure DNS A 记录
Args:
subdomain: 要删除的子域名
Returns:
删除结果
"""
try:
# 获取访问令牌
token = self._get_azure_access_token()
# 构建 DNS API URL
dns_url = (
f"https://management.azure.com/subscriptions/{self.AZURE_SUBSCRIPTION_ID}/"
f"resourceGroups/{self.AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/"
f"dnsZones/{self.AZURE_DNS_ZONE}/A/{subdomain}?api-version=2018-05-01"
)
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(dns_url, headers=headers, timeout=30)
if response.status_code == 404:
logger.info(f"DNS 记录 {subdomain} 不存在,跳过删除")
return {"status": "not_found", "subdomain": subdomain}
response.raise_for_status()
full_domain = f"{subdomain}.{self.AZURE_DNS_ZONE}"
logger.info(f"✅ DNS 记录删除成功: {full_domain}")
return {
"subdomain": subdomain,
"domain": full_domain,
"status": "deleted"
}
except requests.exceptions.RequestException as e:
logger.error(f"删除 DNS 记录失败: {e}")
raise Exception(f"删除 DNS 记录失败: {str(e)}")
def wait_for_loadbalancer_ip(self, service_name: str, namespace: str,
max_wait: int = 300, interval: int = 5) -> str:
"""等待 LoadBalancer 分配外网 IP
Args:
service_name: Service 名称
namespace: 命名空间
max_wait: 最大等待时间(秒)
interval: 检查间隔(秒)
Returns:
分配的外网 IP 地址
"""
logger.info(f"等待 LoadBalancer IP 分配... (最多等待 {max_wait} 秒)")
elapsed = 0
while elapsed < max_wait:
try:
service = self.v1.read_namespaced_service(service_name, namespace)
if service.status.load_balancer.ingress:
ip = service.status.load_balancer.ingress[0].ip
if ip:
logger.info(f"✅ LoadBalancer IP 已分配: {ip}")
return ip
except Exception as e:
logger.warning(f"查询 Service 状态失败: {e}")
time.sleep(interval)
elapsed += interval
logger.info(f"等待中... ({elapsed}/{max_wait} 秒)")
raise Exception(f"LoadBalancer IP 分配超时({max_wait} 秒)")
def get_pod_status(self, pod_name: str) -> Dict:
"""
获取Pod状态
Args:
pod_name: Pod名称
Returns:
Pod状态信息,包含访问URL和资源使用情况
"""
try:
pod = self.v1.read_namespaced_pod(
name=pod_name,
namespace=self.namespace
)
template = pod.metadata.labels.get("template", "unknown")
pod_ip = pod.status.pod_ip
service_port = self.TEMPLATE_PORTS.get(template)
# 获取资源配额信息
container = pod.spec.containers[0]
resources = container.resources
resource_requests = {
"cpu": resources.requests.get("cpu") if resources.requests else None,
"memory": resources.requests.get("memory") if resources.requests else None
}
resource_limits = {
"cpu": resources.limits.get("cpu") if resources.limits else None,
"memory": resources.limits.get("memory") if resources.limits else None
}
# 尝试获取实际资源使用情况(需要metrics-server)
resource_usage = self._get_pod_resource_usage(pod_name)
# 获取容器实际状态 - 检查是否崩溃或异常
container_statuses = pod.status.container_statuses or []
actual_status = pod.status.phase # 默认使用Pod阶段
health_status = "healthy"
container_info = []
for container_status in container_statuses:
container_state = {}
restart_count = container_status.restart_count
# 检查容器状态
if container_status.state.running:
container_state = {
"state": "running",
"started_at": container_status.state.running.started_at.isoformat() if container_status.state.running.started_at else None
}
elif container_status.state.waiting:
container_state = {
"state": "waiting",
"reason": container_status.state.waiting.reason,
"message": container_status.state.waiting.message
}
# 容器在等待状态,标记为不健康
health_status = "unhealthy"
actual_status = "Waiting"
elif container_status.state.terminated:
container_state = {
"state": "terminated",
"reason": container_status.state.terminated.reason,
"exit_code": container_status.state.terminated.exit_code,
"message": container_status.state.terminated.message,
"finished_at": container_status.state.terminated.finished_at.isoformat() if container_status.state.terminated.finished_at else None
}
# 容器已终止,标记为不健康
health_status = "unhealthy"
actual_status = "Terminated"
# 检查容器是否就绪
if not container_status.ready:
health_status = "unhealthy"
# 如果重启次数过多,也标记为不健康
if restart_count > 5:
health_status = "degraded"
container_info.append({
"name": container_status.name,
"ready": container_status.ready,
"restart_count": restart_count,
**container_state
})
result = {
"name": pod.metadata.name,
"namespace": pod.metadata.namespace,
"status": actual_status,
"health_status": health_status, # 新增:真实健康状态
"template": template,
"created_at": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
"node": pod.spec.node_name,
"pod_ip": pod_ip,
"containers": container_info, # 新增:容器详细信息
"resources": {
"requests": resource_requests,
"limits": resource_limits,
"usage": resource_usage
},
"conditions": [
{
"type": condition.type,
"status": condition.status,
"reason": condition.reason
}
for condition in (pod.status.conditions or [])
]
}
# 如果是HTTP服务类型的agent且Pod已有IP,添加访问URL
if service_port and pod_ip:
result["service_port"] = service_port
result["access_url"] = f"http://{pod_ip}:{service_port}"
result["endpoints"] = {
"root": f"http://{pod_ip}:{service_port}/",
"health": f"http://{pod_ip}:{service_port}/health"
}
return result
except ApiException as e:
if e.status == 404:
return {"status": "not_found", "message": f"Pod {pod_name} 不存在"}
logger.error(f"获取Pod状态失败: {e}")
raise Exception(f"获取Pod状态失败: {e.reason}")
def _get_pod_resource_usage(self, pod_name: str) -> Dict:
"""
获取Pod实际资源使用情况(需要metrics-server)
Args:
pod_name: Pod名称
Returns:
资源使用信息(cpu、memory)
"""
try:
# 使用CustomObjectsApi调用metrics API
custom_api = client.CustomObjectsApi()
metrics = custom_api.get_namespaced_custom_object(
group="metrics.k8s.io",
version="v1beta1",
namespace=self.namespace,
plural="pods",
name=pod_name
)
# 解析容器资源使用情况
containers = metrics.get("containers", [])
if containers:
container = containers[0]
usage = container.get("usage", {})
return {
"cpu": usage.get("cpu"),
"memory": usage.get("memory"),
"available": True
}
return {"cpu": None, "memory": None, "available": False, "reason": "无容器数据"}
except ApiException as e:
if e.status == 404:
return {"cpu": None, "memory": None, "available": False, "reason": "metrics-server未安装或Pod不存在"}
logger.warning(f"获取Pod资源使用情况失败: {e.reason}")
return {"cpu": None, "memory": None, "available": False, "reason": f"获取失败: {e.reason}"}
except Exception as e:
logger.warning(f"获取Pod资源使用情况异常: {str(e)}")
return {"cpu": None, "memory": None, "available": False, "reason": f"异常: {str(e)}"}
def get_pod_metrics(self, pod_name: str) -> Dict:
"""
获取Pod资源使用情况(CPU、内存)
Args:
pod_name: Pod名称
Returns:
Pod实时资源使用信息和配额信息
"""
try:
# 获取 Pod 配额信息
pod = self.v1.read_namespaced_pod(
name=pod_name,
namespace=self.namespace
)
container = pod.spec.containers[0]
resources = container.resources
result = {
"name": pod_name,
"namespace": self.namespace,
"requests": {
"cpu": resources.requests.get("cpu") if resources.requests else None,
"memory": resources.requests.get("memory") if resources.requests else None
},
"limits": {
"cpu": resources.limits.get("cpu") if resources.limits else None,
"memory": resources.limits.get("memory") if resources.limits else None
}
}
# 尝试获取实时使用情况(需要 metrics-server)
try:
from kubernetes.client import CustomObjectsApi
custom_api = CustomObjectsApi()
# 调用 metrics.k8s.io API
metrics = custom_api.get_namespaced_custom_object(
group="metrics.k8s.io",
version="v1beta1",
namespace=self.namespace,
plural="pods",
name=pod_name
)
# 提取实时使用数据
if metrics and "containers" in metrics:
container_metrics = metrics["containers"][0]
usage = container_metrics.get("usage", {})
result["usage"] = {
"cpu": usage.get("cpu"),
"memory": usage.get("memory")
}
result["timestamp"] = metrics.get("timestamp")
logger.info(f"✅ 获取到实时资源使用: CPU={usage.get('cpu')}, Memory={usage.get('memory')}")
else:
result["usage"] = None
logger.warning(f"⚠️ Metrics 数据格式异常")
except Exception as metrics_error:
logger.warning(f"⚠️ 无法获取实时资源使用(可能未安装 metrics-server): {str(metrics_error)}")
result["usage"] = None
result["metrics_available"] = False
return result
except ApiException as e:
logger.error(f"获取Pod资源信息失败: {e}")
raise Exception(f"获取Pod资源信息失败: {e.reason}")
def list_pods(self, label_selector: Optional[str] = None) -> List[Dict]:
"""
列出所有Pod
Args:
label_selector: 标签选择器(可选)
Returns:
Pod列表
"""
try:
if label_selector is None:
label_selector = "managed-by=agent-manager"
pods = self.v1.list_namespaced_pod(
namespace=self.namespace,
label_selector=label_selector
)
return [
{
"name": pod.metadata.name,
"status": pod.status.phase,
"template": pod.metadata.labels.get("template", "unknown"),
"created_at": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
"pod_ip": pod.status.pod_ip
}
for pod in pods.items
]
except ApiException as e:
logger.error(f"列出Pod失败: {e}")
raise Exception(f"列出Pod失败: {e.reason}")