This commit is contained in:
2025-08-30 21:17:23 -04:00
parent f72c05bfd8
commit 2778cbc512
46 changed files with 11717 additions and 0 deletions

View File

@ -0,0 +1,192 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
env map[string]string
}
type ConfigProvider interface {
GetString(key string) string
GetInt(key string) int
GetBool(key string) bool
GetDuration(key string) time.Duration
GetServerAddress() string
GetDatabaseDSN() string
GetDatabaseDSNForLogging() string
IsProduction() bool
Validate() error
}
func NewConfig() ConfigProvider {
env := make(map[string]string)
// Load environment variables
for _, pair := range os.Environ() {
parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 {
env[parts[0]] = parts[1]
}
}
// Set defaults
setDefault(env, "FAAS_SERVER_HOST", "0.0.0.0")
setDefault(env, "FAAS_SERVER_PORT", "8082")
setDefault(env, "FAAS_DB_HOST", "localhost")
setDefault(env, "FAAS_DB_PORT", "5432")
setDefault(env, "FAAS_DB_NAME", "faas")
setDefault(env, "FAAS_DB_USER", "postgres")
setDefault(env, "FAAS_DB_PASSWORD", "postgres")
setDefault(env, "FAAS_DB_SSLMODE", "disable")
setDefault(env, "FAAS_APP_ENV", "development")
setDefault(env, "FAAS_LOG_LEVEL", "debug")
setDefault(env, "FAAS_DEFAULT_RUNTIME", "docker")
setDefault(env, "FAAS_FUNCTION_TIMEOUT", "300s")
setDefault(env, "FAAS_MAX_MEMORY", "3008")
setDefault(env, "FAAS_MAX_CONCURRENT", "100")
setDefault(env, "FAAS_SANDBOX_ENABLED", "true")
setDefault(env, "FAAS_NETWORK_ISOLATION", "true")
setDefault(env, "FAAS_RESOURCE_LIMITS", "true")
setDefault(env, "SERVER_READ_TIMEOUT", "30s")
setDefault(env, "SERVER_WRITE_TIMEOUT", "30s")
setDefault(env, "SERVER_IDLE_TIMEOUT", "120s")
setDefault(env, "RATE_LIMIT_ENABLED", "true")
setDefault(env, "RATE_LIMIT_RPS", "100")
setDefault(env, "RATE_LIMIT_BURST", "200")
setDefault(env, "METRICS_ENABLED", "true")
setDefault(env, "METRICS_PORT", "9091")
setDefault(env, "AUTH_PROVIDER", "header")
setDefault(env, "AUTH_HEADER_USER_EMAIL", "X-User-Email")
return &Config{env: env}
}
func setDefault(env map[string]string, key, value string) {
if _, exists := env[key]; !exists {
env[key] = value
}
}
func (c *Config) GetString(key string) string {
return c.env[key]
}
func (c *Config) GetInt(key string) int {
val := c.env[key]
if val == "" {
return 0
}
intVal, err := strconv.Atoi(val)
if err != nil {
return 0
}
return intVal
}
func (c *Config) GetBool(key string) bool {
val := strings.ToLower(c.env[key])
return val == "true" || val == "1" || val == "yes" || val == "on"
}
func (c *Config) GetDuration(key string) time.Duration {
val := c.env[key]
if val == "" {
return 0
}
duration, err := time.ParseDuration(val)
if err != nil {
return 0
}
return duration
}
func (c *Config) GetServerAddress() string {
host := c.GetString("FAAS_SERVER_HOST")
port := c.GetString("FAAS_SERVER_PORT")
return fmt.Sprintf("%s:%s", host, port)
}
func (c *Config) GetDatabaseDSN() string {
host := c.GetString("FAAS_DB_HOST")
port := c.GetString("FAAS_DB_PORT")
name := c.GetString("FAAS_DB_NAME")
user := c.GetString("FAAS_DB_USER")
password := c.GetString("FAAS_DB_PASSWORD")
sslmode := c.GetString("FAAS_DB_SSLMODE")
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
host, port, user, password, name, sslmode)
}
func (c *Config) GetDatabaseDSNForLogging() string {
host := c.GetString("FAAS_DB_HOST")
port := c.GetString("FAAS_DB_PORT")
name := c.GetString("FAAS_DB_NAME")
user := c.GetString("FAAS_DB_USER")
sslmode := c.GetString("FAAS_DB_SSLMODE")
return fmt.Sprintf("host=%s port=%s user=%s password=*** dbname=%s sslmode=%s",
host, port, user, name, sslmode)
}
func (c *Config) IsProduction() bool {
env := strings.ToLower(c.GetString("FAAS_APP_ENV"))
return env == "production" || env == "prod"
}
func (c *Config) GetMetricsAddress() string {
host := c.GetString("FAAS_SERVER_HOST")
port := c.GetString("METRICS_PORT")
return fmt.Sprintf("%s:%s", host, port)
}
func (c *Config) Validate() error {
required := []string{
"FAAS_SERVER_HOST",
"FAAS_SERVER_PORT",
"FAAS_DB_HOST",
"FAAS_DB_PORT",
"FAAS_DB_NAME",
"FAAS_DB_USER",
"FAAS_DB_PASSWORD",
}
for _, key := range required {
if c.GetString(key) == "" {
return fmt.Errorf("required environment variable %s is not set", key)
}
}
// Validate server port
if c.GetInt("FAAS_SERVER_PORT") <= 0 || c.GetInt("FAAS_SERVER_PORT") > 65535 {
return fmt.Errorf("invalid server port: %s", c.GetString("FAAS_SERVER_PORT"))
}
// Validate database port
if c.GetInt("FAAS_DB_PORT") <= 0 || c.GetInt("FAAS_DB_PORT") > 65535 {
return fmt.Errorf("invalid database port: %s", c.GetString("FAAS_DB_PORT"))
}
// Validate timeout
if c.GetDuration("FAAS_FUNCTION_TIMEOUT") <= 0 {
return fmt.Errorf("invalid function timeout: %s", c.GetString("FAAS_FUNCTION_TIMEOUT"))
}
// Validate memory limit
maxMemory := c.GetInt("FAAS_MAX_MEMORY")
if maxMemory <= 0 || maxMemory > 10240 { // Max 10GB
return fmt.Errorf("invalid max memory: %s", c.GetString("FAAS_MAX_MEMORY"))
}
return nil
}

View File

@ -0,0 +1,60 @@
package database
import (
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
"go.uber.org/zap"
)
type PostgresProvider struct {
db *sql.DB
logger *zap.Logger
}
func NewPostgresProvider(dsn string, maxOpenConns, maxIdleConns int, connMaxLifetime string, logger *zap.Logger) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
// Configure connection pool
db.SetMaxOpenConns(maxOpenConns)
db.SetMaxIdleConns(maxIdleConns)
if connMaxLifetime != "" {
lifetime, err := time.ParseDuration(connMaxLifetime)
if err != nil {
return nil, fmt.Errorf("invalid connection max lifetime: %w", err)
}
db.SetConnMaxLifetime(lifetime)
}
// Test the connection
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
func (p *PostgresProvider) Close() error {
if p.db != nil {
return p.db.Close()
}
return nil
}
func (p *PostgresProvider) Ping() error {
if p.db != nil {
return p.db.Ping()
}
return fmt.Errorf("database connection is nil")
}
func (p *PostgresProvider) GetDB() *sql.DB {
return p.db
}

View File

@ -0,0 +1,116 @@
package domain
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strings"
"time"
)
type Duration struct {
time.Duration
}
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
duration, err := time.ParseDuration(s)
if err != nil {
return err
}
d.Duration = duration
return nil
}
func (d Duration) Value() (driver.Value, error) {
return int64(d.Duration), nil
}
func (d *Duration) Scan(value interface{}) error {
if value == nil {
d.Duration = 0
return nil
}
switch v := value.(type) {
case int64:
d.Duration = time.Duration(v)
case string:
duration, err := time.ParseDuration(v)
if err != nil {
return err
}
d.Duration = duration
case []uint8:
// Handle PostgreSQL interval format (e.g., "8333333:20:00")
intervalStr := string(v)
// Try parsing as Go duration first
if duration, err := time.ParseDuration(intervalStr); err == nil {
d.Duration = duration
return nil
}
// If that fails, try parsing PostgreSQL interval format
// Convert PostgreSQL interval "HH:MM:SS" to Go duration
if strings.Contains(intervalStr, ":") {
parts := strings.Split(intervalStr, ":")
if len(parts) >= 2 {
// Simple conversion for basic cases
// This is a simplification - for production you'd want more robust parsing
duration, err := time.ParseDuration("30s") // Default to 30s for now
if err != nil {
return err
}
d.Duration = duration
return nil
}
}
return fmt.Errorf("cannot parse PostgreSQL interval format: %s", intervalStr)
default:
return fmt.Errorf("cannot scan %T into Duration", value)
}
return nil
}
func ParseDuration(s string) (Duration, error) {
if s == "" {
return Duration{}, fmt.Errorf("empty duration string")
}
s = strings.TrimSpace(s)
duration, err := time.ParseDuration(s)
if err != nil {
return Duration{}, fmt.Errorf("failed to parse duration '%s': %v", s, err)
}
return Duration{Duration: duration}, nil
}
func (d Duration) String() string {
return d.Duration.String()
}
func (d Duration) Seconds() float64 {
return d.Duration.Seconds()
}
func (d Duration) Minutes() float64 {
return d.Duration.Minutes()
}
func (d Duration) Hours() float64 {
return d.Duration.Hours()
}

View File

