From 72bbb4c8a0b738c51dc314a80e96079de92cc7bc Mon Sep 17 00:00:00 2001 From: Fendy Date: Tue, 28 Jul 2026 23:52:23 +0800 Subject: [PATCH 1/8] docs: verbose/debug mode design spec --- docs/spec/2026-07-28-verbose-debug-design.md | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/spec/2026-07-28-verbose-debug-design.md diff --git a/docs/spec/2026-07-28-verbose-debug-design.md b/docs/spec/2026-07-28-verbose-debug-design.md new file mode 100644 index 0000000..650025a --- /dev/null +++ b/docs/spec/2026-07-28-verbose-debug-design.md @@ -0,0 +1,70 @@ +# Verbose/Debug Mode Design + +**Date:** 2026-07-28 +**Status:** Approved + +## Goal + +Wire up the existing `--debug` flag (currently unused) and add `--verbose` as an alias. When enabled, print detailed runtime and HTTP request information to stderr. + +## Scope + +4 files modified, 0 new files. + +## Changes + +### 1. `internal/provider/provider.go` — ProviderConfig + +Add `Debug bool` field to `ProviderConfig`: + +```go +type ProviderConfig struct { + APIKey string + AccessKeyID string + AccessKeySecret string + Endpoint string + Debug bool +} +``` + +### 2. `cmd/cli/root.go` — Flag + Runtime Info + +- Register `--verbose` as alias for `--debug` (both set `flagDebug`) +- In `PersistentPreRunE`, when debug is on, print to stderr: + - Provider name + - Config file path + - Output format + +### 3. `internal/provider/rainyun/client.go` — HTTP Logging + +- Add `debug bool` field to `client` struct +- Update `newClient(endpoint, apiKey string, debug bool)` signature +- In `do()`, when `c.debug` is true, print to `os.Stderr`: + - Request: `> METHOD /path` + request body (if any) + - Response: `< StatusCode StatusText Duration` + response body + +### 4. `internal/provider/rainyun/rainyun.go` — Pass Debug + +- `New()` passes `cfg.Debug` to `newClient()` + +## Output Format + +All debug output goes to **stderr** to avoid polluting stdout data pipeline. + +``` +[debug] provider: rainyun +[debug] config: /Users/user/.config/ry/config.json +[debug] output: table +[debug] > GET /api/v2/servers +[debug] < 200 OK 123ms +[debug] {"code":200,"data":[...]} +``` + +## Files + +| File | Change | +|------|--------| +| `internal/provider/provider.go` | Add `Debug` to `ProviderConfig` | +| `cmd/cli/root.go` | Wire `--verbose`, print runtime info | +| `internal/provider/rainyun/client.go` | HTTP request/response logging | +| `internal/provider/rainyun/rainyun.go` | Pass `cfg.Debug` to client | From 86ced3e4bbc2449dbffaf4a33f75a6ae080cc2d2 Mon Sep 17 00:00:00 2001 From: Fendy Date: Tue, 28 Jul 2026 23:53:44 +0800 Subject: [PATCH 2/8] docs: verbose/debug implementation plan --- docs/plan/2026-07-28-verbose-debug-plan.md | 316 +++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 docs/plan/2026-07-28-verbose-debug-plan.md diff --git a/docs/plan/2026-07-28-verbose-debug-plan.md b/docs/plan/2026-07-28-verbose-debug-plan.md new file mode 100644 index 0000000..93eec7a --- /dev/null +++ b/docs/plan/2026-07-28-verbose-debug-plan.md @@ -0,0 +1,316 @@ +# Verbose/Debug Mode Implementation Plan + +> **For agentic workers:** Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Wire up `--debug`/`--verbose` flags to print HTTP request details and runtime info to stderr. + +**Architecture:** Add `Debug bool` to `ProviderConfig`, pass it through to the HTTP client. Client's `do()` method logs request/response when debug is on. Root command prints runtime info in `PersistentPreRunE`. + +**Tech Stack:** Go, cobra, net/http + +## Global Constraints + +- All debug output goes to `os.Stderr` +- `--verbose` is an alias for `--debug` (both set `flagDebug`) +- Debug prefix: `[debug]` +- Existing tests must pass after each task + +--- + +### Task 1: Add Debug field to ProviderConfig + +**Files:** +- Modify: `internal/provider/provider.go` + +**Interfaces:** +- Produces: `ProviderConfig.Debug bool` — consumed by Task 3 and Task 4 + +- [ ] **Step 1: Add Debug field** + +```go +// internal/provider/provider.go — add to ProviderConfig +type ProviderConfig struct { + APIKey string + AccessKeyID string + AccessKeySecret string + Endpoint string + Debug bool +} +``` + +- [ ] **Step 2: Verify build** + +Run: `go build ./...` +Expected: PASS (field addition is backward-compatible) + +- [ ] **Step 3: Commit** + +```bash +git add internal/provider/provider.go +git commit -m "feat: add Debug field to ProviderConfig" +``` + +--- + +### Task 2: Update client to accept and use debug flag + +**Files:** +- Modify: `internal/provider/rainyun/client.go` +- Modify: `internal/provider/rainyun/client_test.go` — update `newClient` calls +- Modify: `internal/provider/rainyun/server_test.go` — update `newClient` calls + +**Interfaces:** +- Consumes: `debug bool` parameter +- Produces: `newClient(endpoint, apiKey string, debug bool) *client` +- Produces: stderr debug output with `[debug]` prefix + +- [ ] **Step 1: Update client struct and newClient** + +```go +// internal/provider/rainyun/client.go + +type client struct { + httpClient *http.Client + baseURL string + apiKey string + debug bool +} + +func newClient(endpoint, apiKey string, debug bool) *client { + if endpoint == "" { + endpoint = defaultEndpoint + } + return &client{ + httpClient: &http.Client{Timeout: 30 * time.Second}, + baseURL: endpoint, + apiKey: apiKey, + debug: debug, + } +} +``` + +- [ ] **Step 2: Add debug logging to do()** + +Add to top of `client.go` imports: `"os"`, `"time"` (time already imported). + +In `do()`, after building the URL and before creating the request, add: + +```go +if c.debug { + fmt.Fprintf(os.Stderr, "[debug] > %s %s\n", method, u) + if body != nil { + fmt.Fprintf(os.Stderr, "[debug] %s\n", bodyReader.(*bytes.Reader). // wrong + } +} +``` + +Actually, the body bytes are already in `b` at that point. Better approach — log after marshaling: + +```go +func (c *client) do(ctx context.Context, method, path string, body, result any, query map[string]string) error { + u := c.baseURL + path + + if len(query) > 0 { + params := url.Values{} + for k, v := range query { + params.Set(k, v) + } + u += "?" + params.Encode() + } + + var bodyReader io.Reader + var bodyBytes []byte + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("marshal request: %w", err) + } + bodyBytes = b + bodyReader = bytes.NewReader(b) + } + + if c.debug { + fmt.Fprintf(os.Stderr, "[debug] > %s %s\n", method, u) + if bodyBytes != nil { + fmt.Fprintf(os.Stderr, "[debug] %s\n", bodyBytes) + } + } + + req, err := http.NewRequestWithContext(ctx, method, u, bodyReader) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + req.Header.Set("X-Api-Key", c.apiKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + start := time.Now() + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + + if c.debug { + fmt.Fprintf(os.Stderr, "[debug] < %d %s %s\n", resp.StatusCode, http.StatusText(resp.StatusCode), time.Since(start).Round(time.Millisecond)) + fmt.Fprintf(os.Stderr, "[debug] %s\n", respBody) + } + + // ... rest unchanged +``` + +- [ ] **Step 3: Update test calls to newClient** + +In `client_test.go`, change all `newClient(srv.URL, "sk-test")` to `newClient(srv.URL, "sk-test", false)`. + +In `server_test.go`, change all `newClient(srv.URL, "sk-test")` to `newClient(srv.URL, "sk-test", false)`. + +- [ ] **Step 4: Run tests** + +Run: `go test ./internal/provider/rainyun/...` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add internal/provider/rainyun/ +git commit -m "feat: HTTP debug logging in client.do()" +``` + +--- + +### Task 3: Pass debug flag from provider to client + +**Files:** +- Modify: `internal/provider/rainyun/rainyun.go` + +**Interfaces:** +- Consumes: `ProviderConfig.Debug` (from Task 1) +- Consumes: `newClient(endpoint, apiKey string, debug bool)` (from Task 2) + +- [ ] **Step 1: Update New()** + +```go +func New(cfg provider.ProviderConfig) (provider.Provider, error) { + c := newClient(cfg.Endpoint, cfg.APIKey, cfg.Debug) + // ... rest unchanged +} +``` + +- [ ] **Step 2: Run tests** + +Run: `go test ./internal/provider/rainyun/...` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add internal/provider/rainyun/rainyun.go +git commit -m "feat: pass Debug flag from config to client" +``` + +--- + +### Task 4: Wire --verbose alias and runtime info in root + +**Files:** +- Modify: `cmd/cli/root.go` + +**Interfaces:** +- Consumes: `flagDebug` (existing, now wired) +- Consumes: `ProviderConfig.Debug` (from Task 1) + +- [ ] **Step 1: Add --verbose alias** + +In `init()`, add after the `--debug` flag registration: + +```go +rootCmd.PersistentFlags().BoolVarP(&flagDebug, "verbose", "v", false, "Enable verbose/debug output") +``` + +Note: cobra doesn't support two flags binding to the same var directly. Instead, use `MarkHidden` or just register `--verbose` as the primary and `--debug` as hidden alias. Simplest approach: + +```go +rootCmd.PersistentFlags().BoolVarP(&flagDebug, "verbose", "v", false, "Enable verbose/debug output (same as --debug)") +rootCmd.PersistentFlags().BoolVar(&flagDebug, "debug", false, "Enable debug output (same as --verbose)") +``` + +- [ ] **Step 2: Wire flagDebug into ProviderConfig** + +In `PersistentPreRunE`, after creating `pc`: + +```go +prov, err = provider.New(providerName, provider.ProviderConfig{ + APIKey: pc.APIKey, + AccessKeyID: pc.AccessKeyID, + AccessKeySecret: pc.AccessKeySecret, + Endpoint: pc.Endpoint, + Debug: flagDebug, +}) +``` + +- [ ] **Step 3: Print runtime info when debug** + +At the end of `PersistentPreRunE`, after creating `out`: + +```go +if flagDebug { + fmt.Fprintf(os.Stderr, "[debug] provider: %s\n", providerName) + fmt.Fprintf(os.Stderr, "[debug] config: %s\n", cfgPath) + fmt.Fprintf(os.Stderr, "[debug] output: %s\n", outputFormat) +} +``` + +- [ ] **Step 4: Run full test suite** + +Run: `go test ./...` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add cmd/cli/root.go +git commit -m "feat: wire --verbose/--debug flags with runtime info" +``` + +--- + +### Task 5: Manual verification + +- [ ] **Step 1: Build and test verbose output** + +```bash +go build -o ry ./cmd/cli/ +./ry --verbose server list 2>&1 | head -20 +``` + +Expected stderr output: +``` +[debug] provider: rainyun +[debug] config: /path/to/config.json +[debug] output: table +[debug] > GET /product/rcs/?Page=1&PageSize=999&PerPage=999 +[debug] < 200 OK 123ms +[debug] {"code":200,"data":{...}} +``` + +- [ ] **Step 2: Verify --debug works too** + +```bash +./ry --debug server list 2>&1 | head -5 +``` + +Expected: same `[debug]` output + +- [ ] **Step 3: Verify no debug output without flag** + +```bash +./ry server list 2>/dev/null +``` + +Expected: clean table output, no debug lines From b7085af9d3e363e402a65dbcf74fba768dacfa9d Mon Sep 17 00:00:00 2001 From: Fendy Date: Tue, 28 Jul 2026 23:55:50 +0800 Subject: [PATCH 3/8] feat: add Debug field to ProviderConfig --- internal/provider/provider.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8b623ea..69b48b4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -11,6 +11,7 @@ type ProviderConfig struct { AccessKeyID string AccessKeySecret string Endpoint string + Debug bool } type Provider interface { From 9faf881efa5101cd2a6b0e9fc6e6cd8329b3501d Mon Sep 17 00:00:00 2001 From: Fendy Date: Tue, 28 Jul 2026 23:57:24 +0800 Subject: [PATCH 4/8] feat: HTTP debug logging in client.do() --- internal/provider/rainyun/client.go | 20 +++++++++++++++++++- internal/provider/rainyun/client_test.go | 4 ++-- internal/provider/rainyun/rainyun.go | 2 +- internal/provider/rainyun/server_test.go | 6 +++--- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/internal/provider/rainyun/client.go b/internal/provider/rainyun/client.go index 2f792c7..f3662d2 100644 --- a/internal/provider/rainyun/client.go +++ b/internal/provider/rainyun/client.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "os" "time" ) @@ -17,9 +18,10 @@ type client struct { httpClient *http.Client baseURL string apiKey string + debug bool } -func newClient(endpoint, apiKey string) *client { +func newClient(endpoint, apiKey string, debug bool) *client { if endpoint == "" { endpoint = defaultEndpoint } @@ -27,6 +29,7 @@ func newClient(endpoint, apiKey string) *client { httpClient: &http.Client{Timeout: 30 * time.Second}, baseURL: endpoint, apiKey: apiKey, + debug: debug, } } @@ -85,14 +88,23 @@ func (c *client) do(ctx context.Context, method, path string, body, result any, } var bodyReader io.Reader + var bodyBytes []byte if body != nil { b, err := json.Marshal(body) if err != nil { return fmt.Errorf("marshal request: %w", err) } + bodyBytes = b bodyReader = bytes.NewReader(b) } + if c.debug { + fmt.Fprintf(os.Stderr, "[debug] > %s %s\n", method, u) + if bodyBytes != nil { + fmt.Fprintf(os.Stderr, "[debug] %s\n", bodyBytes) + } + } + req, err := http.NewRequestWithContext(ctx, method, u, bodyReader) if err != nil { return fmt.Errorf("create request: %w", err) @@ -101,6 +113,7 @@ func (c *client) do(ctx context.Context, method, path string, body, result any, req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") + start := time.Now() resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) @@ -112,6 +125,11 @@ func (c *client) do(ctx context.Context, method, path string, body, result any, return fmt.Errorf("read response: %w", err) } + if c.debug { + fmt.Fprintf(os.Stderr, "[debug] < %d %s %s\n", resp.StatusCode, http.StatusText(resp.StatusCode), time.Since(start).Round(time.Millisecond)) + fmt.Fprintf(os.Stderr, "[debug] %s\n", respBody) + } + var apiResp apiResponse if err := json.Unmarshal(respBody, &apiResp); err == nil { // Rainyun uses code:200 and code:0 both as success indicators diff --git a/internal/provider/rainyun/client_test.go b/internal/provider/rainyun/client_test.go index 13ee17c..bf1e890 100644 --- a/internal/provider/rainyun/client_test.go +++ b/internal/provider/rainyun/client_test.go @@ -16,7 +16,7 @@ func TestClientAuthHeader(t *testing.T) { })) defer srv.Close() - c := newClient(srv.URL, "sk-test") + c := newClient(srv.URL, "sk-test", false) var result struct { ID int `json:"id"` Name string `json:"name"` @@ -40,7 +40,7 @@ func TestClientAPIError(t *testing.T) { })) defer srv.Close() - c := newClient(srv.URL, "sk-test") + c := newClient(srv.URL, "sk-test", false) var result any err := c.get(context.Background(), "/error", &result) if err == nil { diff --git a/internal/provider/rainyun/rainyun.go b/internal/provider/rainyun/rainyun.go index e827d20..f6563bc 100644 --- a/internal/provider/rainyun/rainyun.go +++ b/internal/provider/rainyun/rainyun.go @@ -21,7 +21,7 @@ type Rainyun struct { // New creates a Rainyun provider from config. func New(cfg provider.ProviderConfig) (provider.Provider, error) { - c := newClient(cfg.Endpoint, cfg.APIKey) + c := newClient(cfg.Endpoint, cfg.APIKey, cfg.Debug) r := &Rainyun{client: c} r.server = &ServerService{client: c} r.domain = &DomainService{client: c} diff --git a/internal/provider/rainyun/server_test.go b/internal/provider/rainyun/server_test.go index 0a45faa..3b7668a 100644 --- a/internal/provider/rainyun/server_test.go +++ b/internal/provider/rainyun/server_test.go @@ -36,7 +36,7 @@ func TestServerList(t *testing.T) { })) defer srv.Close() - rain := &Rainyun{client: newClient(srv.URL, "sk-test")} + rain := &Rainyun{client: newClient(srv.URL, "sk-test", false)} svc := rain.Server() servers, err := svc.List(context.Background()) if err != nil { @@ -66,7 +66,7 @@ func TestServerStart(t *testing.T) { })) defer srv.Close() - rain := &Rainyun{client: newClient(srv.URL, "sk-test")} + rain := &Rainyun{client: newClient(srv.URL, "sk-test", false)} err := rain.Server().Start(context.Background(), "12001") if err != nil { t.Fatalf("Start() error: %v", err) @@ -83,7 +83,7 @@ func TestServerVNC(t *testing.T) { })) defer srv.Close() - rain := &Rainyun{client: newClient(srv.URL, "sk-test")} + rain := &Rainyun{client: newClient(srv.URL, "sk-test", false)} url, err := rain.Server().GetVNC(context.Background(), "12001") if err != nil { t.Fatalf("GetVNC() error: %v", err) From 468262305e2127045e661904d717f7dfac3c6eac Mon Sep 17 00:00:00 2001 From: Fendy Date: Tue, 28 Jul 2026 23:59:05 +0800 Subject: [PATCH 5/8] feat: wire --verbose/--debug flags with runtime info --- cmd/cli/root.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 35bf575..8d4eb80 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -67,6 +67,7 @@ Use "ry [command] --help" for more information about a command.`, AccessKeyID: pc.AccessKeyID, AccessKeySecret: pc.AccessKeySecret, Endpoint: pc.Endpoint, + Debug: flagDebug, }) if err != nil { return err @@ -80,6 +81,11 @@ Use "ry [command] --help" for more information about a command.`, outputFormat = "raw" } out = output.New(outputFormat, os.Stdout) + if flagDebug { + fmt.Fprintf(os.Stderr, "[debug] provider: %s\n", providerName) + fmt.Fprintf(os.Stderr, "[debug] config: %s\n", cfgPath) + fmt.Fprintf(os.Stderr, "[debug] output: %s\n", outputFormat) + } return nil }, } @@ -89,6 +95,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&flagProvider, "provider", "p", "", "Cloud provider to use") rootCmd.PersistentFlags().StringVarP(&flagOutput, "output", "o", "", "Output format: table, json, yaml, raw") rootCmd.PersistentFlags().BoolVar(&flagRaw, "raw", false, "Output raw API response") + rootCmd.PersistentFlags().BoolVarP(&flagDebug, "verbose", "v", false, "Enable verbose/debug output") rootCmd.PersistentFlags().BoolVar(&flagDebug, "debug", false, "Enable debug output") completionCmd := &cobra.Command{ From e6e9ad01e15c58e322294e84722d1440d1a3319b Mon Sep 17 00:00:00 2001 From: Fendy Date: Wed, 29 Jul 2026 00:04:16 +0800 Subject: [PATCH 6/8] refactor: remove --debug flag, keep --verbose/-v only --- cmd/cli/root.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 8d4eb80..1cfb03c 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -26,7 +26,7 @@ var ( flagProvider string flagOutput string flagRaw bool - flagDebug bool + flagVerbose bool ) var rootCmd = &cobra.Command{ @@ -67,7 +67,7 @@ Use "ry [command] --help" for more information about a command.`, AccessKeyID: pc.AccessKeyID, AccessKeySecret: pc.AccessKeySecret, Endpoint: pc.Endpoint, - Debug: flagDebug, + Debug: flagVerbose, }) if err != nil { return err @@ -81,7 +81,7 @@ Use "ry [command] --help" for more information about a command.`, outputFormat = "raw" } out = output.New(outputFormat, os.Stdout) - if flagDebug { + if flagVerbose { fmt.Fprintf(os.Stderr, "[debug] provider: %s\n", providerName) fmt.Fprintf(os.Stderr, "[debug] config: %s\n", cfgPath) fmt.Fprintf(os.Stderr, "[debug] output: %s\n", outputFormat) @@ -95,8 +95,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&flagProvider, "provider", "p", "", "Cloud provider to use") rootCmd.PersistentFlags().StringVarP(&flagOutput, "output", "o", "", "Output format: table, json, yaml, raw") rootCmd.PersistentFlags().BoolVar(&flagRaw, "raw", false, "Output raw API response") - rootCmd.PersistentFlags().BoolVarP(&flagDebug, "verbose", "v", false, "Enable verbose/debug output") - rootCmd.PersistentFlags().BoolVar(&flagDebug, "debug", false, "Enable debug output") + rootCmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "Enable verbose output") completionCmd := &cobra.Command{ Use: "completion [bash|zsh|fish]", From 317dc9c4e517e27dac9aa632947c903f72bb2cc7 Mon Sep 17 00:00:00 2001 From: Fendy Date: Wed, 29 Jul 2026 00:39:28 +0800 Subject: [PATCH 7/8] fix: correct rainyunServer JSON field names to match actual API (PascalCase) --- internal/provider/rainyun/server.go | 22 +++++++++++----------- internal/provider/rainyun/server_test.go | 19 ++++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/internal/provider/rainyun/server.go b/internal/provider/rainyun/server.go index 28b257c..d290af1 100644 --- a/internal/provider/rainyun/server.go +++ b/internal/provider/rainyun/server.go @@ -16,15 +16,15 @@ type ServerService struct { // rainyunServer mirrors the Rainyun RCS API JSON fields (PascalCase). type rainyunServer struct { ID int `json:"ID"` - Name string `json:"name"` + HostName string `json:"HostName"` Status string `json:"status"` - IP string `json:"ip"` - VCPU int `json:"vcpu"` + MainIPv4 string `json:"MainIPv4"` + CPU int `json:"CPU"` Memory int `json:"memory"` Disk int `json:"disk"` - OS string `json:"os"` - Region string `json:"zone_name"` - ExpireDate int64 `json:"ExpDate"` // Unix timestamp + OsName string `json:"OsName"` + Zone string `json:"Zone"` + ExpireDate int64 `json:"ExpDate"` } func (s *ServerService) List(ctx context.Context) ([]provider.Server, error) { @@ -90,14 +90,14 @@ func toServer(r rainyunServer) provider.Server { expire := time.Unix(r.ExpireDate, 0) return provider.Server{ ID: fmt.Sprintf("%d", r.ID), - Name: r.Name, + Name: r.HostName, Status: r.Status, - IP: r.IP, - CPU: r.VCPU, + IP: r.MainIPv4, + CPU: r.CPU, Memory: r.Memory, Disk: r.Disk, - OS: r.OS, - Region: r.Region, + OS: r.OsName, + Region: r.Zone, ExpireAt: expire, Raw: r, } diff --git a/internal/provider/rainyun/server_test.go b/internal/provider/rainyun/server_test.go index 3b7668a..c4be8be 100644 --- a/internal/provider/rainyun/server_test.go +++ b/internal/provider/rainyun/server_test.go @@ -18,15 +18,16 @@ func TestServerList(t *testing.T) { "data": map[string]any{ "Records": []map[string]any{ { - "ID": 12001, - "name": "web-server", - "status": "running", - "ip": "1.2.3.4", - "vcpu": 2, - "memory": 4096, - "disk": 50, - "os": "Ubuntu 22.04", - "ExpDate": 1756656000, + "ID": 12001, + "HostName": "web-server", + "status": "running", + "MainIPv4": "1.2.3.4", + "CPU": 2, + "memory": 4096, + "disk": 50, + "OsName": "Ubuntu 22.04", + "Zone": "cn-sy1", + "ExpDate": 1756656000, }, }, "TotalRecords": 1, From 38d5b3f3380c77776d278c36e7ef4e027756a211 Mon Sep 17 00:00:00 2001 From: Fendy Date: Wed, 29 Jul 2026 00:54:28 +0800 Subject: [PATCH 8/8] fix: unwrap nested Data field in server Get response --- internal/provider/rainyun/server.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/provider/rainyun/server.go b/internal/provider/rainyun/server.go index d290af1..c3670b3 100644 --- a/internal/provider/rainyun/server.go +++ b/internal/provider/rainyun/server.go @@ -40,12 +40,14 @@ func (s *ServerService) List(ctx context.Context) ([]provider.Server, error) { } func (s *ServerService) Get(ctx context.Context, id string) (*provider.Server, error) { - var raw rainyunServer + var resp struct { + Data rainyunServer `json:"Data"` + } path := fmt.Sprintf("/product/rcs/%s/", id) - if err := s.client.get(ctx, path, &raw); err != nil { + if err := s.client.get(ctx, path, &resp); err != nil { return nil, err } - sv := toServer(raw) + sv := toServer(resp.Data) return &sv, nil }