Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor HTTP clients #617

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pkg/clients/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package clients

// Authenticator is an interface for handling authentication with a service
type Authenticator interface {
GetToken() string
RefreshToken() error
NeedsTokenRefresh() error
}
115 changes: 59 additions & 56 deletions pkg/clients/cloud_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
Expand Down Expand Up @@ -41,109 +40,72 @@ type CloudProviderResponse struct {

// CloudAPIClient is a client for interacting with the Materialize Cloud API
type CloudAPIClient struct {
HTTPClient *http.Client
FronteggClient *FronteggClient
Endpoint string
BaseEndpoint string
HTTPClient *http.Client
Authenticator Authenticator
Endpoint string
BaseEndpoint string
}

// NewCloudAPIClient creates a new Cloud API client
func NewCloudAPIClient(fronteggClient *FronteggClient, cloudAPIEndpoint, baseEndpoint string) *CloudAPIClient {
func NewCloudAPIClient(authenticator Authenticator, cloudAPIEndpoint, baseEndpoint string) *CloudAPIClient {
return &CloudAPIClient{
HTTPClient: &http.Client{},
FronteggClient: fronteggClient,
Endpoint: cloudAPIEndpoint,
BaseEndpoint: baseEndpoint,
HTTPClient: &http.Client{},
Authenticator: authenticator,
Endpoint: cloudAPIEndpoint,
BaseEndpoint: baseEndpoint,
}
}

// ListCloudProviders fetches the list of cloud providers and their regions
func (c *CloudAPIClient) ListCloudProviders(ctx context.Context) ([]CloudProvider, error) {
providersEndpoint := fmt.Sprintf("%s/api/cloud-regions", c.Endpoint)

// Reuse the FronteggClient's HTTPClient which already includes the Authorization token.
resp, err := c.FronteggClient.HTTPClient.Get(providersEndpoint)
resp, err := c.doRequest(ctx, http.MethodGet, providersEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("error listing cloud providers: %v", err)
return nil, fmt.Errorf("error listing cloud providers: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
return nil, fmt.Errorf("cloud API returned non-200 status code: %d, body: %s", resp.StatusCode, string(body))
}

var response CloudProviderResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, err
return nil, fmt.Errorf("error decoding response: %w", err)
}

log.Printf("[DEBUG] Cloud providers response body: %+v\n", response)

return response.Data, nil
}

// GetRegionDetails fetches the details for a given region
func (c *CloudAPIClient) GetRegionDetails(ctx context.Context, provider CloudProvider) (*CloudRegion, error) {
regionEndpoint := fmt.Sprintf("%s/api/region", provider.Url)

resp, err := c.FronteggClient.HTTPClient.Get(regionEndpoint)
resp, err := c.doRequest(ctx, http.MethodGet, regionEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("error retrieving region details: %v", err)
return nil, fmt.Errorf("error retrieving region details: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
return nil, fmt.Errorf("cloud API returned non-200 status code: %d, body: %s", resp.StatusCode, string(body))
}

log.Printf("[DEBUG] Region details response body: %+v\n", resp.Body)

var region CloudRegion
if err := json.NewDecoder(resp.Body).Decode(&region); err != nil {
return nil, err
return nil, fmt.Errorf("error decoding region details: %w", err)
}

log.Printf("[DEBUG] Region details response body: %+v\n", region)

return &region, nil
}

// EnableRegion sends a PATCH request to enable a cloud region
func (c *CloudAPIClient) EnableRegion(ctx context.Context, provider CloudProvider) (*CloudRegion, error) {
endpoint := fmt.Sprintf("%s/api/region", provider.Url)
emptyJSONPayload := bytes.NewBuffer([]byte("{}"))
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint, emptyJSONPayload)
if err != nil {
return nil, fmt.Errorf("error creating request to enable region: %v", err)
}

req.Header.Add("Content-Type", "application/json")

resp, err := c.FronteggClient.HTTPClient.Do(req)
resp, err := c.doRequest(ctx, http.MethodPatch, endpoint, emptyJSONPayload)
if err != nil {
return nil, fmt.Errorf("error sending request to enable region: %v", err)
return nil, fmt.Errorf("error enabling region: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
return nil, fmt.Errorf("cloud API returned non-200/201 status code: %d, body: %s", resp.StatusCode, string(body))
}

var region CloudRegion
if err := json.NewDecoder(resp.Body).Decode(&region); err != nil {
return nil, err
return nil, fmt.Errorf("error decoding enabled region details: %w", err)
}

return &region, nil
Expand Down Expand Up @@ -198,3 +160,44 @@ func SplitHostPort(hostPortStr string) (host string, port int, err error) {
return "", 0, fmt.Errorf("invalid host:port format")
}
}

func (c *CloudAPIClient) doRequest(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}

if err := c.Authenticator.NeedsTokenRefresh(); err != nil {
if err := c.Authenticator.RefreshToken(); err != nil {
return nil, fmt.Errorf("error refreshing token: %w", err)
}
}

req.Header.Set("Authorization", "Bearer "+c.Authenticator.GetToken())
req.Header.Set("Content-Type", "application/json")

resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending request: %w", err)
}

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, &APIError{
StatusCode: resp.StatusCode,
Message: string(body),
}
}

return resp, nil
}

type APIError struct {
StatusCode int
Message string
}

func (e *APIError) Error() string {
return fmt.Sprintf("API error: %d - %s", e.StatusCode, e.Message)
}
Loading
Loading