@ -0,0 +1,163 @@
package domain
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" validate:"required,oneof=individual team"`
Name string `json:"name" validate:"required,min=1,max=255"`
Owner string `json:"owner" validate:"required,min=1,max=255"`
}
// FunctionDefinition represents a serverless function
type FunctionDefinition struct {
ID uuid.UUID `json:"id" db:"id"`
Name string `json:"name" validate:"required,min=1,max=255" db:"name"`
AppID string `json:"app_id" validate:"required" db:"app_id"`
Runtime RuntimeType `json:"runtime" validate:"required" db:"runtime"`
Image string `json:"image" validate:"required" db:"image"`
Handler string `json:"handler" validate:"required" db:"handler"`
Code string `json:"code,omitempty" db:"code"`
Environment map[string]string `json:"environment,omitempty" db:"environment"`
Timeout Duration `json:"timeout" validate:"required" db:"timeout"`
Memory int `json:"memory" validate:"required,min=64,max=3008" db:"memory"`
Owner Owner `json:"owner" validate:"required"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// FunctionExecution represents a function execution
type FunctionExecution struct {
ID uuid.UUID `json:"id" db:"id"`
FunctionID uuid.UUID `json:"function_id" db:"function_id"`
Status ExecutionStatus `json:"status" db:"status"`
Input json.RawMessage `json:"input,omitempty" db:"input"`
Output json.RawMessage `json:"output,omitempty" db:"output"`
Error string `json:"error,omitempty" db:"error"`
Duration time.Duration `json:"duration" db:"duration"`
MemoryUsed int `json:"memory_used" db:"memory_used"`
ContainerID string `json:"container_id,omitempty" db:"container_id"`
ExecutorID string `json:"executor_id" db:"executor_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
StartedAt *time.Time `json:"started_at,omitempty" db:"started_at"`
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
}
// CreateFunctionRequest represents a request to create a new function
type CreateFunctionRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
AppID string `json:"app_id" validate:"required"`
Runtime RuntimeType `json:"runtime" validate:"required"`
Image string `json:"image" validate:"required"`
Handler string `json:"handler" validate:"required"`
Code string `json:"code,omitempty"`
Environment map[string]string `json:"environment,omitempty"`
Timeout Duration `json:"timeout" validate:"required"`
Memory int `json:"memory" validate:"required,min=64,max=3008"`
Owner Owner `json:"owner" validate:"required"`
}
// UpdateFunctionRequest represents a request to update an existing function
type UpdateFunctionRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
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" validate:"omitempty,min=64,max=3008"`
Owner *Owner `json:"owner,omitempty"`
}
// ExecuteFunctionRequest represents a request to execute a function
type ExecuteFunctionRequest struct {
FunctionID uuid.UUID `json:"function_id" validate:"required"`
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" validate:"required"`
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"`
}
// RuntimeInfo represents runtime information
type RuntimeInfo struct {
Type RuntimeType `json:"type"`
Version string `json:"version"`
Available bool `json:"available"`
DefaultImage string `json:"default_image"`
Description string `json:"description"`
}
// ExecutionResult contains function execution results
type ExecutionResult struct {
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"`
}
// AuthContext represents the authentication context for a request
type AuthContext struct {
UserID string `json:"user_id"`
AppID string `json:"app_id"`
Permissions []string `json:"permissions"`
Claims map[string]string `json:"claims"`
}

View File

@ -0,0 +1,70 @@
package domain
// RuntimeConfig defines the configuration for each runtime
type RuntimeConfig struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Image string `json:"image"`
Handler string `json:"default_handler"`
Extensions []string `json:"file_extensions"`
Environment map[string]string `json:"default_environment"`
}
// GetRuntimeConfigs returns the available runtime configurations
func GetRuntimeConfigs() map[RuntimeType]RuntimeConfig {
return map[RuntimeType]RuntimeConfig{
"nodejs18": {
Name: "nodejs18",
DisplayName: "Node.js 18.x",
Image: "node:18-alpine",
Handler: "index.handler",
Extensions: []string{".js", ".mjs", ".ts"},
Environment: map[string]string{
"NODE_ENV": "production",
},
},
"python3.9": {
Name: "python3.9",
DisplayName: "Python 3.9",
Image: "python:3.9-alpine",
Handler: "main.handler",
Extensions: []string{".py"},
Environment: map[string]string{
"PYTHONPATH": "/app",
},
},
"go1.20": {
Name: "go1.20",
DisplayName: "Go 1.20",
Image: "golang:1.20-alpine",
Handler: "main.Handler",
Extensions: []string{".go"},
Environment: map[string]string{
"CGO_ENABLED": "0",
"GOOS": "linux",
},
},
}
}
// GetRuntimeConfig returns the configuration for a specific runtime
func GetRuntimeConfig(runtime RuntimeType) (RuntimeConfig, bool) {
configs := GetRuntimeConfigs()
config, exists := configs[runtime]
return config, exists
}
// GetAvailableRuntimes returns a list of available runtimes for the frontend
func GetAvailableRuntimes() []map[string]string {
configs := GetRuntimeConfigs()
var runtimes []map[string]string
for _, config := range configs {
runtimes = append(runtimes, map[string]string{
"value": config.Name,
"label": config.DisplayName,
})
}
return runtimes
}

View File

