Files
rainyun-cli/docs/spec/2026-07-28-ry-cli-design.md

457 lines
14 KiB
Markdown
Raw Permalink 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.
# ry CLI 设计规格
> 日期:2026-07-28 | 状态:待审查
## 概述
`ry` 是一个多提供商云服务 CLI 工具,风格对标 `gh`(GitHub CLI)。支持通过统一接口管理不同云厂商(雨云为首个实现),可扩展至阿里云、腾讯云等。
- **语言**:Go
- **配置格式**:TOML
- **架构模式**:Provider 接口 + 编译时注册
---
## 项目结构
```
ry/ # 仓库根目录
├── cmd/ry/ # 入口 main.go
├── internal/
│ ├── config/ # TOML 配置读写
│ ├── provider/ # 提供商接口 + 注册表
│ │ ├── provider.go # Provider 接口定义
│ │ ├── registry.go # 提供商注册/工厂
│ │ └── rainyun/ # 雨云实现
│ │ ├── rainyun.go # 实现 Provider 接口
│ │ ├── server.go # 服务器资源操作
│ │ ├── domain.go # 域名资源操作
│ │ ├── storage.go # 对象存储操作
│ │ ├── cdn.go # CDN 操作
│ │ └── billing.go # 计费操作
│ ├── cmd/ # cobra 命令定义
│ │ ├── root.go # 根命令 + 全局 flag
│ │ ├── provider.go # ry provider use|list|add
│ │ ├── server.go # ry server list|get|start|stop...
│ │ ├── domain.go # ry domain list|dns ...
│ │ ├── storage.go # ry storage list|bucket ...
│ │ ├── billing.go # ry billing orders
│ │ └── config.go # ry config show|set|path
│ └── output/ # 统一输出格式化
│ ├── output.go # Output 接口
│ ├── table.go # 表格渲染(tablewriter)
│ ├── json.go # JSON 输出
│ └── yaml.go # YAML 输出
├── go.mod
├── go.sum
└── README.md
```
---
## Provider 接口
```go
// Provider 云服务商抽象
type Provider interface {
Name() string // 唯一标识,如 "rainyun"
DisplayName() string // 显示名,如 "雨云"
// 认证验证
ValidateAuth(ctx context.Context) error
// 资源子接口(返回 nil 表示不支持该资源)
Server() ServerService
Domain() DomainService
Storage() StorageService
CDN() CDNService
Billing() BillingService
}
// ServerService 云服务器操作
type ServerService interface {
List(ctx context.Context) ([]Server, error)
Get(ctx context.Context, id string) (*Server, error)
Start(ctx context.Context, id string) error
Stop(ctx context.Context, id string) error
Reboot(ctx context.Context, id string) error
Reinstall(ctx context.Context, id, os string) error
ResetPassword(ctx context.Context, id string) error
GetVNC(ctx context.Context, id string) (string, error)
Upgrade(ctx context.Context, id, plan string) error
BackupList(ctx context.Context, id string) ([]Backup, error)
FirewallRuleList(ctx context.Context, id string) ([]FirewallRule, error)
}
// Server 通用服务器模型
type Server struct {
ID string `json:"id" table:"ID"`
Name string `json:"name" table:"NAME"`
Status string `json:"status" table:"STATUS"`
IP string `json:"ip" table:"IP"`
CPU int `json:"cpu" table:"CPU"`
Memory int `json:"memory" table:"MEM"` // MB
Disk int `json:"disk" table:"DISK"` // GB
OS string `json:"os" table:"OS"`
Region string `json:"region" table:"REGION"`
ExpireAt time.Time `json:"expires" table:"EXPIRES"`
Raw any `json:"-"` // 原始响应,--raw 时输出
}
```
**设计要点**:
- 不支持的能力返回 `nil`,命令层检测后隐藏或提示
- 所有模型用 `table` tag 标注表头,`json` tag 标注序列化
- `Raw` 字段保留原始数据,`--raw` flag 时输出
- 初期编译时注册 Provider,后续可扩展为 `.so` 插件
---
## 设计模式(多提供商架构核心)
不同云厂商的 API 格式差异巨大——雨云用 `X-Api-Key` header + 简单 JSON,阿里云用 AK/SK 签名 + XML 响应体,腾讯云用 v3 签名 + 骆驼命名 JSON。这些差异通过以下三种模式隔离:
### 工厂模式 — `Registry` 创建 Provider 实例
```go
// internal/provider/registry.go
type ProviderFactory func(cfg ProviderConfig) (Provider, error)
var registry = map[string]ProviderFactory{}
func Register(name string, factory ProviderFactory) {
registry[name] = factory
}
func New(name string, cfg ProviderConfig) (Provider, error) {
factory, ok := registry[name]
if !ok {
return nil, fmt.Errorf("unknown provider: %s", name)
}
return factory(cfg)
}
```
每个提供商在 `init()` 中自注册:
```go
// internal/provider/rainyun/rainyun.go
func init() {
provider.Register("rainyun", New)
}
func New(cfg provider.ProviderConfig) (provider.Provider, error) {
return &Rainyun{apiKey: cfg.APIKey}, nil
}
```
### 策略模式 — 每个 Provider 独立实现 Service 接口
同一个 `ServerService.List()`,不同提供商的实现完全不同:
```go
// 雨云:简单 GET + header auth
func (r *RainyunServer) List(ctx context.Context) ([]provider.Server, error) {
req, _ := http.NewRequestWithContext(ctx, "GET",
"https://api.v2.rainyun.com/product/rcs/", nil)
req.Header.Set("X-Api-Key", r.apiKey)
// ... 发请求,parse 雨云格式的 JSON
}
// 阿里云:AK/SK 签名 + Query 参数 + XML 解析
func (a *AliyunServer) List(ctx context.Context) ([]provider.Server, error) {
params := map[string]string{"Action": "DescribeInstances", ...}
url := signAndBuild("https://ecs.aliyuncs.com/", a.ak, a.sk, params)
// ... 发请求,parse 阿里云 XML
}
```
两个实现返回相同的 `[]Server` 结构,命令层无感知。
### 适配器模式 — 内部响应统一
每个 Provider 内部有自己的 HTTP 客户端 + 响应适配层:
```go
// 每个 Provider 包内部
type client struct {
httpClient *http.Client
baseURL string
authFunc func(*http.Request) error // 注入认证策略
}
func (c *client) do(ctx context.Context, method, path string, body any) ([]byte, error)
func (c *client) get(ctx context.Context, path string, result any) error
```
认证逻辑作为策略注入 `authFunc`:
- 雨云:`func(r *http.Request) error { r.Header.Set("X-Api-Key", key); return nil }`
- 阿里云:`func(r *http.Request) error { signRequest(r, ak, sk); return nil }`
### 模式全景
```
用户命令 (cobra)
│
▼
Registry.Get("rainyun") ← 工厂模式:按名创建 Provider
│
▼
Provider.Server().List() ← 策略模式:不同实现,相同接口
│
▼
client.do(path, authFunc) ← 适配器模式:统一 HTTP 调用 + 注入认证
│
▼
parse → []Server ← 各 Provider 自行适配响应格式
```
---
## 配置系统
### 配置文件路径
`~/.config/ry/config.toml`
### 配置结构
```toml
# 当前使用的提供商
current_provider = "rainyun"
# 默认输出格式:table | json | yaml
output = "table"
# 提供商认证配置
[providers.rainyun]
api_key = "***"
# 可选:自定义 API 地址
# endpoint = "https://api.v2.rainyun.com"
# [providers.aliyun]
# access_key_id = "..."
# access_key_secret = "..."
# region = "cn-hangzhou"
```
### 配置操作
- `ry provider use <name>` — 切换当前提供商
- `ry provider add <name>` — 交互式录入凭证
- `ry provider list` — 列出已配置的提供商
- `ry config show` — 显示当前配置(API Key 脱敏为 `***`)
- `ry config set <key> <value>` — 修改配置项
- `ry config path` — 打印配置文件路径
### 环境变量
环境变量优先级高于配置文件:
- `RY_PROVIDER` → 覆盖 `current_provider`
- `RY_OUTPUT` → 覆盖 `output`
- `RY_{PROVIDER}_API_KEY` → 覆盖提供商 API Key(如 `RY_RAINYUN_API_KEY`)
---
## 命令树
```
ry
├── provider # 提供商管理
│ ├── use <name> # 切换当前提供商
│ ├── list # 列出已配置的提供商
│ └── add <name> # 交互式添加新提供商
│
├── server # 云服务器
│ ├── list # 列表,支持 --status 过滤
│ ├── get <id> # 详情
│ ├── start <id> # 开机
│ ├── stop <id> # 关机
│ ├── reboot <id> # 重启
│ ├── reinstall <id> # 重装系统 --os <name>
│ ├── reset-password <id> # 重置密码
│ ├── vnc <id> # 获取 VNC 链接
│ └── upgrade <id> # 升级配置 --plan <name>
│
├── domain # 域名
│ ├── list # 域名列表
│ └── dns <domain> # DNS 解析记录
│ ├── list # 列出记录
│ ├── add # 添加记录 --type A --name @ --value 1.2.3.4
│ └── delete <id> # 删除记录
│
├── storage # 对象存储
│ ├── list # 实例列表
│ └── bucket <instance-id> # 存储桶操作
│ ├── list # 桶列表
│ └── create <name> # 创建桶
│
├── billing # 计费
│ └── orders # 订单列表
│
├── config # 配置管理
│ ├── show # 显示当前配置(脱敏)
│ ├── set <key> <value> # 修改配置项
│ └── path # 打印配置文件路径
│
└── completion <shell> # 自动补全脚本(bash/zsh/fish)
```
### 全局 Flag
| Flag | 简写 | 说明 |
|------|------|------|
| `--provider` | `-p` | 临时覆盖提供商 |
| `--output` | `-o` | table / json / yaml |
| `--raw` | — | 输出提供商原始响应 |
| `--debug` | — | 输出详细调试信息 |
| `--timeout` | — | 请求超时秒数(默认 30) |
---
## 输出格式
### 表格模式(默认)
```
$ ry server list
ID NAME STATUS IP CPU MEM EXPIRES
12001 web-server running 171.80.11.70 2C 4GB 2026-08-15
12002 db-server stopped - 4C 8GB 2026-09-01
```
### JSON 模式
```
$ ry server list -o json
[{"id":"12001","name":"web-server","status":"running","ip":"171.80.11.70","cpu":2,"memory":4096,"expires":"2026-08-15T00:00:00Z"}]
```
### YAML 模式
```
$ ry server list -o yaml
- id: "12001"
name: web-server
status: running
```
### Raw 模式
```
$ ry server list --raw
--- 原始 API 响应,不做任何处理,适合 pipe 到 jq ---
```
---
## 错误处理
### 分层策略
```
API 层错误 → 包装为 Error 类型 → 用户友好提示(--debug 时显示原始信息)
```
### Error 类型
```go
type Error struct {
Code int // HTTP 状态码
Message string // 用户可读消息
Detail string // 原始错误信息(--debug 时显示)
Raw []byte // API 原始响应体
}
func (e *Error) Error() string { return e.Message }
```
### 效果示例
```
$ ry server start xxx
Error: 服务器不存在
Run 'ry server list' to see available servers.
$ ry server start xxx --debug
Error: GET https://api.v2.rainyun.com/product/rcs/xxx → 404
{"code":1002,"msg":"product not found"}
```
### 超时与重试
- 默认超时 30s(`--timeout` 可改)
- GET 请求自动重试 1 次(瞬时网络错误)
- POST/PATCH/DELETE 不重试(幂等性不可保证)
- 业务错误(4xx)不重试
---
## 测试策略
| 层 | 测试方式 | 内容 |
|---|---|---|
| `provider/rainyun` | HTTP mock(`httptest`) | API 调用路径、参数、错误处理 |
| `cmd/` | 集成测试 + golden files | 命令输出快照对比 |
| `config/` | 单元测试 | TOML 读写正确性 |
| `output/` | 单元测试 | 表格/JSON/YAML 渲染 |
- 测试框架:Go 标准库 `testing` + `net/http/httptest`
- 不引入 testify 等第三方测试库
- CI 中 `go test ./...` 一跑到底
---
## Go 依赖
| 包 | 用途 |
|---|---|
| `github.com/spf13/cobra` | CLI 命令框架 |
| `github.com/spf13/viper` | 配置 + 环境变量绑定 |
| `github.com/BurntSushi/toml` | TOML 解析 |
| `github.com/olekukoneko/tablewriter` | 表格渲染 |
| `gopkg.in/yaml.v3` | YAML 输出 |
---
## 扩展新提供商
1. 在 `internal/provider/` 下新建包(如 `aliyun/`)
2. 实现 `Provider` 接口
3. 在 `registry.go` 中注册:
```go
func init() {
Register("aliyun", aliyun.New)
}
```
4. 用户 `ry provider add aliyun` 输入凭证即可使用
初期所有 Provider 编译进二进制,后续可考虑 Go plugin 动态加载。
---
## 版本发布
- `go install github.com/xxx/ry/cmd/ry@latest`
- 预编译二进制放到 Gitea Release(`git.yoresee.cc`)
- GoReleaser 自动构建 linux/amd64 + darwin/amd64 + darwin/arm64
---
## 范围边界(第一期)
**包含**:
- 命令树中列出的所有命令(server/domain/storage/billing/config/provider/completion)
- 雨云 Provider 完整实现
- 配置系统(TOML + 环境变量)
- 三种输出格式 + raw
- 错误处理 + 调试模式
- 基础测试覆盖
**不含**:
- 游戏云 MCSM/Pterodactyl 面板操作(后续)
- 云应用 RCA 操作(后续)
- 工单系统(后续)
- 非雨云的其他 Provider(后续)
- GoReleaser 配置(后续)