Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Introduction

The Replicated SDK (software development kit) is a service that allows you to embed key Replicated features alongside your application.
The Replicated SDK (software development kit) is a service that allows you to embed key Replicated features alongside your application.

[Replicated SDK Product Documentation](https://docs.replicated.com/vendor/replicated-sdk-overview)
[Replicated SDK Product Documentation](https://docs.replicated.com/vendor/replicated-sdk-overview)
69 changes: 69 additions & 0 deletions pkg/replicatedclient/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package replicatedclient

import (
"context"
"net/http"
"net/url"
)

// GetAppInfo returns the current application information.
func (c *Client) GetAppInfo(ctx context.Context) (*AppInfo, error) {
var info AppInfo
if err := c.doGet(ctx, "/api/v1/app/info", &info); err != nil {
return nil, err
}
return &info, nil
}

// GetAppStatus returns the current application status.
func (c *Client) GetAppStatus(ctx context.Context) (*AppStatusResponse, error) {
var status AppStatusResponse
if err := c.doGet(ctx, "/api/v1/app/status", &status); err != nil {
return nil, err
}
return &status, nil
}

// GetAppUpdates returns the list of available upstream updates.
func (c *Client) GetAppUpdates(ctx context.Context) ([]ChannelRelease, error) {
var updates []ChannelRelease
if err := c.doGet(ctx, "/api/v1/app/updates", &updates); err != nil {
return nil, err
}
return updates, nil
}

// GetAppHistory returns the deployment history (Helm releases).
func (c *Client) GetAppHistory(ctx context.Context) (*AppHistoryResponse, error) {
var history AppHistoryResponse
if err := c.doGet(ctx, "/api/v1/app/history", &history); err != nil {
return nil, err
}
return &history, nil
}

// SendCustomAppMetrics sends (overwrites) custom application metrics.
// Data values must be scalars (string, number, bool).
func (c *Client) SendCustomAppMetrics(ctx context.Context, data CustomAppMetricsData) error {
req := SendCustomAppMetricsRequest{Data: data}
return c.doSend(ctx, http.MethodPost, "/api/v1/app/custom-metrics", req)
}

// UpdateCustomAppMetrics merges custom application metrics with existing values.
// Data values must be scalars (string, number, bool).
func (c *Client) UpdateCustomAppMetrics(ctx context.Context, data CustomAppMetricsData) error {
req := SendCustomAppMetricsRequest{Data: data}
return c.doSend(ctx, http.MethodPatch, "/api/v1/app/custom-metrics", req)
}

// DeleteCustomAppMetricsKey deletes a specific custom metrics key.
func (c *Client) DeleteCustomAppMetricsKey(ctx context.Context, key string) error {
path := "/api/v1/app/custom-metrics/" + url.PathEscape(key)
return c.doSend(ctx, http.MethodDelete, path, nil)
}

// SendAppInstanceTags sends instance tags for the application.
func (c *Client) SendAppInstanceTags(ctx context.Context, data InstanceTagData) error {
req := SendAppInstanceTagsRequest{Data: data}
return c.doSend(ctx, http.MethodPost, "/api/v1/app/instance-tags", req)
}
136 changes: 136 additions & 0 deletions pkg/replicatedclient/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package replicatedclient

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

// Client is an HTTP client for the Replicated SDK API.
type Client struct {
baseURL string
httpClient *http.Client
licenseID string
}

// Option configures a Client.
type Option func(*Client)

// WithHTTPClient sets a custom http.Client for requests.
func WithHTTPClient(hc *http.Client) Option {
return func(c *Client) {
c.httpClient = hc
}
}

// WithLicenseID sets the license ID sent in the Authorization header.
func WithLicenseID(id string) Option {
return func(c *Client) {
c.licenseID = id
}
}

// New creates a new Replicated SDK client.
// baseURL should include the scheme and host, e.g. "http://localhost:3000".
func New(baseURL string, opts ...Option) *Client {
c := &Client{
baseURL: strings.TrimRight(baseURL, "/"),
httpClient: http.DefaultClient,
}
for _, opt := range opts {
opt(c)
}
return c
}

// APIError is returned when the server responds with a non-2xx status code.
type APIError struct {
StatusCode int
Body string
}

func (e *APIError) Error() string {
return fmt.Sprintf("replicated-sdk: HTTP %d: %s", e.StatusCode, e.Body)
}

// doRequest builds and executes an HTTP request, returning the response.
// The caller is responsible for closing the response body.
func (c *Client) doRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
url := c.baseURL + path

req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, fmt.Errorf("replicated-sdk: create request: %w", err)
}

if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.licenseID != "" {
req.Header.Set("Authorization", c.licenseID)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("replicated-sdk: execute request: %w", err)
}

return resp, nil
}

// doGet performs a GET request and decodes the JSON response into dest.
func (c *Client) doGet(ctx context.Context, path string, dest interface{}) error {
resp, err := c.doRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return readAPIError(resp)
}

if dest != nil {
if err := json.NewDecoder(resp.Body).Decode(dest); err != nil {
return fmt.Errorf("replicated-sdk: decode response: %w", err)
}
}
return nil
}

// doSend performs a request with a JSON body and checks for a successful status.
func (c *Client) doSend(ctx context.Context, method, path string, payload interface{}) error {
var body io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("replicated-sdk: encode request: %w", err)
}
body = bytes.NewReader(data)
}

resp, err := c.doRequest(ctx, method, path, body)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return readAPIError(resp)
}

return nil
}

// readAPIError reads the response body and returns an *APIError.
func readAPIError(resp *http.Response) error {
bodyBytes, _ := io.ReadAll(resp.Body)
return &APIError{
StatusCode: resp.StatusCode,
Body: strings.TrimSpace(string(bodyBytes)),
}
}
Loading