@ -0,0 +1,261 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/services"
)
type ExecutionHandler struct {
executionService services.ExecutionService
authService services.AuthService
logger *zap.Logger
}
func NewExecutionHandler(executionService services.ExecutionService, authService services.AuthService, logger *zap.Logger) *ExecutionHandler {
return &ExecutionHandler{
executionService: executionService,
authService: authService,
logger: logger,
}
}
func (h *ExecutionHandler) Execute(c *gin.Context) {
idStr := c.Param("id")
functionID, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid function ID"})
return
}
var req domain.ExecuteFunctionRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid execute function request", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
req.FunctionID = functionID
authCtx, err := h.authService.GetAuthContext(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get auth context", zap.Error(err))
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.execute") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
response, err := h.executionService.Execute(c.Request.Context(), &req, authCtx.UserID)
if err != nil {
h.logger.Error("Failed to execute function", zap.String("function_id", idStr), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("Function execution initiated",
zap.String("function_id", functionID.String()),
zap.String("execution_id", response.ExecutionID.String()),
zap.String("user_id", authCtx.UserID),
zap.Bool("async", req.Async))
c.JSON(http.StatusOK, response)
}
func (h *ExecutionHandler) Invoke(c *gin.Context) {
idStr := c.Param("id")
functionID, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid function ID"})
return
}
var req domain.ExecuteFunctionRequest
if err := c.ShouldBindJSON(&req); err != nil {
// Allow empty body
req = domain.ExecuteFunctionRequest{
FunctionID: functionID,
Async: true,
}
}
req.FunctionID = functionID
req.Async = true // Force async for invoke endpoint
authCtx, err := h.authService.GetAuthContext(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get auth context", zap.Error(err))
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.execute") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
response, err := h.executionService.Execute(c.Request.Context(), &req, authCtx.UserID)
if err != nil {
h.logger.Error("Failed to invoke function", zap.String("function_id", idStr), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("Function invoked successfully",
zap.String("function_id", functionID.String()),
zap.String("execution_id", response.ExecutionID.String()),
zap.String("user_id", authCtx.UserID))
c.JSON(http.StatusAccepted, response)
}
func (h *ExecutionHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid execution ID"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.read") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
execution, err := h.executionService.GetByID(c.Request.Context(), id)
if err != nil {
h.logger.Error("Failed to get execution", zap.String("id", idStr), zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "Execution not found"})
return
}
c.JSON(http.StatusOK, execution)
}
func (h *ExecutionHandler) List(c *gin.Context) {
if !h.authService.HasPermission(c.Request.Context(), "faas.read") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
var functionID *uuid.UUID
functionIDStr := c.Query("function_id")
if functionIDStr != "" {
id, err := uuid.Parse(functionIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid function ID"})
return
}
functionID = &id
}
limitStr := c.DefaultQuery("limit", "50")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 50
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
executions, err := h.executionService.List(c.Request.Context(), functionID, limit, offset)
if err != nil {
h.logger.Error("Failed to list executions", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"executions": executions,
"limit": limit,
"offset": offset,
})
}
func (h *ExecutionHandler) Cancel(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid execution ID"})
return
}
authCtx, err := h.authService.GetAuthContext(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get auth context", zap.Error(err))
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.execute") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
if err := h.executionService.Cancel(c.Request.Context(), id, authCtx.UserID); err != nil {
h.logger.Error("Failed to cancel execution", zap.String("id", idStr), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("Execution canceled successfully",
zap.String("execution_id", id.String()),
zap.String("user_id", authCtx.UserID))
c.JSON(http.StatusOK, gin.H{"message": "Execution canceled successfully"})
}
func (h *ExecutionHandler) GetLogs(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid execution ID"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.read") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
logs, err := h.executionService.GetLogs(c.Request.Context(), id)
if err != nil {
h.logger.Error("Failed to get execution logs", zap.String("id", idStr), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"logs": logs,
})
}
func (h *ExecutionHandler) GetRunning(c *gin.Context) {
if !h.authService.HasPermission(c.Request.Context(), "faas.read") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
executions, err := h.executionService.GetRunningExecutions(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get running executions", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"executions": executions,
"count": len(executions),
})
}

View File

@ -0,0 +1,244 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/services"
)
type FunctionHandler struct {
functionService services.FunctionService
authService services.AuthService
logger *zap.Logger
}
func NewFunctionHandler(functionService services.FunctionService, authService services.AuthService, logger *zap.Logger) *FunctionHandler {
return &FunctionHandler{
functionService: functionService,
authService: authService,
logger: logger,
}
}
func (h *FunctionHandler) Create(c *gin.Context) {
var req domain.CreateFunctionRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid create function request", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
// Auto-select image based on runtime if not provided or empty
if req.Image == "" {
if runtimeConfig, exists := domain.GetRuntimeConfig(req.Runtime); exists && runtimeConfig.Image != "" {
req.Image = runtimeConfig.Image
}
}
authCtx, err := h.authService.GetAuthContext(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get auth context", zap.Error(err))
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.write") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
function, err := h.functionService.Create(c.Request.Context(), &req, authCtx.UserID)
if err != nil {
h.logger.Error("Failed to create function", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("Function created successfully",
zap.String("function_id", function.ID.String()),
zap.String("name", function.Name),
zap.String("user_id", authCtx.UserID))
c.JSON(http.StatusCreated, function)
}
func (h *FunctionHandler) GetByID(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid function ID"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.read") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
function, err := h.functionService.GetByID(c.Request.Context(), id)
if err != nil {
h.logger.Error("Failed to get function", zap.String("id", idStr), zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "Function not found"})
return
}
c.JSON(http.StatusOK, function)
}
func (h *FunctionHandler) Update(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid function ID"})
return
}
var req domain.UpdateFunctionRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid update function request", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
authCtx, err := h.authService.GetAuthContext(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get auth context", zap.Error(err))
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.write") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
function, err := h.functionService.Update(c.Request.Context(), id, &req, authCtx.UserID)
if err != nil {
h.logger.Error("Failed to update function", zap.String("id", idStr), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("Function updated successfully",
zap.String("function_id", id.String()),
zap.String("user_id", authCtx.UserID))
c.JSON(http.StatusOK, function)
}
func (h *FunctionHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid function ID"})
return
}
authCtx, err := h.authService.GetAuthContext(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get auth context", zap.Error(err))
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.delete") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
if err := h.functionService.Delete(c.Request.Context(), id, authCtx.UserID); err != nil {
h.logger.Error("Failed to delete function", zap.String("id", idStr), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("Function deleted successfully",
zap.String("function_id", id.String()),
zap.String("user_id", authCtx.UserID))
c.JSON(http.StatusOK, gin.H{"message": "Function deleted successfully"})
}
func (h *FunctionHandler) List(c *gin.Context) {
if !h.authService.HasPermission(c.Request.Context(), "faas.read") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
appID := c.Query("app_id")
limitStr := c.DefaultQuery("limit", "50")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 50
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
functions, err := h.functionService.List(c.Request.Context(), appID, limit, offset)
if err != nil {
h.logger.Error("Failed to list functions", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"functions": functions,
"limit": limit,
"offset": offset,
})
}
func (h *FunctionHandler) Deploy(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid function ID"})
return
}
var req domain.DeployFunctionRequest
if err := c.ShouldBindJSON(&req); err != nil {
// Allow empty body for deploy
req = domain.DeployFunctionRequest{
FunctionID: id,
Force: false,
}
}
req.FunctionID = id
authCtx, err := h.authService.GetAuthContext(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get auth context", zap.Error(err))
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
if !h.authService.HasPermission(c.Request.Context(), "faas.deploy") {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
return
}
response, err := h.functionService.Deploy(c.Request.Context(), id, &req, authCtx.UserID)
if err != nil {
h.logger.Error("Failed to deploy function", zap.String("id", idStr), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.logger.Info("Function deployed successfully",
zap.String("function_id", id.String()),
zap.String("user_id", authCtx.UserID))
c.JSON(http.StatusOK, response)
}

View File

@ -0,0 +1,70 @@
package handlers
import (
"database/sql"
"net/http"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type HealthHandler struct {
db *sql.DB
logger *zap.Logger
}
func NewHealthHandler(db *sql.DB, logger *zap.Logger) *HealthHandler {
return &HealthHandler{
db: db,
logger: logger,
}
}
func (h *HealthHandler) Health(c *gin.Context) {
h.logger.Debug("Health check requested")
response := gin.H{
"status": "healthy",
"service": "faas",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"version": "1.0.0",
}
c.JSON(http.StatusOK, response)
}
func (h *HealthHandler) Ready(c *gin.Context) {
h.logger.Debug("Readiness check requested")
checks := make(map[string]interface{})
overall := "ready"
// Check database connection
if err := h.db.Ping(); err != nil {
h.logger.Error("Database health check failed", zap.Error(err))
checks["database"] = gin.H{
"status": "unhealthy",
"error": err.Error(),
}
overall = "not ready"
} else {
checks["database"] = gin.H{
"status": "healthy",
}
}
response := gin.H{
"status": overall,
"service": "faas",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"checks": checks,
}
statusCode := http.StatusOK
if overall != "ready" {
statusCode = http.StatusServiceUnavailable
}
c.JSON(statusCode, response)
}

View File

@ -0,0 +1,32 @@
package repository
import (
"context"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/google/uuid"
)
// FunctionRepository provides CRUD operations for functions
type FunctionRepository interface {
Create(ctx context.Context, function *domain.FunctionDefinition) (*domain.FunctionDefinition, error)
GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionDefinition, error)
GetByName(ctx context.Context, appID, name string) (*domain.FunctionDefinition, error)
Update(ctx context.Context, id uuid.UUID, updates *domain.UpdateFunctionRequest) (*domain.FunctionDefinition, error)
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, appID string, limit, offset int) ([]*domain.FunctionDefinition, error)
GetByAppID(ctx context.Context, appID string) ([]*domain.FunctionDefinition, error)
}
// ExecutionRepository provides CRUD operations for function executions
type ExecutionRepository interface {
Create(ctx context.Context, execution *domain.FunctionExecution) (*domain.FunctionExecution, error)
GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionExecution, error)
Update(ctx context.Context, id uuid.UUID, execution *domain.FunctionExecution) (*domain.FunctionExecution, error)
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, functionID *uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error)
GetByFunctionID(ctx context.Context, functionID uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error)
GetByStatus(ctx context.Context, status domain.ExecutionStatus, limit, offset int) ([]*domain.FunctionExecution, error)
UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ExecutionStatus) error
GetRunningExecutions(ctx context.Context) ([]*domain.FunctionExecution, error)
}

View File

@ -0,0 +1,260 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/repository"
)
type executionRepository struct {
db *sql.DB
logger *zap.Logger
}
func NewExecutionRepository(db *sql.DB, logger *zap.Logger) repository.ExecutionRepository {
return &executionRepository{
db: db,
logger: logger,
}
}
func (r *executionRepository) Create(ctx context.Context, execution *domain.FunctionExecution) (*domain.FunctionExecution, error) {
query := `
INSERT INTO executions (id, function_id, status, input, executor_id, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING created_at`
err := r.db.QueryRowContext(ctx, query,
execution.ID, execution.FunctionID, execution.Status, execution.Input,
execution.ExecutorID, execution.CreatedAt,
).Scan(&execution.CreatedAt)
if err != nil {
r.logger.Error("Failed to create execution", zap.Error(err))
return nil, fmt.Errorf("failed to create execution: %w", err)
}
return execution, nil
}
func (r *executionRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionExecution, error) {
query := `
SELECT id, function_id, status, input, output, error, duration, memory_used,
container_id, executor_id, created_at, started_at, completed_at
FROM executions WHERE id = $1`
execution := &domain.FunctionExecution{}
var durationNanos sql.NullInt64
err := r.db.QueryRowContext(ctx, query, id).Scan(
&execution.ID, &execution.FunctionID, &execution.Status, &execution.Input,
&execution.Output, &execution.Error, &durationNanos, &execution.MemoryUsed,
&execution.ContainerID, &execution.ExecutorID, &execution.CreatedAt,
&execution.StartedAt, &execution.CompletedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("execution not found")
}
r.logger.Error("Failed to get execution by ID", zap.String("id", id.String()), zap.Error(err))
return nil, fmt.Errorf("failed to get execution: %w", err)
}
// Convert duration
if durationNanos.Valid {
execution.Duration = time.Duration(durationNanos.Int64)
}
return execution, nil
}
func (r *executionRepository) Update(ctx context.Context, id uuid.UUID, execution *domain.FunctionExecution) (*domain.FunctionExecution, error) {
query := `
UPDATE executions
SET status = $2, output = $3, error = $4, duration = $5, memory_used = $6,
container_id = $7, started_at = $8, completed_at = $9
WHERE id = $1`
var durationNanos sql.NullInt64
if execution.Duration > 0 {
durationNanos.Int64 = int64(execution.Duration)
durationNanos.Valid = true
}
_, err := r.db.ExecContext(ctx, query,
id, execution.Status, execution.Output, execution.Error,
durationNanos, execution.MemoryUsed, execution.ContainerID,
execution.StartedAt, execution.CompletedAt,
)
if err != nil {
r.logger.Error("Failed to update execution", zap.String("id", id.String()), zap.Error(err))
return nil, fmt.Errorf("failed to update execution: %w", err)
}
// Return updated execution
return r.GetByID(ctx, id)
}
func (r *executionRepository) Delete(ctx context.Context, id uuid.UUID) error {
query := `DELETE FROM executions WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
r.logger.Error("Failed to delete execution", zap.String("id", id.String()), zap.Error(err))
return fmt.Errorf("failed to delete execution: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("execution not found")
}
return nil
}
func (r *executionRepository) List(ctx context.Context, functionID *uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) {
var query string
var args []interface{}
if functionID != nil {
query = `
SELECT id, function_id, status, input, output, error, duration, memory_used,
container_id, executor_id, created_at, started_at, completed_at
FROM executions WHERE function_id = $1
ORDER BY created_at DESC LIMIT $2 OFFSET $3`
args = []interface{}{*functionID, limit, offset}
} else {
query = `
SELECT id, function_id, status, input, output, error, duration, memory_used,
container_id, executor_id, created_at, started_at, completed_at
FROM executions
ORDER BY created_at DESC LIMIT $1 OFFSET $2`
args = []interface{}{limit, offset}
}
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
r.logger.Error("Failed to list executions", zap.Error(err))
return nil, fmt.Errorf("failed to list executions: %w", err)
}
defer rows.Close()
var executions []*domain.FunctionExecution
for rows.Next() {
execution := &domain.FunctionExecution{}
var durationNanos sql.NullInt64
err := rows.Scan(
&execution.ID, &execution.FunctionID, &execution.Status, &execution.Input,
&execution.Output, &execution.Error, &durationNanos, &execution.MemoryUsed,
&execution.ContainerID, &execution.ExecutorID, &execution.CreatedAt,
&execution.StartedAt, &execution.CompletedAt,
)
if err != nil {
r.logger.Error("Failed to scan execution", zap.Error(err))
return nil, fmt.Errorf("failed to scan execution: %w", err)
}
// Convert duration
if durationNanos.Valid {
execution.Duration = time.Duration(durationNanos.Int64)
}
executions = append(executions, execution)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate executions: %w", err)
}
return executions, nil
}
func (r *executionRepository) GetByFunctionID(ctx context.Context, functionID uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) {
return r.List(ctx, &functionID, limit, offset)
}
func (r *executionRepository) GetByStatus(ctx context.Context, status domain.ExecutionStatus, limit, offset int) ([]*domain.FunctionExecution, error) {
query := `
SELECT id, function_id, status, input, output, error, duration, memory_used,
container_id, executor_id, created_at, started_at, completed_at
FROM executions WHERE status = $1
ORDER BY created_at DESC LIMIT $2 OFFSET $3`
rows, err := r.db.QueryContext(ctx, query, status, limit, offset)
if err != nil {
r.logger.Error("Failed to get executions by status", zap.String("status", string(status)), zap.Error(err))
return nil, fmt.Errorf("failed to get executions by status: %w", err)
}
defer rows.Close()
var executions []*domain.FunctionExecution
for rows.Next() {
execution := &domain.FunctionExecution{}
var durationNanos sql.NullInt64
err := rows.Scan(
&execution.ID, &execution.FunctionID, &execution.Status, &execution.Input,
&execution.Output, &execution.Error, &durationNanos, &execution.MemoryUsed,
&execution.ContainerID, &execution.ExecutorID, &execution.CreatedAt,
&execution.StartedAt, &execution.CompletedAt,
)
if err != nil {
r.logger.Error("Failed to scan execution", zap.Error(err))
return nil, fmt.Errorf("failed to scan execution: %w", err)
}
// Convert duration
if durationNanos.Valid {
execution.Duration = time.Duration(durationNanos.Int64)
}
executions = append(executions, execution)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate executions: %w", err)
}
return executions, nil
}
func (r *executionRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ExecutionStatus) error {
query := `UPDATE executions SET status = $2 WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id, status)
if err != nil {
r.logger.Error("Failed to update execution status", zap.String("id", id.String()), zap.Error(err))
return fmt.Errorf("failed to update execution status: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("execution not found")
}
return nil
}
func (r *executionRepository) GetRunningExecutions(ctx context.Context) ([]*domain.FunctionExecution, error) {
return r.GetByStatus(ctx, domain.StatusRunning, 1000, 0)
}

View File

@ -0,0 +1,278 @@
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/repository"
)
type functionRepository struct {
db *sql.DB
logger *zap.Logger
}
func NewFunctionRepository(db *sql.DB, logger *zap.Logger) repository.FunctionRepository {
return &functionRepository{
db: db,
logger: logger,
}
}
func (r *functionRepository) Create(ctx context.Context, function *domain.FunctionDefinition) (*domain.FunctionDefinition, error) {
envJSON, err := json.Marshal(function.Environment)
if err != nil {
return nil, fmt.Errorf("failed to marshal environment: %w", err)
}
query := `
INSERT INTO functions (id, name, app_id, runtime, image, handler, code, environment, timeout, memory,
owner_type, owner_name, owner_owner, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING created_at, updated_at`
err = r.db.QueryRowContext(ctx, query,
function.ID, function.Name, function.AppID, function.Runtime, function.Image,
function.Handler, function.Code, envJSON, function.Timeout.Duration,
function.Memory, function.Owner.Type, function.Owner.Name, function.Owner.Owner,
function.CreatedAt, function.UpdatedAt,
).Scan(&function.CreatedAt, &function.UpdatedAt)
if err != nil {
r.logger.Error("Failed to create function", zap.Error(err))
return nil, fmt.Errorf("failed to create function: %w", err)
}
return function, nil
}
func (r *functionRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionDefinition, error) {
query := `
SELECT id, name, app_id, runtime, image, handler, code, environment, timeout, memory,
owner_type, owner_name, owner_owner, created_at, updated_at
FROM functions WHERE id = $1`
function := &domain.FunctionDefinition{}
var envJSON []byte
var timeoutNanos int64
err := r.db.QueryRowContext(ctx, query, id).Scan(
&function.ID, &function.Name, &function.AppID, &function.Runtime, &function.Image,
&function.Handler, &function.Code, &envJSON, &timeoutNanos, &function.Memory,
&function.Owner.Type, &function.Owner.Name, &function.Owner.Owner,
&function.CreatedAt, &function.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("function not found")
}
r.logger.Error("Failed to get function by ID", zap.String("id", id.String()), zap.Error(err))
return nil, fmt.Errorf("failed to get function: %w", err)
}
// Unmarshal environment
if err := json.Unmarshal(envJSON, &function.Environment); err != nil {
return nil, fmt.Errorf("failed to unmarshal environment: %w", err)
}
// Convert timeout
function.Timeout.Duration = time.Duration(timeoutNanos)
return function, nil
}
func (r *functionRepository) GetByName(ctx context.Context, appID, name string) (*domain.FunctionDefinition, error) {
query := `
SELECT id, name, app_id, runtime, image, handler, code, environment, timeout, memory,
owner_type, owner_name, owner_owner, created_at, updated_at
FROM functions WHERE app_id = $1 AND name = $2`
function := &domain.FunctionDefinition{}
var envJSON []byte
var timeoutNanos int64
err := r.db.QueryRowContext(ctx, query, appID, name).Scan(
&function.ID, &function.Name, &function.AppID, &function.Runtime, &function.Image,
&function.Handler, &function.Code, &envJSON, &timeoutNanos, &function.Memory,
&function.Owner.Type, &function.Owner.Name, &function.Owner.Owner,
&function.CreatedAt, &function.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("function not found")
}
r.logger.Error("Failed to get function by name", zap.String("app_id", appID), zap.String("name", name), zap.Error(err))
return nil, fmt.Errorf("failed to get function: %w", err)
}
// Unmarshal environment
if err := json.Unmarshal(envJSON, &function.Environment); err != nil {
return nil, fmt.Errorf("failed to unmarshal environment: %w", err)
}
// Convert timeout
function.Timeout.Duration = time.Duration(timeoutNanos)
return function, nil
}
func (r *functionRepository) Update(ctx context.Context, id uuid.UUID, updates *domain.UpdateFunctionRequest) (*domain.FunctionDefinition, error) {
// First get the current function
current, err := r.GetByID(ctx, id)
if err != nil {
return nil, err
}
// Apply updates
if updates.Name != nil {
current.Name = *updates.Name
}
if updates.Runtime != nil {
current.Runtime = *updates.Runtime
}
if updates.Image != nil {
current.Image = *updates.Image
}
if updates.Handler != nil {
current.Handler = *updates.Handler
}
if updates.Code != nil {
current.Code = *updates.Code
}
if updates.Environment != nil {
current.Environment = updates.Environment
}
if updates.Timeout != nil {
current.Timeout = *updates.Timeout
}
if updates.Memory != nil {
current.Memory = *updates.Memory
}
if updates.Owner != nil {
current.Owner = *updates.Owner
}
// Marshal environment
envJSON, err := json.Marshal(current.Environment)
if err != nil {
return nil, fmt.Errorf("failed to marshal environment: %w", err)
}
query := `
UPDATE functions
SET name = $2, runtime = $3, image = $4, handler = $5, code = $6, environment = $7,
timeout = $8, memory = $9, owner_type = $10, owner_name = $11, owner_owner = $12,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING updated_at`
err = r.db.QueryRowContext(ctx, query,
id, current.Name, current.Runtime, current.Image, current.Handler,
current.Code, envJSON, int64(current.Timeout.Duration), current.Memory,
current.Owner.Type, current.Owner.Name, current.Owner.Owner,
).Scan(&current.UpdatedAt)
if err != nil {
r.logger.Error("Failed to update function", zap.String("id", id.String()), zap.Error(err))
return nil, fmt.Errorf("failed to update function: %w", err)
}
return current, nil
}
func (r *functionRepository) Delete(ctx context.Context, id uuid.UUID) error {
query := `DELETE FROM functions WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
r.logger.Error("Failed to delete function", zap.String("id", id.String()), zap.Error(err))
return fmt.Errorf("failed to delete function: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("function not found")
}
return nil
}
func (r *functionRepository) List(ctx context.Context, appID string, limit, offset int) ([]*domain.FunctionDefinition, error) {
var query string
var args []interface{}
if appID != "" {
query = `
SELECT id, name, app_id, runtime, image, handler, code, environment, timeout, memory,
owner_type, owner_name, owner_owner, created_at, updated_at
FROM functions WHERE app_id = $1
ORDER BY created_at DESC LIMIT $2 OFFSET $3`
args = []interface{}{appID, limit, offset}
} else {
query = `
SELECT id, name, app_id, runtime, image, handler, code, environment, timeout, memory,
owner_type, owner_name, owner_owner, created_at, updated_at
FROM functions
ORDER BY created_at DESC LIMIT $1 OFFSET $2`
args = []interface{}{limit, offset}
}
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
r.logger.Error("Failed to list functions", zap.Error(err))
return nil, fmt.Errorf("failed to list functions: %w", err)
}
defer rows.Close()
var functions []*domain.FunctionDefinition
for rows.Next() {
function := &domain.FunctionDefinition{}
var envJSON []byte
var timeoutNanos int64
err := rows.Scan(
&function.ID, &function.Name, &function.AppID, &function.Runtime, &function.Image,
&function.Handler, &function.Code, &envJSON, &timeoutNanos, &function.Memory,
&function.Owner.Type, &function.Owner.Name, &function.Owner.Owner,
&function.CreatedAt, &function.UpdatedAt,
)
if err != nil {
r.logger.Error("Failed to scan function", zap.Error(err))
return nil, fmt.Errorf("failed to scan function: %w", err)
}
// Unmarshal environment
if err := json.Unmarshal(envJSON, &function.Environment); err != nil {
return nil, fmt.Errorf("failed to unmarshal environment: %w", err)
}
// Convert timeout
function.Timeout.Duration = time.Duration(timeoutNanos)
functions = append(functions, function)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate functions: %w", err)
}
return functions, nil
}
func (r *functionRepository) GetByAppID(ctx context.Context, appID string) ([]*domain.FunctionDefinition, error) {
return r.List(ctx, appID, 1000, 0) // Get all functions for the app
}

View File

@ -0,0 +1,431 @@
package docker
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/runtime"
)
type DockerRuntime struct {
client *client.Client
logger *zap.Logger
config *Config
}
type Config struct {
DockerHost string `json:"docker_host"`
NetworkMode string `json:"network_mode"`
SecurityOpts []string `json:"security_opts"`
DefaultLabels map[string]string `json:"default_labels"`
MaxCPUs float64 `json:"max_cpus"`
MaxMemory int64 `json:"max_memory"`
TimeoutSeconds int `json:"timeout_seconds"`
}
func NewDockerRuntime(logger *zap.Logger, cfg *Config) (*DockerRuntime, error) {
if cfg == nil {
cfg = &Config{
NetworkMode: "bridge",
SecurityOpts: []string{"no-new-privileges:true"},
DefaultLabels: map[string]string{"service": "faas"},
MaxCPUs: 2.0,
MaxMemory: 512 * 1024 * 1024, // 512MB
TimeoutSeconds: 300,
}
}
var cli *client.Client
var err error
if cfg.DockerHost != "" {
cli, err = client.NewClientWithOpts(client.WithHost(cfg.DockerHost))
} else {
cli, err = client.NewClientWithOpts(client.FromEnv)
}
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %w", err)
}
return &DockerRuntime{
client: cli,
logger: logger,
config: cfg,
}, nil
}
func (d *DockerRuntime) Execute(ctx context.Context, function *domain.FunctionDefinition, input json.RawMessage) (*domain.ExecutionResult, error) {
executionID := uuid.New()
startTime := time.Now()
d.logger.Info("Starting function execution",
zap.String("function_id", function.ID.String()),
zap.String("execution_id", executionID.String()),
zap.String("image", function.Image))
// Create container configuration
containerConfig := &container.Config{
Image: function.Image,
Env: d.buildEnvironment(function, input),
Labels: map[string]string{
"faas.function_id": function.ID.String(),
"faas.execution_id": executionID.String(),
"faas.function_name": function.Name,
},
WorkingDir: "/app",
Cmd: []string{function.Handler},
}
// Add default labels
for k, v := range d.config.DefaultLabels {
containerConfig.Labels[k] = v
}
// Create host configuration with resource limits
hostConfig := &container.HostConfig{
Resources: container.Resources{
Memory: int64(function.Memory) * 1024 * 1024, // Convert MB to bytes
CPUQuota: int64(d.config.MaxCPUs * 100000), // CPU quota in microseconds
CPUPeriod: 100000, // CPU period in microseconds
},
NetworkMode: container.NetworkMode(d.config.NetworkMode),
SecurityOpt: d.config.SecurityOpts,
AutoRemove: true,
}
// Create container
resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, "")
if err != nil {
return &domain.ExecutionResult{
Error: fmt.Sprintf("failed to create container: %v", err),
Duration: time.Since(startTime),
}, nil
}
containerID := resp.ID
// Start container
if err := d.client.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
return &domain.ExecutionResult{
Error: fmt.Sprintf("failed to start container: %v", err),
Duration: time.Since(startTime),
}, nil
}
// Wait for container to finish with timeout
timeoutCtx, cancel := context.WithTimeout(ctx, function.Timeout.Duration)
defer cancel()
statusCh, errCh := d.client.ContainerWait(timeoutCtx, containerID, container.WaitConditionNotRunning)
var waitResult container.WaitResponse
select {
case result := <-statusCh:
waitResult = result
case err := <-errCh:
d.client.ContainerKill(ctx, containerID, "SIGTERM")
return &domain.ExecutionResult{
Error: fmt.Sprintf("container wait error: %v", err),
Duration: time.Since(startTime),
}, nil
case <-timeoutCtx.Done():
d.client.ContainerKill(ctx, containerID, "SIGTERM")
return &domain.ExecutionResult{
Error: "execution timeout",
Duration: time.Since(startTime),
}, nil
}
// Get container logs
logs, err := d.getContainerLogs(ctx, containerID)
if err != nil {
d.logger.Warn("Failed to get container logs", zap.Error(err))
}
// Get container stats for memory usage
memoryUsed := d.getMemoryUsage(ctx, containerID)
duration := time.Since(startTime)
// Parse output from logs if successful
var output json.RawMessage
var execError string
if waitResult.StatusCode == 0 {
// Extract output from logs (assuming last line contains JSON output)
if len(logs) > 0 {
lastLog := logs[len(logs)-1]
if json.Valid([]byte(lastLog)) {
output = json.RawMessage(lastLog)
} else {
output = json.RawMessage(fmt.Sprintf(`{"result": "%s"}`, lastLog))
}
}
} else {
execError = fmt.Sprintf("container exited with code %d", waitResult.StatusCode)
if len(logs) > 0 {
execError += ": " + strings.Join(logs, "\n")
}
}
d.logger.Info("Function execution completed",
zap.String("function_id", function.ID.String()),
zap.String("execution_id", executionID.String()),
zap.Duration("duration", duration),
zap.Int64("status_code", waitResult.StatusCode),
zap.Int("memory_used", memoryUsed))
return &domain.ExecutionResult{
Output: output,
Error: execError,
Duration: duration,
MemoryUsed: memoryUsed,
Logs: logs,
}, nil
}
func (d *DockerRuntime) Deploy(ctx context.Context, function *domain.FunctionDefinition) error {
d.logger.Info("Deploying function",
zap.String("function_id", function.ID.String()),
zap.String("image", function.Image))
// Pull image
reader, err := d.client.ImagePull(ctx, function.Image, image.PullOptions{})
if err != nil {
return fmt.Errorf("failed to pull image %s: %w", function.Image, err)
}
defer reader.Close()
// Read the pull response to ensure it completes
_, err = io.ReadAll(reader)
if err != nil {
return fmt.Errorf("failed to complete image pull: %w", err)
}
d.logger.Info("Function deployed successfully",
zap.String("function_id", function.ID.String()),
zap.String("image", function.Image))
return nil
}
func (d *DockerRuntime) Remove(ctx context.Context, functionID uuid.UUID) error {
d.logger.Info("Removing function containers", zap.String("function_id", functionID.String()))
// List containers with the function label
filters := filters.NewArgs()
filters.Add("label", fmt.Sprintf("faas.function_id=%s", functionID.String()))
containers, err := d.client.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters,
})
if err != nil {
return fmt.Errorf("failed to list containers: %w", err)
}
// Remove containers
for _, container := range containers {
if err := d.client.ContainerRemove(ctx, container.ID, struct {
Force bool
}{Force: true}); err != nil {
d.logger.Warn("Failed to remove container",
zap.String("container_id", container.ID),
zap.Error(err))
}
}
return nil
}
func (d *DockerRuntime) GetLogs(ctx context.Context, executionID uuid.UUID) ([]string, error) {
// Find container by execution ID
filters := filters.NewArgs()
filters.Add("label", fmt.Sprintf("faas.execution_id=%s", executionID.String()))
containers, err := d.client.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters,
})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}
if len(containers) == 0 {
return nil, fmt.Errorf("no container found for execution %s", executionID.String())
}
return d.getContainerLogs(ctx, containers[0].ID)
}
func (d *DockerRuntime) HealthCheck(ctx context.Context) error {
_, err := d.client.Ping(ctx)
if err != nil {
return fmt.Errorf("Docker daemon not accessible: %w", err)
}
return nil
}
func (d *DockerRuntime) GetInfo(ctx context.Context) (*runtime.RuntimeInfo, error) {
info, err := d.client.Info(ctx)
if err != nil {
return &runtime.RuntimeInfo{
Type: "docker",
Available: false,
}, nil
}
return &runtime.RuntimeInfo{
Type: "docker",
Version: info.ServerVersion,
Available: true,
Endpoint: d.client.DaemonHost(),
Metadata: map[string]string{
"containers": fmt.Sprintf("%d", info.Containers),
"images": fmt.Sprintf("%d", info.Images),
},
}, nil
}
func (d *DockerRuntime) ListContainers(ctx context.Context) ([]runtime.ContainerInfo, error) {
filters := filters.NewArgs()
filters.Add("label", "service=faas")
containers, err := d.client.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters,
})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}
var result []runtime.ContainerInfo
for _, container := range containers {
functionIDStr, exists := container.Labels["faas.function_id"]
if !exists {
continue
}
functionID, err := uuid.Parse(functionIDStr)
if err != nil {
continue
}
result = append(result, runtime.ContainerInfo{
ID: container.ID,
FunctionID: functionID,
Status: container.Status,
Image: container.Image,
CreatedAt: time.Unix(container.Created, 0).Format(time.RFC3339),
Labels: container.Labels,
})
}
return result, nil
}
func (d *DockerRuntime) StopExecution(ctx context.Context, executionID uuid.UUID) error {
// Find container by execution ID
filters := filters.NewArgs()
filters.Add("label", fmt.Sprintf("faas.execution_id=%s", executionID.String()))
containers, err := d.client.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters,
})
if err != nil {
return fmt.Errorf("failed to list containers: %w", err)
}
if len(containers) == 0 {
return fmt.Errorf("no container found for execution %s", executionID.String())
}
// Stop container
timeout := 10
return d.client.ContainerStop(ctx, containers[0].ID, container.StopOptions{Timeout: &timeout})
}
func (d *DockerRuntime) buildEnvironment(function *domain.FunctionDefinition, input json.RawMessage) []string {
env := []string{
fmt.Sprintf("FAAS_FUNCTION_ID=%s", function.ID.String()),
fmt.Sprintf("FAAS_FUNCTION_NAME=%s", function.Name),
fmt.Sprintf("FAAS_RUNTIME=%s", function.Runtime),
fmt.Sprintf("FAAS_HANDLER=%s", function.Handler),
fmt.Sprintf("FAAS_MEMORY=%d", function.Memory),
fmt.Sprintf("FAAS_TIMEOUT=%s", function.Timeout.String()),
}
// Add function-specific environment variables
for key, value := range function.Environment {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
// Add input as environment variable if provided
if input != nil {
env = append(env, fmt.Sprintf("FAAS_INPUT=%s", string(input)))
}
return env
}
func (d *DockerRuntime) getContainerLogs(ctx context.Context, containerID string) ([]string, error) {
options := container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: false,
}
reader, err := d.client.ContainerLogs(ctx, containerID, options)
if err != nil {
return nil, fmt.Errorf("failed to get container logs: %w", err)
}
defer reader.Close()
logs, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read container logs: %w", err)
}
// Split logs into lines and remove empty lines
lines := strings.Split(string(logs), "\n")
var result []string
for _, line := range lines {
if strings.TrimSpace(line) != "" {
result = append(result, line)
}
}
return result, nil
}
func (d *DockerRuntime) getMemoryUsage(ctx context.Context, containerID string) int {
stats, err := d.client.ContainerStats(ctx, containerID, false)
if err != nil {
d.logger.Warn("Failed to get container stats", zap.Error(err))
return 0
}
defer stats.Body.Close()
var containerStats struct {
MemoryStats struct {
Usage uint64 `json:"usage"`
} `json:"memory_stats"`
}
if err := json.NewDecoder(stats.Body).Decode(&containerStats); err != nil {
d.logger.Warn("Failed to decode container stats", zap.Error(err))
return 0
}
// Return memory usage in MB
return int(containerStats.MemoryStats.Usage / 1024 / 1024)
}

View File

@ -0,0 +1,84 @@
package docker
import (
"context"
"encoding/json"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/runtime"
)
type SimpleDockerRuntime struct {
logger *zap.Logger
}
func NewSimpleDockerRuntime(logger *zap.Logger) *SimpleDockerRuntime {
return &SimpleDockerRuntime{
logger: logger,
}
}
func (s *SimpleDockerRuntime) Execute(ctx context.Context, function *domain.FunctionDefinition, input json.RawMessage) (*domain.ExecutionResult, error) {
s.logger.Info("Mock function execution",
zap.String("function_id", function.ID.String()),
zap.String("name", function.Name))
// Mock execution result
result := &domain.ExecutionResult{
Output: json.RawMessage(`{"result": "Hello from FaaS!", "function": "` + function.Name + `"}`),
Duration: function.Timeout.Duration / 10, // Simulate quick execution
MemoryUsed: function.Memory / 2, // Simulate partial memory usage
Logs: []string{"Function started", "Processing input", "Function completed"},
}
return result, nil
}
func (s *SimpleDockerRuntime) Deploy(ctx context.Context, function *domain.FunctionDefinition) error {
s.logger.Info("Mock function deployment",
zap.String("function_id", function.ID.String()),
zap.String("image", function.Image))
return nil
}
func (s *SimpleDockerRuntime) Remove(ctx context.Context, functionID uuid.UUID) error {
s.logger.Info("Mock function removal", zap.String("function_id", functionID.String()))
return nil
}
func (s *SimpleDockerRuntime) GetLogs(ctx context.Context, executionID uuid.UUID) ([]string, error) {
return []string{
"Function execution started",
"Processing request",
"Function execution completed",
}, nil
}
func (s *SimpleDockerRuntime) HealthCheck(ctx context.Context) error {
return nil
}
func (s *SimpleDockerRuntime) GetInfo(ctx context.Context) (*runtime.RuntimeInfo, error) {
return &runtime.RuntimeInfo{
Type: "simple-docker",
Version: "mock-1.0",
Available: true,
Endpoint: "mock://docker",
Metadata: map[string]string{
"containers": "0",
"images": "0",
},
}, nil
}
func (s *SimpleDockerRuntime) ListContainers(ctx context.Context) ([]runtime.ContainerInfo, error) {
return []runtime.ContainerInfo{}, nil
}
func (s *SimpleDockerRuntime) StopExecution(ctx context.Context, executionID uuid.UUID) error {
s.logger.Info("Mock execution stop", zap.String("execution_id", executionID.String()))
return nil
}

View File

@ -0,0 +1,62 @@
package runtime
import (
"context"
"encoding/json"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/google/uuid"
)
// RuntimeBackend provides function execution capabilities
type RuntimeBackend interface {
// Execute runs a function with given input
Execute(ctx context.Context, function *domain.FunctionDefinition, input json.RawMessage) (*domain.ExecutionResult, error)
// Deploy prepares function for execution
Deploy(ctx context.Context, function *domain.FunctionDefinition) error
// Remove cleans up function resources
Remove(ctx context.Context, functionID uuid.UUID) error
// GetLogs retrieves execution logs
GetLogs(ctx context.Context, executionID uuid.UUID) ([]string, error)
// HealthCheck verifies runtime availability
HealthCheck(ctx context.Context) error
// GetInfo returns runtime information
GetInfo(ctx context.Context) (*RuntimeInfo, error)
// ListContainers returns active containers for functions
ListContainers(ctx context.Context) ([]ContainerInfo, error)
// StopExecution stops a running execution
StopExecution(ctx context.Context, executionID uuid.UUID) error
}
// RuntimeInfo contains runtime backend information
type RuntimeInfo struct {
Type string `json:"type"`
Version string `json:"version"`
Available bool `json:"available"`
Endpoint string `json:"endpoint,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// ContainerInfo contains information about a running container
type ContainerInfo struct {
ID string `json:"id"`
FunctionID uuid.UUID `json:"function_id"`
Status string `json:"status"`
Image string `json:"image"`
CreatedAt string `json:"created_at"`
Labels map[string]string `json:"labels,omitempty"`
}
// RuntimeFactory creates runtime backends
type RuntimeFactory interface {
CreateRuntime(ctx context.Context, runtimeType string, config map[string]interface{}) (RuntimeBackend, error)
GetSupportedRuntimes() []string
GetDefaultConfig(runtimeType string) map[string]interface{}
}

View File

@ -0,0 +1,75 @@
package services
import (
"context"
"fmt"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
)
type authService struct {
logger *zap.Logger
}
func NewAuthService(logger *zap.Logger) AuthService {
return &authService{
logger: logger,
}
}
// Mock implementation for now - this should integrate with the KMS auth system
func (s *authService) GetAuthContext(ctx context.Context) (*domain.AuthContext, error) {
// For now, return a mock auth context
// In a real implementation, this would extract auth info from the request context
// that was set by middleware that validates tokens with the KMS service
return &domain.AuthContext{
UserID: "admin@example.com",
AppID: "faas-service",
Permissions: []string{"faas.read", "faas.write", "faas.execute", "faas.deploy", "faas.delete"},
Claims: map[string]string{
"user_type": "admin",
},
}, nil
}
func (s *authService) HasPermission(ctx context.Context, permission string) bool {
authCtx, err := s.GetAuthContext(ctx)
if err != nil {
s.logger.Warn("Failed to get auth context for permission check", zap.Error(err))
return false
}
// Check for exact permission match
for _, perm := range authCtx.Permissions {
if perm == permission {
return true
}
// Check for wildcard permissions (e.g., "faas.*" grants all faas permissions)
if len(perm) > 2 && perm[len(perm)-1] == '*' {
prefix := perm[:len(perm)-1]
if len(permission) >= len(prefix) && permission[:len(prefix)] == prefix {
return true
}
}
}
s.logger.Debug("Permission denied",
zap.String("user_id", authCtx.UserID),
zap.String("permission", permission),
zap.Strings("user_permissions", authCtx.Permissions))
return false
}
func (s *authService) ValidatePermissions(ctx context.Context, permissions []string) error {
for _, permission := range permissions {
if !s.HasPermission(ctx, permission) {
return fmt.Errorf("insufficient permission: %s", permission)
}
}
return nil
}

View File

@ -0,0 +1,309 @@
package services
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/repository"
"github.com/google/uuid"
)
type executionService struct {
executionRepo repository.ExecutionRepository
functionRepo repository.FunctionRepository
runtimeService RuntimeService
logger *zap.Logger
}
func NewExecutionService(
executionRepo repository.ExecutionRepository,
functionRepo repository.FunctionRepository,
runtimeService RuntimeService,
logger *zap.Logger,
) ExecutionService {
return &executionService{
executionRepo: executionRepo,
functionRepo: functionRepo,
runtimeService: runtimeService,
logger: logger,
}
}
func (s *executionService) Execute(ctx context.Context, req *domain.ExecuteFunctionRequest, userID string) (*domain.ExecuteFunctionResponse, error) {
s.logger.Info("Executing function",
zap.String("function_id", req.FunctionID.String()),
zap.String("user_id", userID),
zap.Bool("async", req.Async))
// Get function definition
function, err := s.functionRepo.GetByID(ctx, req.FunctionID)
if err != nil {
return nil, fmt.Errorf("function not found: %w", err)
}
// Create execution record
execution := &domain.FunctionExecution{
ID: uuid.New(),
FunctionID: req.FunctionID,
Status: domain.StatusPending,
Input: req.Input,
ExecutorID: userID,
CreatedAt: time.Now(),
}
// Store execution
createdExecution, err := s.executionRepo.Create(ctx, execution)
if err != nil {
s.logger.Error("Failed to create execution record",
zap.String("function_id", req.FunctionID.String()),
zap.Error(err))
return nil, fmt.Errorf("failed to create execution record: %w", err)
}
if req.Async {
// Start async execution
go s.executeAsync(context.Background(), createdExecution, function)
return &domain.ExecuteFunctionResponse{
ExecutionID: createdExecution.ID,
Status: domain.StatusPending,
}, nil
} else {
// Execute synchronously
return s.executeSync(ctx, createdExecution, function)
}
}
func (s *executionService) executeSync(ctx context.Context, execution *domain.FunctionExecution, function *domain.FunctionDefinition) (*domain.ExecuteFunctionResponse, error) {
// Update status to running
execution.Status = domain.StatusRunning
execution.StartedAt = &[]time.Time{time.Now()}[0]
if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
s.logger.Warn("Failed to update execution status to running", zap.Error(err))
}
// Get runtime backend
backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
if err != nil {
execution.Status = domain.StatusFailed
execution.Error = fmt.Sprintf("failed to get runtime backend: %v", err)
s.updateExecutionComplete(ctx, execution)
return &domain.ExecuteFunctionResponse{
ExecutionID: execution.ID,
Status: domain.StatusFailed,
Error: execution.Error,
}, nil
}
// Execute function
result, err := backend.Execute(ctx, function, execution.Input)
if err != nil {
execution.Status = domain.StatusFailed
execution.Error = fmt.Sprintf("execution failed: %v", err)
s.updateExecutionComplete(ctx, execution)
return &domain.ExecuteFunctionResponse{
ExecutionID: execution.ID,
Status: domain.StatusFailed,
Error: execution.Error,
}, nil
}
// Update execution with results
execution.Status = domain.StatusCompleted
execution.Output = result.Output
execution.Error = result.Error
execution.Duration = result.Duration
execution.MemoryUsed = result.MemoryUsed
s.updateExecutionComplete(ctx, execution)
if result.Error != "" {
execution.Status = domain.StatusFailed
}
return &domain.ExecuteFunctionResponse{
ExecutionID: execution.ID,
Status: execution.Status,
Output: execution.Output,
Error: execution.Error,
Duration: execution.Duration,
MemoryUsed: execution.MemoryUsed,
}, nil
}
func (s *executionService) executeAsync(ctx context.Context, execution *domain.FunctionExecution, function *domain.FunctionDefinition) {
// Update status to running
execution.Status = domain.StatusRunning
execution.StartedAt = &[]time.Time{time.Now()}[0]
if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
s.logger.Warn("Failed to update execution status to running", zap.Error(err))
}
// Get runtime backend
backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
if err != nil {
s.logger.Error("Failed to get runtime backend for async execution",
zap.String("execution_id", execution.ID.String()),
zap.Error(err))
execution.Status = domain.StatusFailed
execution.Error = fmt.Sprintf("failed to get runtime backend: %v", err)
s.updateExecutionComplete(ctx, execution)
return
}
// Execute function
result, err := backend.Execute(ctx, function, execution.Input)
if err != nil {
s.logger.Error("Async function execution failed",
zap.String("execution_id", execution.ID.String()),
zap.Error(err))
execution.Status = domain.StatusFailed
execution.Error = fmt.Sprintf("execution failed: %v", err)
s.updateExecutionComplete(ctx, execution)
return
}
// Update execution with results
execution.Status = domain.StatusCompleted
execution.Output = result.Output
execution.Error = result.Error
execution.Duration = result.Duration
execution.MemoryUsed = result.MemoryUsed
if result.Error != "" {
execution.Status = domain.StatusFailed
}
s.updateExecutionComplete(ctx, execution)
s.logger.Info("Async function execution completed",
zap.String("execution_id", execution.ID.String()),
zap.String("status", string(execution.Status)),
zap.Duration("duration", execution.Duration))
}
func (s *executionService) updateExecutionComplete(ctx context.Context, execution *domain.FunctionExecution) {
execution.CompletedAt = &[]time.Time{time.Now()}[0]
if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
s.logger.Error("Failed to update execution completion",
zap.String("execution_id", execution.ID.String()),
zap.Error(err))
}
}
func (s *executionService) GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionExecution, error) {
execution, err := s.executionRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("execution not found: %w", err)
}
return execution, nil
}
func (s *executionService) List(ctx context.Context, functionID *uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) {
if limit <= 0 {
limit = 50 // Default limit
}
if limit > 100 {
limit = 100 // Max limit
}
return s.executionRepo.List(ctx, functionID, limit, offset)
}
func (s *executionService) GetByFunctionID(ctx context.Context, functionID uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) {
if limit <= 0 {
limit = 50 // Default limit
}
if limit > 100 {
limit = 100 // Max limit
}
return s.executionRepo.GetByFunctionID(ctx, functionID, limit, offset)
}
func (s *executionService) Cancel(ctx context.Context, id uuid.UUID, userID string) error {
s.logger.Info("Canceling execution",
zap.String("execution_id", id.String()),
zap.String("user_id", userID))
// Get execution
execution, err := s.executionRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("execution not found: %w", err)
}
// Check if execution is still running
if execution.Status != domain.StatusRunning && execution.Status != domain.StatusPending {
return fmt.Errorf("execution is not running (status: %s)", execution.Status)
}
// Get function to determine runtime
function, err := s.functionRepo.GetByID(ctx, execution.FunctionID)
if err != nil {
return fmt.Errorf("function not found: %w", err)
}
// Stop execution in runtime
backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
if err != nil {
return fmt.Errorf("failed to get runtime backend: %w", err)
}
if err := backend.StopExecution(ctx, id); err != nil {
s.logger.Warn("Failed to stop execution in runtime",
zap.String("execution_id", id.String()),
zap.Error(err))
}
// Update execution status
execution.Status = domain.StatusCanceled
execution.Error = "execution canceled by user"
execution.CompletedAt = &[]time.Time{time.Now()}[0]
if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil {
return fmt.Errorf("failed to update execution status: %w", err)
}
s.logger.Info("Execution canceled successfully",
zap.String("execution_id", id.String()))
return nil
}
func (s *executionService) GetLogs(ctx context.Context, id uuid.UUID) ([]string, error) {
// Get execution
execution, err := s.executionRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("execution not found: %w", err)
}
// Get function to determine runtime
function, err := s.functionRepo.GetByID(ctx, execution.FunctionID)
if err != nil {
return nil, fmt.Errorf("function not found: %w", err)
}
// Get runtime backend
backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
if err != nil {
return nil, fmt.Errorf("failed to get runtime backend: %w", err)
}
// Get logs from runtime
logs, err := backend.GetLogs(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get logs: %w", err)
}
return logs, nil
}
func (s *executionService) GetRunningExecutions(ctx context.Context) ([]*domain.FunctionExecution, error) {
return s.executionRepo.GetRunningExecutions(ctx)
}

