faas-clinet
This commit is contained in:
297
faas-client/README.md
Normal file
297
faas-client/README.md
Normal file
@ -0,0 +1,297 @@
|
||||
# Skybridge FaaS Client
|
||||
|
||||
A lightweight Go client library for interacting with the Skybridge Function-as-a-Service (FaaS) platform.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/RyanCopley/skybridge/faas-client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Client Setup
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
faasclient "github.com/RyanCopley/skybridge/faas-client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a new client
|
||||
client := faasclient.NewClient(
|
||||
"http://localhost:8080", // FaaS API base URL
|
||||
faasclient.WithUserEmail("admin@example.com"), // Authentication
|
||||
)
|
||||
|
||||
// Use the client...
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Options
|
||||
|
||||
```go
|
||||
// Using user email header (for development)
|
||||
client := faasclient.NewClient(baseURL,
|
||||
faasclient.WithUserEmail("user@example.com"))
|
||||
|
||||
// Using custom auth headers
|
||||
client := faasclient.NewClient(baseURL,
|
||||
faasclient.WithAuth(map[string]string{
|
||||
"Authorization": "Bearer " + token,
|
||||
"X-User-Email": "user@example.com",
|
||||
}))
|
||||
|
||||
// Using custom HTTP client
|
||||
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||
client := faasclient.NewClient(baseURL,
|
||||
faasclient.WithHTTPClient(httpClient))
|
||||
```
|
||||
|
||||
### Function Management
|
||||
|
||||
#### Creating a Function
|
||||
|
||||
```go
|
||||
req := &faasclient.CreateFunctionRequest{
|
||||
Name: "my-function",
|
||||
AppID: "my-app",
|
||||
Runtime: faasclient.RuntimeNodeJS18,
|
||||
Image: "node:18-alpine", // Optional, auto-selected if not provided
|
||||
Handler: "index.handler",
|
||||
Code: "exports.handler = async (event) => { return 'Hello World'; }",
|
||||
Environment: map[string]string{
|
||||
"NODE_ENV": "production",
|
||||
},
|
||||
Timeout: faasclient.Duration(30 * time.Second),
|
||||
Memory: 512,
|
||||
Owner: faasclient.Owner{
|
||||
Type: faasclient.OwnerTypeIndividual,
|
||||
Name: "John Doe",
|
||||
Owner: "john@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
function, err := client.CreateFunction(context.Background(), req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Created function: %s (ID: %s)", function.Name, function.ID)
|
||||
```
|
||||
|
||||
#### Getting a Function
|
||||
|
||||
```go
|
||||
function, err := client.GetFunction(context.Background(), functionID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Function: %s, Runtime: %s", function.Name, function.Runtime)
|
||||
```
|
||||
|
||||
#### Updating a Function
|
||||
|
||||
```go
|
||||
newTimeout := faasclient.Duration(60 * time.Second)
|
||||
req := &faasclient.UpdateFunctionRequest{
|
||||
Timeout: &newTimeout,
|
||||
Environment: map[string]string{
|
||||
"NODE_ENV": "development",
|
||||
},
|
||||
}
|
||||
|
||||
function, err := client.UpdateFunction(context.Background(), functionID, req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
#### Listing Functions
|
||||
|
||||
```go
|
||||
response, err := client.ListFunctions(context.Background(), "my-app", 50, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, fn := range response.Functions {
|
||||
log.Printf("Function: %s (ID: %s)", fn.Name, fn.ID)
|
||||
}
|
||||
```
|
||||
|
||||
#### Deleting a Function
|
||||
|
||||
```go
|
||||
err := client.DeleteFunction(context.Background(), functionID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Function Deployment
|
||||
|
||||
```go
|
||||
// Deploy with default options
|
||||
resp, err := client.DeployFunction(context.Background(), functionID, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Force deployment
|
||||
req := &faasclient.DeployFunctionRequest{
|
||||
Force: true,
|
||||
}
|
||||
resp, err = client.DeployFunction(context.Background(), functionID, req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Deployment status: %s", resp.Status)
|
||||
```
|
||||
|
||||
### Function Execution
|
||||
|
||||
#### Synchronous Execution
|
||||
|
||||
```go
|
||||
input := json.RawMessage(`{"name": "World"}`)
|
||||
req := &faasclient.ExecuteFunctionRequest{
|
||||
FunctionID: functionID,
|
||||
Input: input,
|
||||
Async: false,
|
||||
}
|
||||
|
||||
response, err := client.ExecuteFunction(context.Background(), req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Result: %s", string(response.Output))
|
||||
log.Printf("Duration: %v", response.Duration)
|
||||
```
|
||||
|
||||
#### Asynchronous Execution
|
||||
|
||||
```go
|
||||
input := json.RawMessage(`{"name": "World"}`)
|
||||
response, err := client.InvokeFunction(context.Background(), functionID, input)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Execution ID: %s", response.ExecutionID)
|
||||
log.Printf("Status: %s", response.Status)
|
||||
```
|
||||
|
||||
### Execution Management
|
||||
|
||||
#### Getting Execution Details
|
||||
|
||||
```go
|
||||
execution, err := client.GetExecution(context.Background(), executionID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Status: %s", execution.Status)
|
||||
log.Printf("Duration: %v", execution.Duration)
|
||||
if execution.Error != "" {
|
||||
log.Printf("Error: %s", execution.Error)
|
||||
}
|
||||
```
|
||||
|
||||
#### Listing Executions
|
||||
|
||||
```go
|
||||
// List all executions
|
||||
response, err := client.ListExecutions(context.Background(), nil, 50, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// List executions for a specific function
|
||||
response, err = client.ListExecutions(context.Background(), &functionID, 50, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, exec := range response.Executions {
|
||||
log.Printf("Execution: %s, Status: %s", exec.ID, exec.Status)
|
||||
}
|
||||
```
|
||||
|
||||
#### Canceling an Execution
|
||||
|
||||
```go
|
||||
err := client.CancelExecution(context.Background(), executionID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
#### Getting Execution Logs
|
||||
|
||||
```go
|
||||
logs, err := client.GetExecutionLogs(context.Background(), executionID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, logLine := range logs.Logs {
|
||||
log.Printf("Log: %s", logLine)
|
||||
}
|
||||
```
|
||||
|
||||
#### Getting Running Executions
|
||||
|
||||
```go
|
||||
response, err := client.GetRunningExecutions(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Running executions: %d", response.Count)
|
||||
for _, exec := range response.Executions {
|
||||
log.Printf("Running: %s (Function: %s)", exec.ID, exec.FunctionID)
|
||||
}
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
The client provides comprehensive type definitions that match the FaaS API:
|
||||
|
||||
- `FunctionDefinition` - Complete function metadata
|
||||
- `FunctionExecution` - Execution details and results
|
||||
- `RuntimeType` - Supported runtimes (NodeJS18, Python39, Go120, Custom)
|
||||
- `ExecutionStatus` - Execution states (Pending, Running, Completed, Failed, etc.)
|
||||
- `Owner` - Ownership information
|
||||
- Request/Response types for all operations
|
||||
|
||||
## Error Handling
|
||||
|
||||
The client provides detailed error messages that include HTTP status codes and response bodies:
|
||||
|
||||
```go
|
||||
function, err := client.GetFunction(ctx, nonExistentID)
|
||||
if err != nil {
|
||||
// Error will include status code and details
|
||||
log.Printf("Error: %v", err) // "get function failed with status 404: Function not found"
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
This client package is designed as a lightweight, standalone library that:
|
||||
|
||||
- **No heavy dependencies**: Only requires `google/uuid`
|
||||
- **Zero coupling**: Doesn't import the entire FaaS service
|
||||
- **Modular**: Can be used by any service in your monolith
|
||||
- **Type-safe**: Comprehensive Go types for all API operations
|
||||
- **Flexible auth**: Supports multiple authentication methods
|
||||
396
faas-client/client.go
Normal file
396
faas-client/client.go
Normal file
@ -0,0 +1,396 @@
|
||||
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
|
||||
}
|
||||
148
faas-client/example/main.go
Normal file
148
faas-client/example/main.go
Normal file
@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
faasclient "github.com/RyanCopley/skybridge/faas-client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create FaaS client
|
||||
client := faasclient.NewClient(
|
||||
"http://localhost:8080",
|
||||
faasclient.WithUserEmail("admin@example.com"),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a simple Node.js function
|
||||
log.Println("Creating function...")
|
||||
createReq := &faasclient.CreateFunctionRequest{
|
||||
Name: "hello-world-example",
|
||||
AppID: "example-app",
|
||||
Runtime: faasclient.RuntimeNodeJS18,
|
||||
Handler: "index.handler",
|
||||
Code: "exports.handler = async (event) => { return { message: 'Hello, ' + (event.name || 'World') + '!' }; }",
|
||||
Environment: map[string]string{
|
||||
"NODE_ENV": "production",
|
||||
},
|
||||
Timeout: faasclient.Duration(30 * time.Second),
|
||||
Memory: 256,
|
||||
Owner: faasclient.Owner{
|
||||
Type: faasclient.OwnerTypeIndividual,
|
||||
Name: "Example User",
|
||||
Owner: "admin@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
function, err := client.CreateFunction(ctx, createReq)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create function: %v", err)
|
||||
}
|
||||
log.Printf("Created function: %s (ID: %s)", function.Name, function.ID)
|
||||
|
||||
// Deploy the function
|
||||
log.Println("Deploying function...")
|
||||
deployResp, err := client.DeployFunction(ctx, function.ID, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to deploy function: %v", err)
|
||||
}
|
||||
log.Printf("Deployment status: %s - %s", deployResp.Status, deployResp.Message)
|
||||
|
||||
// Execute function synchronously
|
||||
log.Println("Executing function synchronously...")
|
||||
input := json.RawMessage(`{"name": "Skybridge"}`)
|
||||
executeReq := &faasclient.ExecuteFunctionRequest{
|
||||
FunctionID: function.ID,
|
||||
Input: input,
|
||||
Async: false,
|
||||
}
|
||||
|
||||
execResp, err := client.ExecuteFunction(ctx, executeReq)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to execute function: %v", err)
|
||||
}
|
||||
log.Printf("Sync execution result: %s", string(execResp.Output))
|
||||
log.Printf("Duration: %v, Memory used: %d MB", execResp.Duration, execResp.MemoryUsed)
|
||||
|
||||
// Execute function asynchronously
|
||||
log.Println("Invoking function asynchronously...")
|
||||
asyncResp, err := client.InvokeFunction(ctx, function.ID, input)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to invoke function: %v", err)
|
||||
}
|
||||
log.Printf("Async execution ID: %s, Status: %s", asyncResp.ExecutionID, asyncResp.Status)
|
||||
|
||||
// Wait a moment for async execution to complete
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Get execution details
|
||||
log.Println("Getting execution details...")
|
||||
execution, err := client.GetExecution(ctx, asyncResp.ExecutionID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get execution: %v", err)
|
||||
}
|
||||
log.Printf("Execution status: %s", execution.Status)
|
||||
if execution.Status == faasclient.StatusCompleted {
|
||||
log.Printf("Async execution result: %s", string(execution.Output))
|
||||
log.Printf("Duration: %v, Memory used: %d MB", execution.Duration, execution.MemoryUsed)
|
||||
}
|
||||
|
||||
// Get execution logs
|
||||
log.Println("Getting execution logs...")
|
||||
logs, err := client.GetExecutionLogs(ctx, asyncResp.ExecutionID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get logs: %v", err)
|
||||
} else {
|
||||
log.Printf("Logs (%d entries):", len(logs.Logs))
|
||||
for _, logLine := range logs.Logs {
|
||||
log.Printf(" %s", logLine)
|
||||
}
|
||||
}
|
||||
|
||||
// List functions
|
||||
log.Println("Listing functions...")
|
||||
listResp, err := client.ListFunctions(ctx, "example-app", 10, 0)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list functions: %v", err)
|
||||
}
|
||||
log.Printf("Found %d functions:", len(listResp.Functions))
|
||||
for _, fn := range listResp.Functions {
|
||||
log.Printf(" - %s (%s) - Runtime: %s", fn.Name, fn.ID, fn.Runtime)
|
||||
}
|
||||
|
||||
// List executions for this function
|
||||
log.Println("Listing executions...")
|
||||
execListResp, err := client.ListExecutions(ctx, &function.ID, 10, 0)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list executions: %v", err)
|
||||
}
|
||||
log.Printf("Found %d executions:", len(execListResp.Executions))
|
||||
for _, exec := range execListResp.Executions {
|
||||
status := string(exec.Status)
|
||||
log.Printf(" - %s: %s (Duration: %v)", exec.ID, status, exec.Duration)
|
||||
}
|
||||
|
||||
// Clean up - delete the function
|
||||
log.Println("Cleaning up...")
|
||||
err = client.DeleteFunction(ctx, function.ID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to delete function: %v", err)
|
||||
}
|
||||
log.Printf("Deleted function: %s", function.ID)
|
||||
|
||||
log.Println("Example completed successfully!")
|
||||
}
|
||||
|
||||
// Helper function to create a UUID from string (for testing)
|
||||
func mustParseUUID(s string) uuid.UUID {
|
||||
id, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
7
faas-client/go.mod
Normal file
7
faas-client/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module github.com/RyanCopley/skybridge/faas-client
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
)
|
||||
191
faas-client/types.go
Normal file
191
faas-client/types.go
Normal file
@ -0,0 +1,191 @@
|
||||
package faasclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// RuntimeType represents supported function runtimes
|
||||
type RuntimeType string
|
||||
|
||||
const (
|
||||
RuntimeNodeJS18 RuntimeType = "nodejs18"
|
||||
RuntimePython39 RuntimeType = "python3.9"
|
||||
RuntimeGo120 RuntimeType = "go1.20"
|
||||
RuntimeCustom RuntimeType = "custom"
|
||||
)
|
||||
|
||||
// ExecutionStatus represents the status of function execution
|
||||
type ExecutionStatus string
|
||||
|
||||
const (
|
||||
StatusPending ExecutionStatus = "pending"
|
||||
StatusRunning ExecutionStatus = "running"
|
||||
StatusCompleted ExecutionStatus = "completed"
|
||||
StatusFailed ExecutionStatus = "failed"
|
||||
StatusTimeout ExecutionStatus = "timeout"
|
||||
StatusCanceled ExecutionStatus = "canceled"
|
||||
)
|
||||
|
||||
// OwnerType represents the type of owner
|
||||
type OwnerType string
|
||||
|
||||
const (
|
||||
OwnerTypeIndividual OwnerType = "individual"
|
||||
OwnerTypeTeam OwnerType = "team"
|
||||
)
|
||||
|
||||
// Owner represents ownership information
|
||||
type Owner struct {
|
||||
Type OwnerType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
// Duration wraps time.Duration for JSON marshaling
|
||||
type Duration time.Duration
|
||||
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Duration(d).String())
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
var v interface{}
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
switch value := v.(type) {
|
||||
case float64:
|
||||
*d = Duration(time.Duration(value))
|
||||
return nil
|
||||
case string:
|
||||
tmp, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = Duration(tmp)
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// FunctionDefinition represents a serverless function
|
||||
type FunctionDefinition struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AppID string `json:"app_id"`
|
||||
Runtime RuntimeType `json:"runtime"`
|
||||
Image string `json:"image"`
|
||||
Handler string `json:"handler"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
Timeout Duration `json:"timeout"`
|
||||
Memory int `json:"memory"`
|
||||
Owner Owner `json:"owner"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// FunctionExecution represents a function execution
|
||||
type FunctionExecution struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
FunctionID uuid.UUID `json:"function_id"`
|
||||
Status ExecutionStatus `json:"status"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
Output json.RawMessage `json:"output,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
MemoryUsed int `json:"memory_used"`
|
||||
Logs []string `json:"logs,omitempty"`
|
||||
ContainerID string `json:"container_id,omitempty"`
|
||||
ExecutorID string `json:"executor_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
// CreateFunctionRequest represents a request to create a new function
|
||||
type CreateFunctionRequest struct {
|
||||
Name string `json:"name"`
|
||||
AppID string `json:"app_id"`
|
||||
Runtime RuntimeType `json:"runtime"`
|
||||
Image string `json:"image"`
|
||||
Handler string `json:"handler"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
Timeout Duration `json:"timeout"`
|
||||
Memory int `json:"memory"`
|
||||
Owner Owner `json:"owner"`
|
||||
}
|
||||
|
||||
// UpdateFunctionRequest represents a request to update an existing function
|
||||
type UpdateFunctionRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Runtime *RuntimeType `json:"runtime,omitempty"`
|
||||
Image *string `json:"image,omitempty"`
|
||||
Handler *string `json:"handler,omitempty"`
|
||||
Code *string `json:"code,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
Timeout *Duration `json:"timeout,omitempty"`
|
||||
Memory *int `json:"memory,omitempty"`
|
||||
Owner *Owner `json:"owner,omitempty"`
|
||||
}
|
||||
|
||||
// ExecuteFunctionRequest represents a request to execute a function
|
||||
type ExecuteFunctionRequest struct {
|
||||
FunctionID uuid.UUID `json:"function_id"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
Async bool `json:"async,omitempty"`
|
||||
}
|
||||
|
||||
// ExecuteFunctionResponse represents a response for function execution
|
||||
type ExecuteFunctionResponse struct {
|
||||
ExecutionID uuid.UUID `json:"execution_id"`
|
||||
Status ExecutionStatus `json:"status"`
|
||||
Output json.RawMessage `json:"output,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
MemoryUsed int `json:"memory_used,omitempty"`
|
||||
}
|
||||
|
||||
// DeployFunctionRequest represents a request to deploy a function
|
||||
type DeployFunctionRequest struct {
|
||||
FunctionID uuid.UUID `json:"function_id"`
|
||||
Force bool `json:"force,omitempty"`
|
||||
}
|
||||
|
||||
// DeployFunctionResponse represents a response for function deployment
|
||||
type DeployFunctionResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
ImageID string `json:"image_id,omitempty"`
|
||||
}
|
||||
|
||||
// ListFunctionsResponse represents the response for listing functions
|
||||
type ListFunctionsResponse struct {
|
||||
Functions []FunctionDefinition `json:"functions"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
// ListExecutionsResponse represents the response for listing executions
|
||||
type ListExecutionsResponse struct {
|
||||
Executions []FunctionExecution `json:"executions"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
// GetLogsResponse represents the response for getting execution logs
|
||||
type GetLogsResponse struct {
|
||||
Logs []string `json:"logs"`
|
||||
}
|
||||
|
||||
// GetRunningExecutionsResponse represents the response for getting running executions
|
||||
type GetRunningExecutionsResponse struct {
|
||||
Executions []FunctionExecution `json:"executions"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
Reference in New Issue
Block a user