396 lines
11 KiB
Go
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
|
|
} |