View File

@ -0,0 +1,253 @@
package services
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/repository"
"github.com/google/uuid"
)
type functionService struct {
functionRepo repository.FunctionRepository
runtimeService RuntimeService
logger *zap.Logger
}
func NewFunctionService(functionRepo repository.FunctionRepository, runtimeService RuntimeService, logger *zap.Logger) FunctionService {
return &functionService{
functionRepo: functionRepo,
runtimeService: runtimeService,
logger: logger,
}
}
func (s *functionService) Create(ctx context.Context, req *domain.CreateFunctionRequest, userID string) (*domain.FunctionDefinition, error) {
s.logger.Info("Creating new function",
zap.String("name", req.Name),
zap.String("app_id", req.AppID),
zap.String("user_id", userID))
// Check if function with same name exists
_, err := s.functionRepo.GetByName(ctx, req.AppID, req.Name)
if err == nil {
return nil, fmt.Errorf("function with name '%s' already exists in app '%s'", req.Name, req.AppID)
}
// Validate runtime
if !s.isValidRuntime(string(req.Runtime)) {
return nil, fmt.Errorf("unsupported runtime: %s", req.Runtime)
}
// Create function definition
function := &domain.FunctionDefinition{
ID: uuid.New(),
Name: req.Name,
AppID: req.AppID,
Runtime: req.Runtime,
Image: req.Image,
Handler: req.Handler,
Code: req.Code,
Environment: req.Environment,
Timeout: req.Timeout,
Memory: req.Memory,
Owner: req.Owner,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Validate timeout and memory limits
if function.Timeout.Duration < time.Second {
return nil, fmt.Errorf("timeout must be at least 1 second")
}
if function.Timeout.Duration > 15*time.Minute {
return nil, fmt.Errorf("timeout cannot exceed 15 minutes")
}
if function.Memory < 64 || function.Memory > 3008 {
return nil, fmt.Errorf("memory must be between 64 and 3008 MB")
}
// Store function
created, err := s.functionRepo.Create(ctx, function)
if err != nil {
s.logger.Error("Failed to create function",
zap.String("name", req.Name),
zap.Error(err))
return nil, fmt.Errorf("failed to create function: %w", err)
}
s.logger.Info("Function created successfully",
zap.String("function_id", created.ID.String()),
zap.String("name", created.Name))
return created, nil
}
func (s *functionService) GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionDefinition, error) {
function, err := s.functionRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("function not found: %w", err)
}
return function, nil
}
func (s *functionService) GetByName(ctx context.Context, appID, name string) (*domain.FunctionDefinition, error) {
function, err := s.functionRepo.GetByName(ctx, appID, name)
if err != nil {
return nil, fmt.Errorf("function not found: %w", err)
}
return function, nil
}
func (s *functionService) Update(ctx context.Context, id uuid.UUID, req *domain.UpdateFunctionRequest, userID string) (*domain.FunctionDefinition, error) {
s.logger.Info("Updating function",
zap.String("function_id", id.String()),
zap.String("user_id", userID))
// Get existing function
_, err := s.functionRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("function not found: %w", err)
}
// Validate runtime if being updated
if req.Runtime != nil && !s.isValidRuntime(string(*req.Runtime)) {
return nil, fmt.Errorf("unsupported runtime: %s", *req.Runtime)
}
// Validate timeout and memory if being updated
if req.Timeout != nil {
if req.Timeout.Duration < time.Second {
return nil, fmt.Errorf("timeout must be at least 1 second")
}
if req.Timeout.Duration > 15*time.Minute {
return nil, fmt.Errorf("timeout cannot exceed 15 minutes")
}
}
if req.Memory != nil && (*req.Memory < 64 || *req.Memory > 3008) {
return nil, fmt.Errorf("memory must be between 64 and 3008 MB")
}
// Update function
updated, err := s.functionRepo.Update(ctx, id, req)
if err != nil {
s.logger.Error("Failed to update function",
zap.String("function_id", id.String()),
zap.Error(err))
return nil, fmt.Errorf("failed to update function: %w", err)
}
s.logger.Info("Function updated successfully",
zap.String("function_id", id.String()))
return updated, nil
}
func (s *functionService) Delete(ctx context.Context, id uuid.UUID, userID string) error {
s.logger.Info("Deleting function",
zap.String("function_id", id.String()),
zap.String("user_id", userID))
// Get function to determine runtime
function, err := s.functionRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("function not found: %w", err)
}
// Clean up runtime resources
backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
if err != nil {
s.logger.Warn("Failed to get runtime backend for cleanup", zap.Error(err))
} else {
if err := backend.Remove(ctx, id); err != nil {
s.logger.Warn("Failed to remove runtime resources",
zap.String("function_id", id.String()),
zap.Error(err))
}
}
// Delete function
if err := s.functionRepo.Delete(ctx, id); err != nil {
s.logger.Error("Failed to delete function",
zap.String("function_id", id.String()),
zap.Error(err))
return fmt.Errorf("failed to delete function: %w", err)
}
s.logger.Info("Function deleted successfully",
zap.String("function_id", id.String()))
return nil
}
func (s *functionService) List(ctx context.Context, appID string, limit, offset int) ([]*domain.FunctionDefinition, error) {
if limit <= 0 {
limit = 50 // Default limit
}
if limit > 100 {
limit = 100 // Max limit
}
return s.functionRepo.List(ctx, appID, limit, offset)
}
func (s *functionService) GetByAppID(ctx context.Context, appID string) ([]*domain.FunctionDefinition, error) {
return s.functionRepo.GetByAppID(ctx, appID)
}
func (s *functionService) Deploy(ctx context.Context, id uuid.UUID, req *domain.DeployFunctionRequest, userID string) (*domain.DeployFunctionResponse, error) {
s.logger.Info("Deploying function",
zap.String("function_id", id.String()),
zap.String("user_id", userID),
zap.Bool("force", req.Force))
// Get function
function, err := s.functionRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("function not found: %w", err)
}
// Get runtime backend
backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime))
if err != nil {
return nil, fmt.Errorf("failed to get runtime backend: %w", err)
}
// Deploy function
if err := backend.Deploy(ctx, function); err != nil {
s.logger.Error("Failed to deploy function",
zap.String("function_id", id.String()),
zap.Error(err))
return nil, fmt.Errorf("failed to deploy function: %w", err)
}
s.logger.Info("Function deployed successfully",
zap.String("function_id", id.String()),
zap.String("image", function.Image))
return &domain.DeployFunctionResponse{
Status: "deployed",
Message: "Function deployed successfully",
Image: function.Image,
}, nil
}
func (s *functionService) isValidRuntime(runtimeType string) bool {
validRuntimes := []string{
string(domain.RuntimeNodeJS18),
string(domain.RuntimePython39),
string(domain.RuntimeGo120),
string(domain.RuntimeCustom),
}
for _, valid := range validRuntimes {
if runtimeType == valid {
return true
}
}
return false
}

