Skip to content

Commit 36ac5fa

Browse files
committed
Improve CLI resilience and JSON output
1 parent 74842ae commit 36ac5fa

25 files changed

Lines changed: 998 additions & 176 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
done
4444
(
4545
cd dist
46-
sha256sum ./*.tar.gz > checksums.txt
46+
sha256sum cloudcanal_*.tar.gz > checksums.txt
4747
)
4848
4949
- name: Publish GitHub Release

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ curl -fsSL https://raw.githubusercontent.com/Arlowen/cloudcanal-openapi-cli/main
3232
```
3333

3434
这个一键安装脚本会从 GitHub Releases 下载预编译二进制,不需要本机安装 Go。
35+
下载后会自动校验 release 中的 `checksums.txt`,再执行安装。
3536
默认会把二进制安装到 `~/.local/share/cloudcanal-openapi-cli/bin/cloudcanal`
3637

3738
一键卸载:
@@ -49,17 +50,37 @@ curl -fsSL https://raw.githubusercontent.com/Arlowen/cloudcanal-openapi-cli/main
4950
## 使用方式
5051

5152
详细命令、参数和示例请看上面的使用说明文档。
53+
如需机器可读输出,可以在命令后追加 `--output json`,例如:
54+
55+
```bash
56+
cloudcanal jobs list --type SYNC --output json
57+
```
5258

5359
## 初始化配置
5460

5561
第一次启动会进入初始化向导,配置文件保存到 `~/.cloudcanal/config.json`。配置格式、字段含义和命令参数说明见详细文档。
5662

63+
高级网络选项也可以直接写进配置文件:
64+
65+
```json
66+
{
67+
"apiBaseUrl": "https://cc.example.com",
68+
"accessKey": "your-ak",
69+
"secretKey": "your-sk",
70+
"language": "en",
71+
"httpTimeoutSeconds": 15,
72+
"httpReadMaxRetries": 2,
73+
"httpReadRetryBackoffMillis": 300
74+
}
75+
```
76+
5777
## 开发
5878

5979
发布:
6080

6181
- 推送 tag,例如 `v0.1.0`
6282
- GitHub Actions 会自动构建并发布 `darwin/linux + amd64/arm64` 的 release 资产
83+
- Release 会同时生成 `checksums.txt`,供一键安装脚本做完整性校验
6384

6485
只编译:
6586

cmd/cloudcanal/main.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"cloudcanal-openapi-cli/internal/console"
1111
"cloudcanal-openapi-cli/internal/i18n"
1212
"cloudcanal-openapi-cli/internal/repl"
13-
"cloudcanal-openapi-cli/internal/util"
1413
)
1514

1615
func main() {
@@ -35,14 +34,14 @@ func main() {
3534
shell := repl.NewShell(io, runtime)
3635
if len(os.Args) > 1 {
3736
if err := shell.ExecuteArgs(os.Args[1:]); err != nil {
38-
io.Println(i18n.T("common.fatalErrorPrefix", util.SummarizeError(err)))
37+
shell.PrintFatalError(err)
3938
os.Exit(1)
4039
}
4140
return
4241
}
4342

4443
if err := shell.Run(); err != nil {
45-
io.Println(i18n.T("common.fatalErrorPrefix", util.SummarizeError(err)))
44+
shell.PrintFatalError(err)
4645
os.Exit(1)
4746
}
4847
}

docs/cloudcanal-cli-usage.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ cloudcanal
1515
```bash
1616
cloudcanal jobs list
1717
cloudcanal datasources list --type MYSQL
18+
cloudcanal jobs list --type SYNC --output json
1819
```
1920

2021
如果还没有安装到系统命令,也可以直接执行本地二进制:
@@ -32,6 +33,7 @@ curl -fsSL https://raw.githubusercontent.com/Arlowen/cloudcanal-openapi-cli/main
3233
说明:
3334

3435
- 当前一键安装会从 GitHub Releases 下载预编译二进制
36+
- 下载后会自动校验 release 里的 `checksums.txt`
3537
- 不需要本机安装 `Go`
3638
- 默认会把二进制安装到 `~/.local/share/cloudcanal-openapi-cli/bin/cloudcanal`
3739
- 之后会自动完成命令、PATH 和补全安装
@@ -57,7 +59,10 @@ curl -fsSL https://raw.githubusercontent.com/Arlowen/cloudcanal-openapi-cli/main
5759
"apiBaseUrl": "https://cc.example.com",
5860
"accessKey": "your-ak",
5961
"secretKey": "your-sk",
60-
"language": "en"
62+
"language": "en",
63+
"httpTimeoutSeconds": 15,
64+
"httpReadMaxRetries": 2,
65+
"httpReadRetryBackoffMillis": 300
6166
}
6267
```
6368

@@ -67,6 +72,9 @@ curl -fsSL https://raw.githubusercontent.com/Arlowen/cloudcanal-openapi-cli/main
6772
- `accessKey` 是访问密钥 ID
6873
- `secretKey` 是访问密钥 Secret,不会在 `config show` 中明文展示
6974
- `language` 是 CLI 文案语言,支持 `en``zh`
75+
- `httpTimeoutSeconds` 是单次 HTTP 请求超时秒数,默认 `10`
76+
- `httpReadMaxRetries` 是只读请求的最大重试次数,默认 `0`
77+
- `httpReadRetryBackoffMillis` 是只读请求的首次退避毫秒数,默认 `250`
7078

7179
## 基本命令
7280

@@ -127,12 +135,14 @@ cloudcanal completion bash
127135
- `--desc <desc>`: 按任务描述过滤
128136
- `--source-id <id>`: 按源数据源 ID 过滤
129137
- `--target-id <id>`: 按目标数据源 ID 过滤
138+
- `--output <text|json>`: 输出文本表格或 JSON
130139

131140
示例:
132141

133142
```bash
134143
cloudcanal jobs list --type SYNC --name demo
135144
cloudcanal jobs list --desc "nightly sync"
145+
cloudcanal jobs list --type SYNC --output json
136146
```
137147

138148
`jobs show <jobId>`
@@ -256,6 +266,7 @@ cloudcanal job-config specs --type SYNC --initial-sync=true
256266
## 使用建议
257267

258268
- 带空格的参数值请使用引号包裹,例如 `--desc "nightly sync"`
269+
- 可以在查询类命令后追加 `--output json` 获取机器可读结果
259270
- 交互模式下如果终端支持行编辑,可以直接使用 `TAB` 补全命令、子命令和常见参数
260271
- 可以先执行 `cloudcanal help` 查看帮助主题,再执行 `cloudcanal help jobs` 这类子帮助查看参数含义
261272
- 如果想切换中文或英文文案,可执行 `cloudcanal lang set zh``cloudcanal lang set en`

internal/app/runtime.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,7 @@ func (r *Runtime) validateConfig(cfg config.AppConfig) error {
118118
if err != nil {
119119
return err
120120
}
121-
service := datajob.NewService(client)
122-
_, err = service.ListJobs(datajob.ListOptions{})
123-
return err
121+
return client.ProbeAuthentication()
124122
}
125123

126124
func (r *Runtime) activate(cfg config.AppConfig) error {

internal/cluster/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ type listResponse struct {
4949

5050
func (s *Service) List(options ListOptions) ([]Cluster, error) {
5151
var out listResponse
52-
if err := s.client.PostJSON(listPath, listRequest{
52+
if err := s.client.PostJSONWithOptions(listPath, listRequest{
5353
CloudOrIDCName: options.CloudOrIDCName,
5454
ClusterDesc: options.ClusterDesc,
5555
ClusterName: options.ClusterName,
5656
Region: options.Region,
57-
}, &out); err != nil {
57+
}, &out, openapi.RequestOptions{Retryable: true}); err != nil {
5858
return nil, err
5959
}
6060
if err := openapi.EnsureSuccess(out.Response, "failed to list clusters"); err != nil {

internal/config/config.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@ import (
88
"os"
99
"path/filepath"
1010
"strings"
11+
"time"
12+
)
13+
14+
const (
15+
defaultHTTPTimeoutSeconds = 10
16+
defaultHTTPReadRetryBackoffMillis = 250
1117
)
1218

1319
type AppConfig struct {
14-
APIBaseURL string `json:"apiBaseUrl"`
15-
AccessKey string `json:"accessKey"`
16-
SecretKey string `json:"secretKey"`
17-
Language string `json:"language,omitempty"`
20+
APIBaseURL string `json:"apiBaseUrl"`
21+
AccessKey string `json:"accessKey"`
22+
SecretKey string `json:"secretKey"`
23+
Language string `json:"language,omitempty"`
24+
HTTPTimeoutSeconds int `json:"httpTimeoutSeconds,omitempty"`
25+
HTTPReadMaxRetries int `json:"httpReadMaxRetries,omitempty"`
26+
HTTPReadRetryBackoffMillis int `json:"httpReadRetryBackoffMillis,omitempty"`
1827
}
1928

2029
func (c AppConfig) Validate() error {
@@ -31,6 +40,15 @@ func (c AppConfig) Validate() error {
3140
if normalized := i18n.NormalizeLanguage(c.Language); normalized == "" && strings.TrimSpace(c.Language) != "" {
3241
return errors.New(i18n.T("config.languageUnsupported"))
3342
}
43+
if c.HTTPTimeoutSeconds < 0 {
44+
return errors.New(i18n.TFor(language, "config.httpTimeoutSecondsInvalid"))
45+
}
46+
if c.HTTPReadMaxRetries < 0 {
47+
return errors.New(i18n.TFor(language, "config.httpReadMaxRetriesInvalid"))
48+
}
49+
if c.HTTPReadRetryBackoffMillis < 0 {
50+
return errors.New(i18n.TFor(language, "config.httpReadRetryBackoffMillisInvalid"))
51+
}
3452

3553
parsed, err := url.Parse(strings.TrimSpace(c.APIBaseURL))
3654
if err != nil {
@@ -63,6 +81,35 @@ func (c AppConfig) WithDefaults() AppConfig {
6381
return c
6482
}
6583

84+
func (c AppConfig) HTTPTimeout() time.Duration {
85+
return time.Duration(c.HTTPTimeoutSecondsValue()) * time.Second
86+
}
87+
88+
func (c AppConfig) HTTPTimeoutSecondsValue() int {
89+
if c.HTTPTimeoutSeconds <= 0 {
90+
return defaultHTTPTimeoutSeconds
91+
}
92+
return c.HTTPTimeoutSeconds
93+
}
94+
95+
func (c AppConfig) HTTPReadMaxRetriesValue() int {
96+
if c.HTTPReadMaxRetries <= 0 {
97+
return 0
98+
}
99+
return c.HTTPReadMaxRetries
100+
}
101+
102+
func (c AppConfig) HTTPReadRetryBackoff() time.Duration {
103+
return time.Duration(c.HTTPReadRetryBackoffMillisValue()) * time.Millisecond
104+
}
105+
106+
func (c AppConfig) HTTPReadRetryBackoffMillisValue() int {
107+
if c.HTTPReadRetryBackoffMillis <= 0 {
108+
return defaultHTTPReadRetryBackoffMillis
109+
}
110+
return c.HTTPReadRetryBackoffMillis
111+
}
112+
66113
type Service struct {
67114
path string
68115
}

internal/consolejob/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ type queryResponse struct {
5757

5858
func (s *Service) Get(consoleJobID int64) (Job, error) {
5959
var out queryResponse
60-
if err := s.client.PostJSON(queryPath, queryRequest{ConsoleJobID: consoleJobID}, &out); err != nil {
60+
if err := s.client.PostJSONWithOptions(queryPath, queryRequest{ConsoleJobID: consoleJobID}, &out, openapi.RequestOptions{Retryable: true}); err != nil {
6161
return Job{}, err
6262
}
6363
if err := openapi.EnsureSuccess(out.Response, "failed to query console job"); err != nil {

internal/datajob/service.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ type queryJobSchemaResponse struct {
128128

129129
func (s *Service) ListJobs(options ListOptions) ([]Job, error) {
130130
var out listJobsResponse
131-
if err := s.client.PostJSON(listPath, newListJobsRequest(options), &out); err != nil {
131+
if err := s.client.PostJSONWithOptions(listPath, newListJobsRequest(options), &out, openapi.RequestOptions{Retryable: true}); err != nil {
132132
return nil, err
133133
}
134134
if err := openapi.EnsureSuccess(out.Response, "failed to list jobs"); err != nil {
@@ -142,7 +142,7 @@ func (s *Service) ListJobs(options ListOptions) ([]Job, error) {
142142

143143
func (s *Service) GetJob(jobID int64) (Job, error) {
144144
var out queryJobResponse
145-
if err := s.client.PostJSON(queryPath, jobActionRequest{JobID: jobID}, &out); err != nil {
145+
if err := s.client.PostJSONWithOptions(queryPath, jobActionRequest{JobID: jobID}, &out, openapi.RequestOptions{Retryable: true}); err != nil {
146146
return Job{}, err
147147
}
148148
if err := openapi.EnsureSuccess(out.Response, "failed to query job"); err != nil {
@@ -153,7 +153,7 @@ func (s *Service) GetJob(jobID int64) (Job, error) {
153153

154154
func (s *Service) GetJobSchema(jobID int64) (JobSchema, error) {
155155
var out queryJobSchemaResponse
156-
if err := s.client.PostJSON(schemaPath, jobActionRequest{JobID: jobID}, &out); err != nil {
156+
if err := s.client.PostJSONWithOptions(schemaPath, jobActionRequest{JobID: jobID}, &out, openapi.RequestOptions{Retryable: true}); err != nil {
157157
return JobSchema{}, err
158158
}
159159
if err := openapi.EnsureSuccess(out.Response, "failed to query job schema"); err != nil {

internal/datasource/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type listResponse struct {
5858

5959
func (s *Service) List(options ListOptions) ([]DataSource, error) {
6060
var out listResponse
61-
if err := s.client.PostJSON(listPath, newListRequest(options), &out); err != nil {
61+
if err := s.client.PostJSONWithOptions(listPath, newListRequest(options), &out, openapi.RequestOptions{Retryable: true}); err != nil {
6262
return nil, err
6363
}
6464
if err := openapi.EnsureSuccess(out.Response, "failed to list data sources"); err != nil {

0 commit comments

Comments
 (0)