-
This commit is contained in:
192
faas/internal/config/config.go
Normal file
192
faas/internal/config/config.go
Normal 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
|
||||
}
|
||||
60
faas/internal/database/postgres.go
Normal file
60
faas/internal/database/postgres.go
Normal 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
|
||||
}
|
||||
116
faas/internal/domain/duration.go
Normal file
116
faas/internal/domain/duration.go
Normal 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()
|
||||
}
|
||||
163
faas/internal/domain/models.go
Normal file
163
faas/internal/domain/models.go
Normal 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"`
|
||||
}
|
||||
70
faas/internal/domain/runtime_config.go
Normal file
70
faas/internal/domain/runtime_config.go
Normal 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
|
||||
}
|
||||
261
faas/internal/handlers/execution.go
Normal file
261
faas/internal/handlers/execution.go
Normal 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),
|
||||
})
|
||||
}
|
||||
244
faas/internal/handlers/function.go
Normal file
244
faas/internal/handlers/function.go
Normal 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)
|
||||
}
|
||||
70
faas/internal/handlers/health.go
Normal file
70
faas/internal/handlers/health.go
Normal 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)
|
||||
}
|
||||
32
faas/internal/repository/interfaces.go
Normal file
32
faas/internal/repository/interfaces.go
Normal 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)
|
||||
}
|
||||
260
faas/internal/repository/postgres/execution_repository.go
Normal file
260
faas/internal/repository/postgres/execution_repository.go
Normal 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)
|
||||
}
|
||||
278
faas/internal/repository/postgres/function_repository.go
Normal file
278
faas/internal/repository/postgres/function_repository.go
Normal 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(¤t.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
|
||||
}
|
||||
431
faas/internal/runtime/docker/docker.go.bak
Normal file
431
faas/internal/runtime/docker/docker.go.bak
Normal 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)
|
||||
}
|
||||
84
faas/internal/runtime/docker/simple.go
Normal file
84
faas/internal/runtime/docker/simple.go
Normal 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
|
||||
}
|
||||
62
faas/internal/runtime/interfaces.go
Normal file
62
faas/internal/runtime/interfaces.go
Normal 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{}
|
||||
}
|
||||
75
faas/internal/services/auth_service.go
Normal file
75
faas/internal/services/auth_service.go
Normal 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
|
||||
}
|
||||
309
faas/internal/services/execution_service.go
Normal file
309
faas/internal/services/execution_service.go
Normal 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)
|
||||
}
|
||||
253
faas/internal/services/function_service.go
Normal file
253
faas/internal/services/function_service.go
Normal 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
|
||||
}
|
||||
48
faas/internal/services/interfaces.go
Normal file
48
faas/internal/services/interfaces.go
Normal 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
|
||||
}
|
||||
194
faas/internal/services/runtime_service.go
Normal file
194
faas/internal/services/runtime_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user