Metadata-Version: 2.4
Name: mlflow-tcdeploy-plugin
Version: 1.0.3
Summary: Tencent Cloud deployment plugin for MLflow
Home-page: https://git.woa.com/WeDataOS/wedata3-monorepo
Author: Tencent WeData Team
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: mlflow<3.11.0,>=3.10.0
Requires-Dist: tencentcloud-sdk-python>=3.0.1478
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: python-dotenv; extra == "dev"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# mlflow-tcdeploy-plugin

[![PyPI version](https://img.shields.io/pypi/v/mlflow-tcdeploy-plugin)](https://pypi.org/project/mlflow-tcdeploy-plugin/)
[![Python](https://img.shields.io/pypi/pyversions/mlflow-tcdeploy-plugin)](https://pypi.org/project/mlflow-tcdeploy-plugin/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.txt)

腾讯云模型服务部署插件 —— 将腾讯云 WeData 在线推理服务对接为 [MLflow Deployments](https://mlflow.org/docs/latest/deployments.html) 后端。

与 [`mlflow-tclake-plugin`](../mlflow-tclake-plugin)（Model Registry）配套使用：

| 插件 | 职责 | 后端 |
|------|------|------|
| `mlflow-tclake-plugin` | 模型注册与版本管理 | TCLake |
| **`mlflow-tcdeploy-plugin`** | 模型在线部署与推理 | 腾讯云 WeData 推理服务 |

---

## 目录

- [特性](#特性)
- [架构](#架构)
- [安装](#安装)
- [配置](#配置)
- [快速上手](#快速上手)
- [API 参考](#api-参考)
  - [create_deployment — 创建服务](#create_deployment--创建服务)
  - [update_deployment — 更新服务](#update_deployment--更新服务)
  - [delete_deployment — 删除服务](#delete_deployment--删除服务)
  - [get_deployment — 查询服务详情](#get_deployment--查询服务详情)
  - [list_deployments — 列举服务组](#list_deployments--列举服务组)
  - [predict — 在线推理](#predict--在线推理)
  - [list_instance_types — 查询可用规格](#list_instance_types--查询可用规格)
  - [debug_pod_shell — 调试 Pod](#debug_pod_shell--调试-pod)
  - [restart_pod — 重启 Pod](#restart_pod--重启-pod)
  - [get_pod_logs — 查询 Pod 日志](#get_pod_logs--查询-pod-日志)
- [Model URI 格式](#model-uri-格式)
- [安全机制](#安全机制)
- [项目结构](#项目结构)
- [开发](#开发)
- [License](#license)

---

## 特性

- ✅ 完整的 MLflow Deployments 生命周期：创建 / 更新 / 删除 / 查询 / 列举
- ✅ 在线推理调用（`predict`）
- ✅ 实例规格查询（直连 TIone SDK）
- ✅ Pod 运维：日志查看、Pod 重启、调试 Shell
- ✅ 三段式 Model URI 解析（`models:/Catalog.Schema.Model/Version`）
- ✅ 自动 TC3 请求签名
- ✅ SSRF 防护：endpoint 域名白名单 + 请求路径严格校验
- ✅ snake_case ↔ PascalCase 自动转换

---

## 架构

```
┌─────────────────────────────────────────────────────┐
│  用户代码                                            │
│  client = get_deploy_client("tcdeploy")             │
│  client.create_deployment(...)                      │
└──────────────────┬──────────────────────────────────┘
                   │
    ┌──────────────▼──────────────┐
    │  TcDeploymentClient         │  ← MLflow BaseDeploymentClient
    │  (client.py)                │
    │  - 参数校验 / SSRF 防护     │
    │  - snake_case ↔ PascalCase  │
    └──────────────┬──────────────┘
                   │
    ┌──────────────▼──────────────┐
    │  ModelServiceApiClient      │  ← 封装 WeData 云 API
    │  (api_client.py)            │
    │  - TC3 签名 (AbstractClient)│
    │  - 错误码 → MlflowException │
    └──────────────┬──────────────┘
                   │ HTTPS (TC3-HMAC-SHA256)
    ┌──────────────▼──────────────┐
    │  WeData API                 │
    │  (tencentcloudapi.com)      │
    └─────────────────────────────┘
```

**核心模块说明：**

| 模块 | 文件 | 说明 |
|------|------|------|
| **Client** | `client.py` | 实现 `BaseDeploymentClient`，对外暴露所有操作方法 |
| **API Client** | `api_client.py` | 封装 WeData / TIone 云 API 调用，继承腾讯云 SDK 的 `AbstractClient` 实现 TC3 签名 |
| **Config** | `config.py` | 环境变量 / 参数配置管理，含 endpoint 域名白名单校验 |
| **Models** | `models.py` | 请求体构建（Create/Update）、Model URI 解析、大小写转换 |

---

## 安装

```bash
pip install mlflow-tcdeploy-plugin
```

**依赖：**
- `mlflow >= 3.10.0, < 3.11.0`
- `tencentcloud-sdk-python >= 3.0.1478`
- Python >= 3.10

---

## 配置

插件通过环境变量获取配置（通常由 `wedata-pre-execute` 自动注入），也支持构造时直接传参：

| 环境变量 | 说明 | 必填 | 默认值 |
|---------|------|:----:|-------|
| `KERNEL_WEDATA_CLOUD_SDK_SECRET_ID` | 腾讯云 SecretId | ✅ | — |
| `KERNEL_WEDATA_CLOUD_SDK_SECRET_KEY` | 腾讯云 SecretKey | ✅ | — |
| `WEDATA_WORKSPACE_ID` | WeData 工作空间 ID | ✅ | — |
| `KERNEL_WEDATA_REGION` | 地域 | ❌ | `ap-guangzhou` |
| `TENCENTCLOUD_ENDPOINT` | WeData API endpoint | ❌ | `wedata.internal.tencentcloudapi.com` |
| `KERNEL_WEDATA_CLOUD_SDK_SECRET_TOKEN` | 临时凭证 Token（STS） | ❌ | — |
| `KERNEL_LOGIN_UIN` | 子账号 UIN | ❌ | — |
| `QCLOUD_UIN` | 主账号 UIN | ❌ | — |

> **安全提示**：`TENCENTCLOUD_ENDPOINT` 受域名白名单保护，仅允许 `*.tencentcloudapi.com` 和 `*.tencentcloud.com` 后缀。

---

## 快速上手

```python
from mlflow.deployments import get_deploy_client

# 初始化客户端（自动读取环境变量）
client = get_deploy_client("tcdeploy")

# 1. 查询可用规格
specs = client.list_instance_types(spec_type="CPU")
available = [s for s in specs if s["available"]]
print(available[0]["spec_name"])  # e.g. "TI.SA5.2XLARGE32.POST"

# 2. 创建部署
result = client.create_deployment(
    name="my-service",
    model_uri="models:/default.default.MyModel/1",
    config={
        "instance_type": "TI.SA5.2XLARGE32.POST",
        "replicas": 1,
    },
)
print(result["service_id"])

# 3. 查询部署详情（name 传入的是 ServiceId，非服务名称）
info = client.get_deployment(name="svc-xxxx")
print(info["status"])

# 4. 在线推理
response = client.predict(
    deployment_name="grp-xxxx",
    inputs={"text": "Hello, world!"},
)
print(response)

# 5. 更新部署（扩容到 2 副本，name = ServiceId）
client.update_deployment(name="svc-xxxx", config={"replicas": 2})

# 6. 删除部署（幂等，name = ServiceId）
client.delete_deployment(name="svc-xxxx")
```

---

## API 参考

> **⚠️ 重要说明**：在 `update_deployment`、`delete_deployment`、`get_deployment` 等方法中，`name` 参数传入的是**腾讯云 ServiceId**（如 `svc-xxxx`），而**不是**服务名称。这是因为 MLflow `BaseDeploymentClient` 接口规范使用 `name` 作为参数名，但在本插件的上下文中它对应的是 ServiceId —— 服务的唯一标识符。

### `create_deployment` — 创建服务

```python
client.create_deployment(
    name="my-service",                              # 服务名称
    model_uri="models:/Catalog.Schema.Model/1",     # MLflow Model URI
    config={                                         # 部署配置
        "instance_type": "TI.SA5.2XLARGE32.POST",  # 必填：实例规格
        "replicas": 1,                              # 副本数（默认 1）
        "charge_type": "POSTPAID_BY_HOUR",          # 计费模式（默认按量后付费）
        "scale_mode": "MANUAL",                     # 伸缩模式（默认手动）
        "log_enable": False,                        # 是否启用日志
        "authorization_enable": False,              # 是否启用鉴权
        "service_description": "...",               # 服务描述
        "log_config": {...},                        # 日志配置
        "scheduled_action": {...},                  # 定时伸缩配置
        "service_limit": {...},                     # 限流配置
    },
    endpoint="grp-xxxx",                            # 可选：服务组 ID
)
```

**调用的云 API**：`CreateMLModelServices`

---

### `update_deployment` — 更新服务

```python
client.update_deployment(
    name="svc-xxxx",                                # ⚠️ ServiceId（非服务名称）
    model_uri="models:/Catalog.Schema.Model/2",     # 可选：更新模型版本
    config={                                         # 仅发送需更新的字段
        "replicas": 3,
        "instance_type": "TI.SA5.4XLARGE64.POST",
        "service_port": 8080,
        "status": "RUNNING",
    },
)
```

> **注意**：`name` 参数对应腾讯云的 **ServiceId**（如 `svc-xxxx`），而非服务名称。受 MLflow `BaseDeploymentClient` 接口约束，参数名为 `name`，但语义上是服务唯一标识。

**调用的云 API**：[`UpdateMLModelService`](https://cloud.tencent.com/document/product/851/83228)（扁平结构，非嵌套 `Services` 数组）

#### `ServiceAction` — 特殊更新行为

`config` 中可以通过 `service_action` 字段触发特殊操作。**⚠️ 当指定 `ServiceAction` 时，请求中的其他更新字段会被服务端忽略。**

| `service_action` 值 | 含义 | 说明 |
|---------------------|------|------|
| `"STOP"` | 停止服务 | 将运行中的服务停止 |
| `"RESUME"` | 重启服务 | 将已停止的服务恢复运行 |
| `"SCALE"` | 扩缩容 | 触发服务扩缩容操作 |

**示例：停止服务**

```python
client.update_deployment(
    name="svc-xxxx",
    config={"service_action": "STOP"},
)
```

**示例：恢复服务**

```python
client.update_deployment(
    name="svc-xxxx",
    config={"service_action": "RESUME"},
)
```

**示例：触发扩缩容**

```python
client.update_deployment(
    name="svc-xxxx",
    config={"service_action": "SCALE"},
)
```

---

### `delete_deployment` — 删除服务

```python
client.delete_deployment(name="svc-xxxx")  # ⚠️ name = ServiceId；幂等：资源不存在时不报错
```

**调用的云 API**：`DeleteMLModelService`

---

### `get_deployment` — 查询服务详情

```python
info = client.get_deployment(name="svc-xxxx")  # ⚠️ name = ServiceId
# info 是 snake_case 字典
print(info["status"], info["service_name"])
```

**调用的云 API**：`GetMLModelService`

---

### `list_deployments` — 列举服务组

```python
# 列举所有服务组
groups = client.list_deployments()

# 按服务组 ID 过滤
groups = client.list_deployments(endpoint="grp-xxxx")

# 每个 item 包含 "name" 字段（= ServiceGroupId），符合 MLflow 规范
for g in groups:
    print(g["name"], g["service_group_name"])
```

**调用的云 API**：`ListMLModelServiceGroups`

---

### `predict` — 在线推理

```python
result = client.predict(
    deployment_name="grp-xxxx",        # 服务组 ID
    inputs={"text": "classify this"},  # JSON 可序列化的输入
    endpoint="/v1/predict",            # 可选：相对路径（默认 /predict）
    config={"auth_token": "xxx"},      # 可选：鉴权 Token
)
```

**调用的云 API**：`ModelServiceInterfaceCallTest`

> `endpoint` 参数会经过 SSRF 安全校验，防止注入恶意 URL。

---

### `list_instance_types` — 查询可用规格

```python
# 全部规格
all_specs = client.list_instance_types()

# 仅 GPU 规格
gpu_specs = client.list_instance_types(spec_type="GPU")

# 返回结构
# [{"spec_name": "TI.SA5.2XLARGE32.POST", "spec_alias": "...",
#   "spec_type": "CPU", "available": True, "available_region": [...],
#   "gpu_type": ""}]
```

**调用的 SDK**：直连 TIone SDK `DescribeInferenceSpecs`

---

### `debug_pod_shell` — 调试 Pod

```python
shell_info = client.debug_pod_shell(
    service_id="svc-xxxx",
    pod_name="ms-xxxx-0",
)
# 返回 WebShell URL 等信息
```

**调用的云 API**：`CreateModelServicePodUrl`

---

### `restart_pod` — 重启 Pod

```python
result = client.restart_pod(
    service_id="svc-xxxx",
    pod_name="ms-xxxx-0",
)
print(result["request_id"])
```

**调用的云 API**：`RebuildModelServicePod`

---

### `get_pod_logs` — 查询 Pod 日志

```python
result = client.get_pod_logs(
    service_id="svc-xxxx",
    pod_name="ms-xxxx-*",              # 支持通配符
    limit=100,                          # 最大条数
    start_time="2026-03-10T00:00:00+08:00",
    end_time="2026-03-10T23:59:59+08:00",
    context=None,                       # 翻页 token
)

for log in result["logs"]:
    print(f"[{log['timestamp']}] {log['pod_name']}: {log['message']}")

# 翻页
next_page = client.get_pod_logs(service_id="svc-xxxx", pod_name="ms-xxxx-*", context=result["context"])
```

**调用的云 API**：`ListMLServiceLogs`

---

## Model URI 格式

本插件使用 **三段式** Model URI：

```
models:/CatalogName.SchemaName.ModelName/Version
```

- **CatalogName**：目录名（如 `default`）
- **SchemaName**：Schema 名（如 `default`）
- **ModelName**：模型名称
- **Version**：模型版本号

示例：`models:/default.default.MyTextClassifier/3`

当提供 `api_client` 时，插件会调用 `ListModelVersions` 自动解析模型的 `Id` 和 `ModelPath`。

也支持 `runs:/` 格式的 URI，此时直接将 URI 作为 `ModelPath` 传递。

---

## 安全机制

### Endpoint 域名白名单

`config.py` 中对 API endpoint 实施域名白名单校验：

- 仅允许 `*.tencentcloudapi.com` 和 `*.tencentcloud.com` 后缀
- 拒绝包含 scheme（`https://`）、路径（`/api`）、`@` 等非法格式
- 防止通过篡改 `TENCENTCLOUD_ENDPOINT` 环境变量将请求导向恶意服务器

### 请求路径 SSRF 防护

`predict` 方法的 `endpoint` 参数经过严格校验（`_validate_relative_url`）：

| 攻击向量 | 示例 | 防御方式 |
|---------|------|---------|
| 绝对 URL 注入 | `http://evil.com` | 检测 `://` |
| Protocol-relative | `//evil.com` | 检测 `//` 和 `\` 开头 |
| Percent-encoded 绕过 | `%2f%2fevil.com` | 先 `unquote` 解码再校验 |
| `@` host-override | `/legit@evil.com` | 拒绝含 `@` 的路径 |
| 路径遍历 | `/../internal-api` | 逐段检测 `..` 并拒绝 |
| CRLF / null-byte 注入 | `/predict\r\nHost: evil` | 正则匹配控制字符 |

---

## 项目结构

```
mlflow-tcdeploy-plugin/
├── mlflow_tcdeploy_plugin/
│   ├── __init__.py          # 版本号
│   ├── client.py            # TcDeploymentClient (MLflow Deployments 接口实现)
│   ├── api_client.py        # ModelServiceApiClient (WeData 云 API 封装)
│   ├── config.py            # TcDeployConfig (环境变量配置 + endpoint 安全校验)
│   └── models.py            # 请求体构建、Model URI 解析、大小写转换
├── tests/
│   ├── test_client.py       # Client 层单元测试 (含 SSRF 防护测试)
│   ├── test_api_client.py   # API Client 层单元测试
│   ├── test_config.py       # 配置层单元测试 (含 endpoint 白名单测试)
│   └── test_models.py       # 模型层单元测试 (Create/Update 请求构建)
├── setup.py                 # 打包配置
├── build.sh                 # 构建 & 发布脚本
├── LICENSE.txt              # MIT License
└── README.md                # 本文件
```

---

## 开发

```bash
# 克隆并进入项目
cd mlflow-tcdeploy-plugin

# 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate

# 安装开发依赖
pip install -e ".[dev]"

# 运行单元测试
pytest tests/ -v

# 构建 & 发布
bash build.sh
```

---

## License

[MIT License](LICENSE.txt) — Tencent WeData Team
