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

Add client for Connections API, some wiring up the client to the provider #1809

Merged
merged 21 commits into from
Sep 24, 2024
Merged
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
13 changes: 8 additions & 5 deletions internal/common/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
SMAPI "github.com/grafana/synthetic-monitoring-api-go-client"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/grafana/terraform-provider-grafana/v3/internal/common/connectionsapi"
)

type Client struct {
Expand All @@ -22,11 +24,12 @@ type Client struct {
GrafanaAPI *goapi.GrafanaHTTPAPI
GrafanaAPIConfig *goapi.TransportConfig

GrafanaCloudAPI *gcom.APIClient
SMAPI *SMAPI.Client
MLAPI *mlapi.Client
OnCallClient *onCallAPI.Client
SLOClient *slo.APIClient
GrafanaCloudAPI *gcom.APIClient
SMAPI *SMAPI.Client
MLAPI *mlapi.Client
OnCallClient *onCallAPI.Client
SLOClient *slo.APIClient
ConnectionsAPIClient *connectionsapi.Client

alertingMutex sync.Mutex
}
Expand Down
149 changes: 149 additions & 0 deletions internal/common/connectionsapi/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package connectionsapi

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"

"github.com/hashicorp/go-retryablehttp"
)

type Client struct {
authToken string
apiURL url.URL
client *http.Client
}

const (
defaultRetries = 3
defaultTimeout = 90 * time.Second
pathPrefix = "/metrics-endpoint/stack"
)

func NewClient(authToken string, rawURL string, client *http.Client) (*Client, error) {
parsedURL, err := url.Parse(rawURL)
if parsedURL.Scheme != "https" {
return nil, fmt.Errorf("https URL scheme expected, provided %q", parsedURL.Scheme)
}

if err != nil {
return nil, fmt.Errorf("failed to parse connections API url: %w", err)
}

if client == nil {
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = defaultRetries
client = retryClient.StandardClient()
client.Timeout = defaultTimeout
}

return &Client{
authToken: authToken,
apiURL: *parsedURL,
client: client,
}, nil
}

type apiResponseWrapper[T any] struct {
Data T `json:"data"`
}

type MetricsEndpointScrapeJob struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
AuthenticationMethod string `json:"authentication_method"`
AuthenticationBearerToken string `json:"bearer_token,omitempty"`
AuthenticationBasicUsername string `json:"basic_username,omitempty"`
AuthenticationBasicPassword string `json:"basic_password,omitempty"`
fridgepoet marked this conversation as resolved.
Show resolved Hide resolved
URL string `json:"url"`
ScrapeIntervalSeconds int64 `json:"scrape_interval_seconds"`
}

func (c *Client) CreateMetricsEndpointScrapeJob(ctx context.Context, stackID string, jobData MetricsEndpointScrapeJob) (MetricsEndpointScrapeJob, error) {
path := fmt.Sprintf("%s/%s/jobs/%s", pathPrefix, stackID, jobData.Name)
respData := apiResponseWrapper[map[string]MetricsEndpointScrapeJob]{}
err := c.doAPIRequest(ctx, http.MethodPost, path, &jobData, &respData)
if err != nil {
return MetricsEndpointScrapeJob{}, fmt.Errorf("failed to create metrics endpoint scrape job: %w", err)
}
return respData.Data[jobData.Name], nil
}

func (c *Client) GetMetricsEndpointScrapeJob(ctx context.Context, stackID string, jobName string) (MetricsEndpointScrapeJob, error) {
path := fmt.Sprintf("%s/%s/jobs/%s", pathPrefix, stackID, jobName)
respData := apiResponseWrapper[map[string]MetricsEndpointScrapeJob]{}
err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &respData)
if err != nil {
return MetricsEndpointScrapeJob{}, fmt.Errorf("failed to get metrics endpoint scrape job: %w", err)
}
return respData.Data[jobName], nil
}

func (c *Client) UpdateMetricsEndpointScrapeJob(ctx context.Context, stackID string, jobName string, jobData MetricsEndpointScrapeJob) (MetricsEndpointScrapeJob, error) {
path := fmt.Sprintf("%s/%s/jobs/%s", pathPrefix, stackID, jobName)
respData := apiResponseWrapper[map[string]MetricsEndpointScrapeJob]{}
err := c.doAPIRequest(ctx, http.MethodPut, path, &jobData, &respData)
if err != nil {
return MetricsEndpointScrapeJob{}, fmt.Errorf("failed to update metrics endpoint scrape job: %w", err)
}
return respData.Data[jobName], nil
}

func (c *Client) DeleteMetricsEndpointScrapeJob(ctx context.Context, stackID string, jobName string) error {
path := fmt.Sprintf("%s/%s/jobs/%s", pathPrefix, stackID, jobName)
err := c.doAPIRequest(ctx, http.MethodDelete, path, nil, nil)
if err != nil {
return fmt.Errorf("failed to delete metrics endpoint scrape job: %w", err)
}
return nil
}

var ErrNotFound = fmt.Errorf("job not found")

func (c *Client) doAPIRequest(ctx context.Context, method string, path string, body any, responseData any) error {
var reqBodyBytes io.Reader
if body != nil {
bs, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
reqBodyBytes = bytes.NewReader(bs)
}
var resp *http.Response

req, err := http.NewRequestWithContext(ctx, method, c.apiURL.String()+path, reqBodyBytes)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.authToken))
req.Header.Add("Content-Type", "application/json")

resp, err = c.client.Do(req)
if err != nil {
return fmt.Errorf("failed to do request: %w", err)
}

bodyContents, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
if resp.StatusCode == 404 {
return ErrNotFound
}
return fmt.Errorf("status: %d, body: %v", resp.StatusCode, string(bodyContents))
}
if responseData != nil && resp.StatusCode != http.StatusNoContent {
err = json.Unmarshal(bodyContents, &responseData)
if err != nil {
return fmt.Errorf("failed to unmarshal response body: %w", err)
}
}
return nil
}
Loading
Loading