457 lines
14 KiB
Markdown
457 lines
14 KiB
Markdown
# 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 配置(后续)
|