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