Files
cloud-ip-validator/internal/apiclient/apiclient.go
T

65 lines
1.8 KiB
Go
Raw Normal View History

2026-08-21 07:34:45 +03:00
// Package apiclient is a thin HTTP client for the Control API's /api/v1
// surface, shared by the validator-agent and prober binaries. Neither
// binary talks to the database directly — this is their only channel to
// shared state, keeping both genuinely stateless.
package apiclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
BaseURL string
HTTPClient *http.Client
}
func New(baseURL string, timeout time.Duration) *Client {
return &Client{BaseURL: baseURL, HTTPClient: &http.Client{Timeout: timeout}}
}
// Do issues a JSON request and decodes a JSON response into out (if
// non-nil). A 204 response is treated as "no content" and out is left
// untouched, with ok=false — used for the assignment poll's empty case.
func (c *Client) Do(ctx context.Context, method, path string, body, out interface{}) (ok bool, err error) {
var reader io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return false, fmt.Errorf("marshal request: %w", err)
}
reader = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
if err != nil {
return false, fmt.Errorf("build request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return false, fmt.Errorf("%s %s: %w", method, path, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return false, nil
}
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
return false, fmt.Errorf("%s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
}
if out != nil && len(respBody) > 0 {
if err := json.Unmarshal(respBody, out); err != nil {
return false, fmt.Errorf("%s %s: decode response: %w", method, path, err)
}
}
return true, nil
}