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 }