package cli import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // APIClient is a REST client for the Multica server API. // Used by ctrl subcommands (agent, runtime, status, etc.). type APIClient struct { BaseURL string WorkspaceID string HTTPClient *http.Client } // NewAPIClient creates a new API client for ctrl commands. func NewAPIClient(baseURL, workspaceID string) *APIClient { return &APIClient{ BaseURL: strings.TrimRight(baseURL, "/"), WorkspaceID: workspaceID, HTTPClient: &http.Client{Timeout: 15 * time.Second}, } } // GetJSON performs a GET request and decodes the JSON response. func (c *APIClient) GetJSON(ctx context.Context, path string, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+path, nil) if err != nil { return err } if c.WorkspaceID != "" { req.Header.Set("X-Workspace-ID", c.WorkspaceID) } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return fmt.Errorf("GET %s returned %d: %s", path, resp.StatusCode, strings.TrimSpace(string(data))) } if out == nil { return nil } return json.NewDecoder(resp.Body).Decode(out) } // DeleteJSON performs a DELETE request. func (c *APIClient) DeleteJSON(ctx context.Context, path string) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.BaseURL+path, nil) if err != nil { return err } if c.WorkspaceID != "" { req.Header.Set("X-Workspace-ID", c.WorkspaceID) } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return fmt.Errorf("DELETE %s returned %d: %s", path, resp.StatusCode, strings.TrimSpace(string(data))) } return nil } // PutJSON performs a PUT request with a JSON body. func (c *APIClient) PutJSON(ctx context.Context, path string, body any, out any) error { data, err := json.Marshal(body) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.BaseURL+path, bytes.NewReader(data)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") if c.WorkspaceID != "" { req.Header.Set("X-Workspace-ID", c.WorkspaceID) } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { respData, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return fmt.Errorf("PUT %s returned %d: %s", path, resp.StatusCode, strings.TrimSpace(string(respData))) } if out == nil { return nil } return json.NewDecoder(resp.Body).Decode(out) } // HealthCheck hits the /health endpoint and returns the response body. func (c *APIClient) HealthCheck(ctx context.Context) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/health", nil) if err != nil { return "", err } resp, err := c.HTTPClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) if resp.StatusCode >= 400 { return "", fmt.Errorf("health check returned %d: %s", resp.StatusCode, strings.TrimSpace(string(data))) } return strings.TrimSpace(string(data)), nil }