package emapi import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "go.uber.org/zap" "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" ) const ( defaultHTTPTimeout = 20 * time.Second defaultBaseURL = "https://api.equinix.com/metal/v1" authHeader = "X-Auth-Token" consumerHeader = "X-Consumer-Token" staffHeader = "X-Packet-Staff" staffHeaderValue = "true" ) var DefaultHTTPClient = &http.Client{ Timeout: defaultHTTPTimeout, } var _ providers.Provider = &Client{} type Client struct { logger *zap.SugaredLogger httpClient *http.Client baseURL *url.URL authToken string consumerToken string } func (c *Client) Do(req *http.Request, out any) (*http.Response, error) { if c.authToken != "" { req.Header.Set(authHeader, c.authToken) } if c.consumerToken != "" { req.Header.Set(consumerHeader, c.consumerToken) } if c.authToken != "" && c.consumerToken != "" { req.Header.Set(staffHeader, staffHeaderValue) } resp, err := c.httpClient.Do(req) if err != nil { return nil, err } if out != nil { defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(out); err != nil { return resp, err } } return resp, nil } func (c *Client) DoRequest(ctx context.Context, method, path string, body io.Reader, out any) (*http.Response, error) { path = strings.TrimPrefix(path, c.baseURL.Path) pathURL, err := url.Parse(path) if err != nil { return nil, err } url := c.baseURL.JoinPath(pathURL.Path) query := url.Query() for k, v := range pathURL.Query() { query[k] = v } url.RawQuery = query.Encode() req, err := http.NewRequestWithContext(ctx, method, url.String(), body) if err != nil { return nil, fmt.Errorf("failed to build request: %w", err) } resp, err := c.Do(req, out) if err != nil { return nil, fmt.Errorf("failed to get response: %w", err) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } return resp, nil } // New creates a new emapi client func New(options ...Option) (*Client, error) { client := &Client{} for _, opt := range options { if err := opt(client); err != nil { return nil, err } } if client.logger == nil { client.logger = zap.NewNop().Sugar() } if client.httpClient == nil { client.httpClient = DefaultHTTPClient } if client.baseURL == nil { u, err := url.Parse(defaultBaseURL) if err != nil { return nil, fmt.Errorf("failed to parse emapi base url %s: %w", defaultBaseURL, err) } client.baseURL = u } return client, nil }