chore: reorganize docs — spec/plan/provider subdirs
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
# 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 配置(后续)
|
||||
Reference in New Issue
Block a user