Files
skybridge/faas-client/client.go
2025-08-31 17:19:13 -04:00

396 lines
11 KiB
Go

package faasclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"github.com/google/uuid"
)
// Client represents the FaaS API client
type Client struct {
baseURL string
httpClient *http.Client
authHeader map[string]string
}
// NewClient creates a new FaaS client
func NewClient(baseURL string, options ...ClientOption) *Client {
client := &Client{
baseURL: baseURL,
httpClient: http.DefaultClient,
authHeader: make(map[string]string),
}
for _, option := range options {
option(client)
}
return client
}
// ClientOption represents a configuration option for the client
type ClientOption func(*Client)
// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = httpClient
}
}
// WithAuth sets authentication headers
func WithAuth(headers map[string]string) ClientOption {
return func(c *Client) {
for k, v := range headers {
c.authHeader[k] = v
}
}
}
// WithUserEmail sets the X-User-Email header for authentication
func WithUserEmail(email string) ClientOption {
return func(c *Client) {
c.authHeader["X-User-Email"] = email
}
}
// doRequest performs an HTTP request
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
var reqBody io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewBuffer(jsonData)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
// Add authentication headers
for k, v := range c.authHeader {
req.Header.Set(k, v)
}
return c.httpClient.Do(req)
}
// CreateFunction creates a new function
func (c *Client) CreateFunction(ctx context.Context, req *CreateFunctionRequest) (*FunctionDefinition, error) {
resp, err := c.doRequest(ctx, "POST", "/api/v1/functions", req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("create function failed with status %d: %s", resp.StatusCode, string(body))
}
var function FunctionDefinition
if err := json.NewDecoder(resp.Body).Decode(&function); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &function, nil
}
// GetFunction retrieves a function by ID
func (c *Client) GetFunction(ctx context.Context, id uuid.UUID) (*FunctionDefinition, error) {
resp, err := c.doRequest(ctx, "GET", "/api/v1/functions/"+id.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("get function failed with status %d: %s", resp.StatusCode, string(body))
}
var function FunctionDefinition
if err := json.NewDecoder(resp.Body).Decode(&function); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &function, nil
}
// UpdateFunction updates an existing function
func (c *Client) UpdateFunction(ctx context.Context, id uuid.UUID, req *UpdateFunctionRequest) (*FunctionDefinition, error) {
resp, err := c.doRequest(ctx, "PUT", "/api/v1/functions/"+id.String(), req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("update function failed with status %d: %s", resp.StatusCode, string(body))
}
var function FunctionDefinition
if err := json.NewDecoder(resp.Body).Decode(&function); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &function, nil
}
// DeleteFunction deletes a function
func (c *Client) DeleteFunction(ctx context.Context, id uuid.UUID) error {
resp, err := c.doRequest(ctx, "DELETE", "/api/v1/functions/"+id.String(), nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete function failed with status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// ListFunctions lists functions with optional filtering
func (c *Client) ListFunctions(ctx context.Context, appID string, limit, offset int) (*ListFunctionsResponse, error) {
params := url.Values{}
if appID != "" {
params.Set("app_id", appID)
}
if limit > 0 {
params.Set("limit", strconv.Itoa(limit))
}
if offset > 0 {
params.Set("offset", strconv.Itoa(offset))
}
path := "/api/v1/functions"
if len(params) > 0 {
path += "?" + params.Encode()
}
resp, err := c.doRequest(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("list functions failed with status %d: %s", resp.StatusCode, string(body))
}
var response ListFunctionsResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}
// DeployFunction deploys a function
func (c *Client) DeployFunction(ctx context.Context, id uuid.UUID, req *DeployFunctionRequest) (*DeployFunctionResponse, error) {
if req == nil {
req = &DeployFunctionRequest{FunctionID: id}
}
req.FunctionID = id
resp, err := c.doRequest(ctx, "POST", "/api/v1/functions/"+id.String()+"/deploy", req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("deploy function failed with status %d: %s", resp.StatusCode, string(body))
}
var response DeployFunctionResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}
// ExecuteFunction executes a function synchronously or asynchronously
func (c *Client) ExecuteFunction(ctx context.Context, req *ExecuteFunctionRequest) (*ExecuteFunctionResponse, error) {
resp, err := c.doRequest(ctx, "POST", "/api/v1/functions/"+req.FunctionID.String()+"/execute", req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("execute function failed with status %d: %s", resp.StatusCode, string(body))
}
var response ExecuteFunctionResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}
// InvokeFunction invokes a function asynchronously
func (c *Client) InvokeFunction(ctx context.Context, functionID uuid.UUID, input json.RawMessage) (*ExecuteFunctionResponse, error) {
req := &ExecuteFunctionRequest{
FunctionID: functionID,
Input: input,
Async: true,
}
resp, err := c.doRequest(ctx, "POST", "/api/v1/functions/"+functionID.String()+"/invoke", req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("invoke function failed with status %d: %s", resp.StatusCode, string(body))
}
var response ExecuteFunctionResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}
// GetExecution retrieves an execution by ID
func (c *Client) GetExecution(ctx context.Context, id uuid.UUID) (*FunctionExecution, error) {
resp, err := c.doRequest(ctx, "GET", "/api/v1/executions/"+id.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("get execution failed with status %d: %s", resp.StatusCode, string(body))
}
var execution FunctionExecution
if err := json.NewDecoder(resp.Body).Decode(&execution); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &execution, nil
}
// ListExecutions lists executions with optional filtering
func (c *Client) ListExecutions(ctx context.Context, functionID *uuid.UUID, limit, offset int) (*ListExecutionsResponse, error) {
params := url.Values{}
if functionID != nil {
params.Set("function_id", functionID.String())
}
if limit > 0 {
params.Set("limit", strconv.Itoa(limit))
}
if offset > 0 {
params.Set("offset", strconv.Itoa(offset))
}
path := "/api/v1/executions"
if len(params) > 0 {
path += "?" + params.Encode()
}
resp, err := c.doRequest(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("list executions failed with status %d: %s", resp.StatusCode, string(body))
}
var response ListExecutionsResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}
// CancelExecution cancels a running execution
func (c *Client) CancelExecution(ctx context.Context, id uuid.UUID) error {
resp, err := c.doRequest(ctx, "POST", "/api/v1/executions/"+id.String()+"/cancel", nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("cancel execution failed with status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// GetExecutionLogs retrieves logs for an execution
func (c *Client) GetExecutionLogs(ctx context.Context, id uuid.UUID) (*GetLogsResponse, error) {
resp, err := c.doRequest(ctx, "GET", "/api/v1/executions/"+id.String()+"/logs", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("get execution logs failed with status %d: %s", resp.StatusCode, string(body))
}
var response GetLogsResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}
// GetRunningExecutions retrieves all currently running executions
func (c *Client) GetRunningExecutions(ctx context.Context) (*GetRunningExecutionsResponse, error) {
resp, err := c.doRequest(ctx, "GET", "/api/v1/executions/running", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("get running executions failed with status %d: %s", resp.StatusCode, string(body))
}
var response GetRunningExecutionsResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}