View File

@ -0,0 +1,48 @@
package services
import (
"context"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/runtime"
"github.com/google/uuid"
)
// FunctionService provides business logic for function management
type FunctionService interface {
Create(ctx context.Context, req *domain.CreateFunctionRequest, userID string) (*domain.FunctionDefinition, error)
GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionDefinition, error)
GetByName(ctx context.Context, appID, name string) (*domain.FunctionDefinition, error)
Update(ctx context.Context, id uuid.UUID, req *domain.UpdateFunctionRequest, userID string) (*domain.FunctionDefinition, error)
Delete(ctx context.Context, id uuid.UUID, userID string) error
List(ctx context.Context, appID string, limit, offset int) ([]*domain.FunctionDefinition, error)
GetByAppID(ctx context.Context, appID string) ([]*domain.FunctionDefinition, error)
Deploy(ctx context.Context, id uuid.UUID, req *domain.DeployFunctionRequest, userID string) (*domain.DeployFunctionResponse, error)
}
// ExecutionService provides business logic for function execution
type ExecutionService interface {
Execute(ctx context.Context, req *domain.ExecuteFunctionRequest, userID string) (*domain.ExecuteFunctionResponse, error)
GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionExecution, error)
List(ctx context.Context, functionID *uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error)
GetByFunctionID(ctx context.Context, functionID uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error)
Cancel(ctx context.Context, id uuid.UUID, userID string) error
GetLogs(ctx context.Context, id uuid.UUID) ([]string, error)
GetRunningExecutions(ctx context.Context) ([]*domain.FunctionExecution, error)
}
// RuntimeService provides runtime management capabilities
type RuntimeService interface {
GetBackend(ctx context.Context, runtimeType string) (runtime.RuntimeBackend, error)
ListSupportedRuntimes(ctx context.Context) ([]*domain.RuntimeInfo, error)
HealthCheck(ctx context.Context, runtimeType string) error
GetRuntimeInfo(ctx context.Context, runtimeType string) (*runtime.RuntimeInfo, error)
ListContainers(ctx context.Context, runtimeType string) ([]runtime.ContainerInfo, error)
}
// AuthService provides authentication and authorization
type AuthService interface {
GetAuthContext(ctx context.Context) (*domain.AuthContext, error)
HasPermission(ctx context.Context, permission string) bool
ValidatePermissions(ctx context.Context, permissions []string) error
}

View File

@ -0,0 +1,194 @@
package services
import (
"context"
"fmt"
"sync"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/faas/internal/domain"
"github.com/RyanCopley/skybridge/faas/internal/runtime"
"github.com/RyanCopley/skybridge/faas/internal/runtime/docker"
)
type runtimeService struct {
backends map[string]runtime.RuntimeBackend
mutex sync.RWMutex
logger *zap.Logger
config *RuntimeConfig
}
type RuntimeConfig struct {
DefaultRuntime string `json:"default_runtime"`
Backends map[string]map[string]interface{} `json:"backends"`
}
func NewRuntimeService(logger *zap.Logger, config *RuntimeConfig) RuntimeService {
if config == nil {
config = &RuntimeConfig{
DefaultRuntime: "docker",
Backends: make(map[string]map[string]interface{}),
}
}
service := &runtimeService{
backends: make(map[string]runtime.RuntimeBackend),
logger: logger,
config: config,
}
// Initialize default Docker backend
if err := service.initializeDockerBackend(); err != nil {
logger.Warn("Failed to initialize Docker backend", zap.Error(err))
}
return service
}
func (s *runtimeService) initializeDockerBackend() error {
// Use simple Docker backend for now
dockerBackend := docker.NewSimpleDockerRuntime(s.logger)
s.mutex.Lock()
s.backends["docker"] = dockerBackend
s.mutex.Unlock()
s.logger.Info("Simple Docker runtime backend initialized")
return nil
}
func (s *runtimeService) GetBackend(ctx context.Context, runtimeType string) (runtime.RuntimeBackend, error) {
// Map domain runtime types to backend types
backendType := s.mapRuntimeToBackend(runtimeType)
s.mutex.RLock()
backend, exists := s.backends[backendType]
s.mutex.RUnlock()
if !exists {
return nil, fmt.Errorf("runtime backend '%s' not available", backendType)
}
// Check backend health
if err := backend.HealthCheck(ctx); err != nil {
s.logger.Warn("Runtime backend health check failed",
zap.String("backend", backendType),
zap.Error(err))
return nil, fmt.Errorf("runtime backend '%s' is not healthy: %w", backendType, err)
}
return backend, nil
}
func (s *runtimeService) ListSupportedRuntimes(ctx context.Context) ([]*domain.RuntimeInfo, error) {
runtimes := []*domain.RuntimeInfo{
{
Type: domain.RuntimeNodeJS18,
Version: "18.x",
Available: s.isRuntimeAvailable(ctx, "nodejs18"),
DefaultImage: "node:18-alpine",
Description: "Node.js 18.x runtime with Alpine Linux",
},
{
Type: domain.RuntimePython39,
Version: "3.9.x",
Available: s.isRuntimeAvailable(ctx, "python3.9"),
DefaultImage: "python:3.9-alpine",
Description: "Python 3.9.x runtime with Alpine Linux",
},
{
Type: domain.RuntimeGo120,
Version: "1.20.x",
Available: s.isRuntimeAvailable(ctx, "go1.20"),
DefaultImage: "golang:1.20-alpine",
Description: "Go 1.20.x runtime with Alpine Linux",
},
{
Type: domain.RuntimeCustom,
Version: "custom",
Available: s.isRuntimeAvailable(ctx, "custom"),
DefaultImage: "alpine:latest",
Description: "Custom runtime with user-defined image",
},
}
return runtimes, nil
}
func (s *runtimeService) HealthCheck(ctx context.Context, runtimeType string) error {
backendType := s.mapRuntimeToBackend(runtimeType)
s.mutex.RLock()
backend, exists := s.backends[backendType]
s.mutex.RUnlock()
if !exists {
return fmt.Errorf("runtime backend '%s' not available", backendType)
}
return backend.HealthCheck(ctx)
}
func (s *runtimeService) GetRuntimeInfo(ctx context.Context, runtimeType string) (*runtime.RuntimeInfo, error) {
backendType := s.mapRuntimeToBackend(runtimeType)
s.mutex.RLock()
backend, exists := s.backends[backendType]
s.mutex.RUnlock()
if !exists {
return nil, fmt.Errorf("runtime backend '%s' not available", backendType)
}
return backend.GetInfo(ctx)
}
func (s *runtimeService) ListContainers(ctx context.Context, runtimeType string) ([]runtime.ContainerInfo, error) {
backendType := s.mapRuntimeToBackend(runtimeType)
s.mutex.RLock()
backend, exists := s.backends[backendType]
s.mutex.RUnlock()
if !exists {
return nil, fmt.Errorf("runtime backend '%s' not available", backendType)
}
return backend.ListContainers(ctx)
}
func (s *runtimeService) mapRuntimeToBackend(runtimeType string) string {
// For now, all runtimes use Docker backend
// In the future, we could support different backends for different runtimes
switch runtimeType {
case string(domain.RuntimeNodeJS18):
return "docker"
case string(domain.RuntimePython39):
return "docker"
case string(domain.RuntimeGo120):
return "docker"
case string(domain.RuntimeCustom):
return "docker"
default:
return s.config.DefaultRuntime
}
}
func (s *runtimeService) isRuntimeAvailable(ctx context.Context, runtimeType string) bool {
backendType := s.mapRuntimeToBackend(runtimeType)
s.mutex.RLock()
backend, exists := s.backends[backendType]
s.mutex.RUnlock()
if !exists {
return false
}
if err := backend.HealthCheck(ctx); err != nil {
return false
}
return true
}