-
This commit is contained in:
208
docker-compose.yml
Normal file
208
docker-compose.yml
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database for KMS
|
||||||
|
kms-postgres:
|
||||||
|
image: docker.io/library/postgres:15-alpine
|
||||||
|
container_name: kms-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: kms
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- kms_postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./kms/migrations:/docker-entrypoint-initdb.d:Z
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d kms"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# PostgreSQL Database for FaaS
|
||||||
|
faas-postgres:
|
||||||
|
image: docker.io/library/postgres:15-alpine
|
||||||
|
container_name: faas-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: faas
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- faas_postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./faas/migrations:/docker-entrypoint-initdb.d:Z
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d faas"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# KMS API Service
|
||||||
|
kms-api-service:
|
||||||
|
build:
|
||||||
|
context: ./kms
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: kms-api-service
|
||||||
|
environment:
|
||||||
|
APP_ENV: development
|
||||||
|
DB_HOST: kms-postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: kms
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: postgres
|
||||||
|
DB_SSLMODE: disable
|
||||||
|
DB_CONN_MAX_LIFETIME: 5m
|
||||||
|
DB_MAX_OPEN_CONNS: 25
|
||||||
|
DB_MAX_IDLE_CONNS: 5
|
||||||
|
SERVER_HOST: 0.0.0.0
|
||||||
|
SERVER_PORT: 8080
|
||||||
|
LOG_LEVEL: debug
|
||||||
|
MIGRATION_PATH: /app/migrations
|
||||||
|
INTERNAL_HMAC_KEY: 3924f352b7ea63b27db02bf4b0014f2961a5d2f7c27643853a4581bb3a5457cb
|
||||||
|
JWT_SECRET: 7f5e11d55e957988b00ce002418680af384219ef98c50d08cbbbdd541978450c
|
||||||
|
AUTH_SIGNING_KEY: 484f921b39c383e6b3e0cc5a7cef3c2cec3d7c8d474ab5102891dc4c2bf63a68
|
||||||
|
AUTH_PROVIDER: header
|
||||||
|
AUTH_HEADER_USER_EMAIL: X-User-Email
|
||||||
|
RATE_LIMIT_ENABLED: true
|
||||||
|
CACHE_ENABLED: false
|
||||||
|
METRICS_ENABLED: true
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
- "9090:9090" # Metrics port
|
||||||
|
depends_on:
|
||||||
|
kms-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
volumes:
|
||||||
|
- ./kms/migrations:/app/migrations:ro,Z
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# FaaS API Service
|
||||||
|
faas-api-service:
|
||||||
|
build:
|
||||||
|
context: ./faas
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: faas-api-service
|
||||||
|
environment:
|
||||||
|
FAAS_APP_ENV: development
|
||||||
|
FAAS_DB_HOST: faas-postgres
|
||||||
|
FAAS_DB_PORT: 5432
|
||||||
|
FAAS_DB_NAME: faas
|
||||||
|
FAAS_DB_USER: postgres
|
||||||
|
FAAS_DB_PASSWORD: postgres
|
||||||
|
FAAS_DB_SSLMODE: disable
|
||||||
|
DB_CONN_MAX_LIFETIME: 5m
|
||||||
|
DB_MAX_OPEN_CONNS: 25
|
||||||
|
DB_MAX_IDLE_CONNS: 5
|
||||||
|
FAAS_SERVER_HOST: 0.0.0.0
|
||||||
|
FAAS_SERVER_PORT: 8082
|
||||||
|
FAAS_LOG_LEVEL: debug
|
||||||
|
FAAS_DEFAULT_RUNTIME: docker
|
||||||
|
FAAS_FUNCTION_TIMEOUT: 300s
|
||||||
|
FAAS_MAX_MEMORY: 3008
|
||||||
|
FAAS_MAX_CONCURRENT: 100
|
||||||
|
FAAS_SANDBOX_ENABLED: true
|
||||||
|
FAAS_NETWORK_ISOLATION: true
|
||||||
|
FAAS_RESOURCE_LIMITS: true
|
||||||
|
AUTH_PROVIDER: header
|
||||||
|
AUTH_HEADER_USER_EMAIL: X-User-Email
|
||||||
|
RATE_LIMIT_ENABLED: true
|
||||||
|
METRICS_ENABLED: true
|
||||||
|
METRICS_PORT: 9091
|
||||||
|
ports:
|
||||||
|
- "8082:8082"
|
||||||
|
- "9091:9091" # Metrics port
|
||||||
|
depends_on:
|
||||||
|
faas-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro # For Docker runtime
|
||||||
|
- ./faas/migrations:/app/migrations:ro,Z
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Shell Dashboard
|
||||||
|
shell-dashboard:
|
||||||
|
build:
|
||||||
|
context: ./web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: shell-dashboard
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Demo Module
|
||||||
|
demo-module:
|
||||||
|
build:
|
||||||
|
context: ./demo
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: demo-module
|
||||||
|
ports:
|
||||||
|
- "3001:80"
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# KMS Frontend
|
||||||
|
kms-frontend:
|
||||||
|
build:
|
||||||
|
context: ./kms/web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: kms-frontend
|
||||||
|
ports:
|
||||||
|
- "3002:80"
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# FaaS Frontend
|
||||||
|
faas-frontend:
|
||||||
|
build:
|
||||||
|
context: ./faas/web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: faas-frontend
|
||||||
|
ports:
|
||||||
|
- "3003:80"
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Nginx Reverse Proxy
|
||||||
|
nginx:
|
||||||
|
image: docker.io/library/nginx:alpine
|
||||||
|
container_name: skybridge-nginx
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro,Z
|
||||||
|
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro,Z
|
||||||
|
depends_on:
|
||||||
|
- kms-api-service
|
||||||
|
- faas-api-service
|
||||||
|
- shell-dashboard
|
||||||
|
- demo-module
|
||||||
|
- kms-frontend
|
||||||
|
- faas-frontend
|
||||||
|
networks:
|
||||||
|
- skybridge-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
kms_postgres_data:
|
||||||
|
driver: local
|
||||||
|
faas_postgres_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
skybridge-network:
|
||||||
|
driver: bridge
|
||||||
38
faas/Dockerfile
Normal file
38
faas/Dockerfile
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# Copy go.mod and go.sum
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o faas-server ./cmd/server
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the binary
|
||||||
|
COPY --from=builder /app/faas-server .
|
||||||
|
|
||||||
|
# Copy migrations
|
||||||
|
COPY --from=builder /app/migrations ./migrations
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8082 9091
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["./faas-server"]
|
||||||
261
faas/cmd/server/main.go
Normal file
261
faas/cmd/server/main.go
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/RyanCopley/skybridge/faas/internal/config"
|
||||||
|
"github.com/RyanCopley/skybridge/faas/internal/database"
|
||||||
|
"github.com/RyanCopley/skybridge/faas/internal/domain"
|
||||||
|
"github.com/RyanCopley/skybridge/faas/internal/handlers"
|
||||||
|
"github.com/RyanCopley/skybridge/faas/internal/repository/postgres"
|
||||||
|
"github.com/RyanCopley/skybridge/faas/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize configuration
|
||||||
|
cfg := config.NewConfig()
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
log.Fatal("Configuration validation failed:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
logger := initLogger(cfg)
|
||||||
|
defer logger.Sync()
|
||||||
|
|
||||||
|
logger.Info("Starting Function-as-a-Service",
|
||||||
|
zap.String("version", "1.0.0"),
|
||||||
|
zap.String("environment", cfg.GetString("FAAS_APP_ENV")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
logger.Info("Connecting to database",
|
||||||
|
zap.String("dsn", cfg.GetDatabaseDSNForLogging()))
|
||||||
|
|
||||||
|
db, err := database.NewPostgresProvider(
|
||||||
|
cfg.GetDatabaseDSN(),
|
||||||
|
cfg.GetInt("DB_MAX_OPEN_CONNS"),
|
||||||
|
cfg.GetInt("DB_MAX_IDLE_CONNS"),
|
||||||
|
cfg.GetString("DB_CONN_MAX_LIFETIME"),
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("Failed to initialize database",
|
||||||
|
zap.String("dsn", cfg.GetDatabaseDSNForLogging()),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Database connection established successfully")
|
||||||
|
|
||||||
|
// Initialize repositories
|
||||||
|
functionRepo := postgres.NewFunctionRepository(db, logger)
|
||||||
|
executionRepo := postgres.NewExecutionRepository(db, logger)
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
runtimeService := services.NewRuntimeService(logger, nil)
|
||||||
|
functionService := services.NewFunctionService(functionRepo, runtimeService, logger)
|
||||||
|
executionService := services.NewExecutionService(executionRepo, functionRepo, runtimeService, logger)
|
||||||
|
authService := services.NewAuthService(logger) // Mock auth service for now
|
||||||
|
|
||||||
|
// Initialize handlers
|
||||||
|
healthHandler := handlers.NewHealthHandler(db, logger)
|
||||||
|
functionHandler := handlers.NewFunctionHandler(functionService, authService, logger)
|
||||||
|
executionHandler := handlers.NewExecutionHandler(executionService, authService, logger)
|
||||||
|
|
||||||
|
// Set up router
|
||||||
|
router := setupRouter(cfg, logger, healthHandler, functionHandler, executionHandler)
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: cfg.GetServerAddress(),
|
||||||
|
Handler: router,
|
||||||
|
ReadTimeout: cfg.GetDuration("SERVER_READ_TIMEOUT"),
|
||||||
|
WriteTimeout: cfg.GetDuration("SERVER_WRITE_TIMEOUT"),
|
||||||
|
IdleTimeout: cfg.GetDuration("SERVER_IDLE_TIMEOUT"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server in goroutine
|
||||||
|
go func() {
|
||||||
|
logger.Info("Starting HTTP server", zap.String("address", srv.Addr))
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Fatal("Failed to start server", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start metrics server if enabled
|
||||||
|
var metricsSrv *http.Server
|
||||||
|
if cfg.GetBool("METRICS_ENABLED") {
|
||||||
|
metricsSrv = startMetricsServer(cfg, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for interrupt signal to gracefully shutdown the server
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
logger.Info("Shutting down server...")
|
||||||
|
|
||||||
|
// Give outstanding requests time to complete
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Shutdown main server
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
logger.Error("Server forced to shutdown", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown metrics server
|
||||||
|
if metricsSrv != nil {
|
||||||
|
if err := metricsSrv.Shutdown(ctx); err != nil {
|
||||||
|
logger.Error("Metrics server forced to shutdown", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Server exited")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLogger(cfg config.ConfigProvider) *zap.Logger {
|
||||||
|
var logger *zap.Logger
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if cfg.IsProduction() {
|
||||||
|
logger, err = zap.NewProduction()
|
||||||
|
} else {
|
||||||
|
logger, err = zap.NewDevelopment()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to initialize logger:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *handlers.HealthHandler, functionHandler *handlers.FunctionHandler, executionHandler *handlers.ExecutionHandler) *gin.Engine {
|
||||||
|
// Set Gin mode based on environment
|
||||||
|
if cfg.IsProduction() {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
|
||||||
|
// Add middleware
|
||||||
|
router.Use(gin.Logger())
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
|
// CORS middleware
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-User-Email")
|
||||||
|
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Health check endpoints (no authentication required)
|
||||||
|
router.GET("/health", healthHandler.Health)
|
||||||
|
router.GET("/ready", healthHandler.Ready)
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
api := router.Group("/api")
|
||||||
|
{
|
||||||
|
// Function Management
|
||||||
|
api.GET("/functions", functionHandler.List)
|
||||||
|
api.POST("/functions", functionHandler.Create)
|
||||||
|
api.GET("/functions/:id", functionHandler.GetByID)
|
||||||
|
api.PUT("/functions/:id", functionHandler.Update)
|
||||||
|
api.DELETE("/functions/:id", functionHandler.Delete)
|
||||||
|
api.POST("/functions/:id/deploy", functionHandler.Deploy)
|
||||||
|
|
||||||
|
// Function Execution
|
||||||
|
api.POST("/functions/:id/execute", executionHandler.Execute)
|
||||||
|
api.POST("/functions/:id/invoke", executionHandler.Invoke)
|
||||||
|
|
||||||
|
// Execution Management
|
||||||
|
api.GET("/executions", executionHandler.List)
|
||||||
|
api.GET("/executions/:id", executionHandler.GetByID)
|
||||||
|
api.DELETE("/executions/:id", executionHandler.Cancel)
|
||||||
|
api.GET("/executions/:id/logs", executionHandler.GetLogs)
|
||||||
|
api.GET("/executions/running", executionHandler.GetRunning)
|
||||||
|
|
||||||
|
// Runtime information endpoint
|
||||||
|
api.GET("/runtimes", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"runtimes": domain.GetAvailableRuntimes(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Documentation endpoint
|
||||||
|
api.GET("/docs", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"service": "Function-as-a-Service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"documentation": "FaaS API Documentation",
|
||||||
|
"endpoints": map[string]interface{}{
|
||||||
|
"functions": []string{
|
||||||
|
"GET /api/functions",
|
||||||
|
"POST /api/functions",
|
||||||
|
"GET /api/functions/:id",
|
||||||
|
"PUT /api/functions/:id",
|
||||||
|
"DELETE /api/functions/:id",
|
||||||
|
"POST /api/functions/:id/deploy",
|
||||||
|
},
|
||||||
|
"executions": []string{
|
||||||
|
"POST /api/functions/:id/execute",
|
||||||
|
"POST /api/functions/:id/invoke",
|
||||||
|
"GET /api/executions",
|
||||||
|
"GET /api/executions/:id",
|
||||||
|
"DELETE /api/executions/:id",
|
||||||
|
"GET /api/executions/:id/logs",
|
||||||
|
"GET /api/executions/running",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMetricsServer(cfg config.ConfigProvider, logger *zap.Logger) *http.Server {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Health endpoint for metrics server
|
||||||
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Metrics endpoint would go here
|
||||||
|
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("# FaaS metrics placeholder\n"))
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: cfg.GetString("FAAS_SERVER_HOST") + ":" + cfg.GetString("METRICS_PORT"),
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logger.Info("Starting metrics server", zap.String("address", srv.Addr))
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Error("Failed to start metrics server", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return srv
|
||||||
|
}
|
||||||
85
faas/docker-compose.yml
Normal file
85
faas/docker-compose.yml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
faas-postgres:
|
||||||
|
image: docker.io/library/postgres:15-alpine
|
||||||
|
container_name: faas-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: faas
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- "5434:5432"
|
||||||
|
volumes:
|
||||||
|
- faas_postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./migrations:/docker-entrypoint-initdb.d:Z
|
||||||
|
networks:
|
||||||
|
- faas-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d faas"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
faas-api-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: faas-api-service
|
||||||
|
environment:
|
||||||
|
FAAS_APP_ENV: development
|
||||||
|
FAAS_DB_HOST: faas-postgres
|
||||||
|
FAAS_DB_PORT: 5432
|
||||||
|
FAAS_DB_NAME: faas
|
||||||
|
FAAS_DB_USER: postgres
|
||||||
|
FAAS_DB_PASSWORD: postgres
|
||||||
|
FAAS_DB_SSLMODE: disable
|
||||||
|
DB_CONN_MAX_LIFETIME: 5m
|
||||||
|
DB_MAX_OPEN_CONNS: 25
|
||||||
|
DB_MAX_IDLE_CONNS: 5
|
||||||
|
FAAS_SERVER_HOST: 0.0.0.0
|
||||||
|
FAAS_SERVER_PORT: 8083
|
||||||
|
FAAS_LOG_LEVEL: debug
|
||||||
|
FAAS_DEFAULT_RUNTIME: docker
|
||||||
|
FAAS_FUNCTION_TIMEOUT: 300s
|
||||||
|
FAAS_MAX_MEMORY: 3008
|
||||||
|
FAAS_MAX_CONCURRENT: 100
|
||||||
|
FAAS_SANDBOX_ENABLED: true
|
||||||
|
FAAS_NETWORK_ISOLATION: true
|
||||||
|
FAAS_RESOURCE_LIMITS: true
|
||||||
|
AUTH_PROVIDER: header
|
||||||
|
AUTH_HEADER_USER_EMAIL: X-User-Email
|
||||||
|
RATE_LIMIT_ENABLED: true
|
||||||
|
METRICS_ENABLED: true
|
||||||
|
METRICS_PORT: 9091
|
||||||
|
ports:
|
||||||
|
- "8083:8083"
|
||||||
|
- "9091:9091" # Metrics port
|
||||||
|
depends_on:
|
||||||
|
faas-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- faas-network
|
||||||
|
volumes:
|
||||||
|
- /run/user/1000/podman/podman.sock:/var/run/docker.sock:ro # For Podman runtime
|
||||||
|
- ./migrations:/app/migrations:ro,Z
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# faas-frontend:
|
||||||
|
# build:
|
||||||
|
# context: ./web
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# container_name: faas-frontend
|
||||||
|
# ports:
|
||||||
|
# - "3003:80"
|
||||||
|
# networks:
|
||||||
|
# - faas-network
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
faas_postgres_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
faas-network:
|
||||||
|
driver: bridge
|
||||||
BIN
faas/faas-server
Executable file
BIN
faas/faas-server
Executable file
Binary file not shown.
68
faas/go.mod
Normal file
68
faas/go.mod
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
module github.com/RyanCopley/skybridge/faas
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/docker/docker v28.3.3+incompatible
|
||||||
|
github.com/docker/go-connections v0.4.0
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
|
go.uber.org/zap v1.26.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.16.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
|
go.uber.org/goleak v1.3.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
|
)
|
||||||
208
faas/go.sum
Normal file
208
faas/go.sum
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||||
|
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||||
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
|
||||||
|
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||||
|
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||||
|
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
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
|
||||||
|
}
|
||||||
23
faas/migrations/001_initial_schema.down.sql
Normal file
23
faas/migrations/001_initial_schema.down.sql
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
-- Drop view
|
||||||
|
DROP VIEW IF EXISTS function_stats;
|
||||||
|
|
||||||
|
-- Drop triggers
|
||||||
|
DROP TRIGGER IF EXISTS update_functions_updated_at ON functions;
|
||||||
|
DROP FUNCTION IF EXISTS update_updated_at_column();
|
||||||
|
|
||||||
|
-- Drop indexes
|
||||||
|
DROP INDEX IF EXISTS idx_executions_created_at;
|
||||||
|
DROP INDEX IF EXISTS idx_executions_executor_id;
|
||||||
|
DROP INDEX IF EXISTS idx_executions_status;
|
||||||
|
DROP INDEX IF EXISTS idx_executions_function_id;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_functions_created_at;
|
||||||
|
DROP INDEX IF EXISTS idx_functions_runtime;
|
||||||
|
DROP INDEX IF EXISTS idx_functions_app_id;
|
||||||
|
|
||||||
|
-- Drop tables
|
||||||
|
DROP TABLE IF EXISTS executions;
|
||||||
|
DROP TABLE IF EXISTS functions;
|
||||||
|
|
||||||
|
-- Drop extension (only if no other tables use it)
|
||||||
|
-- DROP EXTENSION IF EXISTS "uuid-ossp";
|
||||||
84
faas/migrations/001_initial_schema.up.sql
Normal file
84
faas/migrations/001_initial_schema.up.sql
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
-- Create UUID extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Create functions table
|
||||||
|
CREATE TABLE functions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
app_id VARCHAR(255) NOT NULL,
|
||||||
|
runtime VARCHAR(50) NOT NULL,
|
||||||
|
image VARCHAR(500) NOT NULL,
|
||||||
|
handler VARCHAR(255) NOT NULL,
|
||||||
|
code TEXT,
|
||||||
|
environment JSONB DEFAULT '{}',
|
||||||
|
timeout INTERVAL NOT NULL DEFAULT '30 seconds',
|
||||||
|
memory INTEGER NOT NULL DEFAULT 128,
|
||||||
|
owner_type VARCHAR(50) NOT NULL,
|
||||||
|
owner_name VARCHAR(255) NOT NULL,
|
||||||
|
owner_owner VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE(app_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create executions table
|
||||||
|
CREATE TABLE executions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
function_id UUID NOT NULL REFERENCES functions(id) ON DELETE CASCADE,
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'pending',
|
||||||
|
input JSONB,
|
||||||
|
output JSONB,
|
||||||
|
error TEXT,
|
||||||
|
duration INTERVAL,
|
||||||
|
memory_used INTEGER,
|
||||||
|
container_id VARCHAR(255),
|
||||||
|
executor_id VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
started_at TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for better query performance
|
||||||
|
CREATE INDEX idx_functions_app_id ON functions(app_id);
|
||||||
|
CREATE INDEX idx_functions_runtime ON functions(runtime);
|
||||||
|
CREATE INDEX idx_functions_created_at ON functions(created_at);
|
||||||
|
|
||||||
|
CREATE INDEX idx_executions_function_id ON executions(function_id);
|
||||||
|
CREATE INDEX idx_executions_status ON executions(status);
|
||||||
|
CREATE INDEX idx_executions_executor_id ON executions(executor_id);
|
||||||
|
CREATE INDEX idx_executions_created_at ON executions(created_at);
|
||||||
|
|
||||||
|
-- Create trigger to update updated_at timestamp
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
CREATE TRIGGER update_functions_updated_at
|
||||||
|
BEFORE UPDATE ON functions
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Insert some default runtime configurations
|
||||||
|
COMMENT ON TABLE functions IS 'Function definitions and configurations';
|
||||||
|
COMMENT ON TABLE executions IS 'Function execution records and results';
|
||||||
|
|
||||||
|
-- Create a view for function statistics
|
||||||
|
CREATE OR REPLACE VIEW function_stats AS
|
||||||
|
SELECT
|
||||||
|
f.id,
|
||||||
|
f.name,
|
||||||
|
f.app_id,
|
||||||
|
f.runtime,
|
||||||
|
COUNT(e.id) as total_executions,
|
||||||
|
COUNT(CASE WHEN e.status = 'completed' THEN 1 END) as successful_executions,
|
||||||
|
COUNT(CASE WHEN e.status = 'failed' THEN 1 END) as failed_executions,
|
||||||
|
COUNT(CASE WHEN e.status = 'running' THEN 1 END) as running_executions,
|
||||||
|
AVG(EXTRACT(epoch FROM e.duration)) as avg_duration_seconds,
|
||||||
|
MAX(e.created_at) as last_execution_at
|
||||||
|
FROM functions f
|
||||||
|
LEFT JOIN executions e ON f.id = e.function_id
|
||||||
|
GROUP BY f.id, f.name, f.app_id, f.runtime;
|
||||||
31
faas/web/Dockerfile
Normal file
31
faas/web/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built files from builder stage
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
60
faas/web/nginx.conf
Normal file
60
faas/web/nginx.conf
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Enable gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
|
||||||
|
# CORS headers for Module Federation
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
|
||||||
|
|
||||||
|
# Handle preflight OPTIONS requests
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header Access-Control-Allow-Origin "*";
|
||||||
|
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
||||||
|
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
|
||||||
|
add_header Access-Control-Max-Age 1728000;
|
||||||
|
add_header Content-Type "text/plain; charset=utf-8";
|
||||||
|
add_header Content-Length 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main location
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle .js files with correct MIME type
|
||||||
|
location ~* \.js$ {
|
||||||
|
add_header Content-Type application/javascript;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
5981
faas/web/package-lock.json
generated
Normal file
5981
faas/web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
faas/web/package.json
Normal file
37
faas/web/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "faas-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^7.0.0",
|
||||||
|
"@mantine/hooks": "^7.0.0",
|
||||||
|
"@mantine/notifications": "^7.0.0",
|
||||||
|
"@mantine/form": "^7.0.0",
|
||||||
|
"@mantine/code-highlight": "^7.0.0",
|
||||||
|
"@tabler/icons-react": "^2.40.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.8.0",
|
||||||
|
"axios": "^1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.22.0",
|
||||||
|
"@babel/preset-react": "^7.22.0",
|
||||||
|
"@babel/preset-typescript": "^7.22.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"babel-loader": "^9.1.0",
|
||||||
|
"css-loader": "^6.8.0",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"style-loader": "^3.3.0",
|
||||||
|
"typescript": "^5.1.0",
|
||||||
|
"webpack": "^5.88.0",
|
||||||
|
"webpack-cli": "^5.1.0",
|
||||||
|
"webpack-dev-server": "^4.15.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "webpack serve --mode development",
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"dev": "npm start"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
faas/web/public/index.html
Normal file
11
faas/web/public/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>FaaS - Function as a Service</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
97
faas/web/src/App.tsx
Normal file
97
faas/web/src/App.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { MantineProvider, AppShell, Title, Group, Badge, Text } from '@mantine/core';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
|
import { IconFunction } from '@tabler/icons-react';
|
||||||
|
import { FunctionList } from './components/FunctionList';
|
||||||
|
import { FunctionForm } from './components/FunctionForm';
|
||||||
|
import { ExecutionModal } from './components/ExecutionModal';
|
||||||
|
import { FunctionDefinition } from './types';
|
||||||
|
|
||||||
|
// Default Mantine theme
|
||||||
|
const theme = {
|
||||||
|
colorScheme: 'light',
|
||||||
|
};
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
const [functionFormOpened, setFunctionFormOpened] = useState(false);
|
||||||
|
const [executionModalOpened, setExecutionModalOpened] = useState(false);
|
||||||
|
const [editingFunction, setEditingFunction] = useState<FunctionDefinition | null>(null);
|
||||||
|
const [executingFunction, setExecutingFunction] = useState<FunctionDefinition | null>(null);
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
|
||||||
|
const handleCreateFunction = () => {
|
||||||
|
setEditingFunction(null);
|
||||||
|
setFunctionFormOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditFunction = (func: FunctionDefinition) => {
|
||||||
|
setEditingFunction(func);
|
||||||
|
setFunctionFormOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecuteFunction = (func: FunctionDefinition) => {
|
||||||
|
setExecutingFunction(func);
|
||||||
|
setExecutionModalOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSuccess = () => {
|
||||||
|
setRefreshKey(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormClose = () => {
|
||||||
|
setFunctionFormOpened(false);
|
||||||
|
setEditingFunction(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecutionClose = () => {
|
||||||
|
setExecutionModalOpened(false);
|
||||||
|
setExecutingFunction(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider theme={theme}>
|
||||||
|
<Notifications />
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header>
|
||||||
|
<Group h="100%" px="md" justify="space-between">
|
||||||
|
<Group>
|
||||||
|
<IconFunction size={24} />
|
||||||
|
<Title order={3}>Function as a Service</Title>
|
||||||
|
<Badge variant="light" color="blue">FaaS</Badge>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Serverless Functions Platform
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Main>
|
||||||
|
<FunctionList
|
||||||
|
key={refreshKey}
|
||||||
|
onCreateFunction={handleCreateFunction}
|
||||||
|
onEditFunction={handleEditFunction}
|
||||||
|
onExecuteFunction={handleExecuteFunction}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FunctionForm
|
||||||
|
opened={functionFormOpened}
|
||||||
|
onClose={handleFormClose}
|
||||||
|
onSuccess={handleFormSuccess}
|
||||||
|
editFunction={editingFunction}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExecutionModal
|
||||||
|
opened={executionModalOpened}
|
||||||
|
onClose={handleExecutionClose}
|
||||||
|
function={executingFunction}
|
||||||
|
/>
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
323
faas/web/src/components/ExecutionModal.tsx
Normal file
323
faas/web/src/components/ExecutionModal.tsx
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
Switch,
|
||||||
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Divider,
|
||||||
|
Paper,
|
||||||
|
JsonInput,
|
||||||
|
Loader,
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconPlay, IconPlayerStop, IconRefresh, IconCopy } from '@tabler/icons-react';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { functionApi, executionApi } from '../services/api';
|
||||||
|
import { FunctionDefinition, ExecuteFunctionResponse, FunctionExecution } from '../types';
|
||||||
|
|
||||||
|
interface ExecutionModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
function: FunctionDefinition | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExecutionModal: React.FC<ExecutionModalProps> = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
function: func,
|
||||||
|
}) => {
|
||||||
|
const [input, setInput] = useState('{}');
|
||||||
|
const [async, setAsync] = useState(false);
|
||||||
|
const [executing, setExecuting] = useState(false);
|
||||||
|
const [result, setResult] = useState<ExecuteFunctionResponse | null>(null);
|
||||||
|
const [execution, setExecution] = useState<FunctionExecution | null>(null);
|
||||||
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
|
const [loadingLogs, setLoadingLogs] = useState(false);
|
||||||
|
|
||||||
|
if (!func) return null;
|
||||||
|
|
||||||
|
const handleExecute = async () => {
|
||||||
|
try {
|
||||||
|
setExecuting(true);
|
||||||
|
setResult(null);
|
||||||
|
setExecution(null);
|
||||||
|
setLogs([]);
|
||||||
|
|
||||||
|
let parsedInput;
|
||||||
|
try {
|
||||||
|
parsedInput = input.trim() ? JSON.parse(input) : undefined;
|
||||||
|
} catch (e) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Invalid JSON input',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await functionApi.execute(func.id, {
|
||||||
|
input: parsedInput,
|
||||||
|
async,
|
||||||
|
});
|
||||||
|
|
||||||
|
setResult(response.data);
|
||||||
|
|
||||||
|
if (async) {
|
||||||
|
// Poll for execution status
|
||||||
|
pollExecution(response.data.execution_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: `Function ${async ? 'invoked' : 'executed'} successfully`,
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Execution error:', error);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to execute function',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setExecuting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pollExecution = async (executionId: string) => {
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const response = await executionApi.getById(executionId);
|
||||||
|
setExecution(response.data);
|
||||||
|
|
||||||
|
if (response.data.status === 'running' || response.data.status === 'pending') {
|
||||||
|
setTimeout(poll, 2000); // Poll every 2 seconds
|
||||||
|
} else {
|
||||||
|
// Execution completed, get logs
|
||||||
|
loadLogs(executionId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error polling execution:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLogs = async (executionId: string) => {
|
||||||
|
try {
|
||||||
|
setLoadingLogs(true);
|
||||||
|
const response = await executionApi.getLogs(executionId);
|
||||||
|
setLogs(response.data.logs || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading logs:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingLogs(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
if (result && async) {
|
||||||
|
try {
|
||||||
|
await executionApi.cancel(result.execution_id);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Execution canceled',
|
||||||
|
color: 'orange',
|
||||||
|
});
|
||||||
|
// Refresh execution status
|
||||||
|
if (result.execution_id) {
|
||||||
|
const response = await executionApi.getById(result.execution_id);
|
||||||
|
setExecution(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to cancel execution',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'green';
|
||||||
|
case 'failed': return 'red';
|
||||||
|
case 'running': return 'blue';
|
||||||
|
case 'pending': return 'yellow';
|
||||||
|
case 'canceled': return 'orange';
|
||||||
|
case 'timeout': return 'red';
|
||||||
|
default: return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Copied',
|
||||||
|
message: 'Copied to clipboard',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
title={`Execute Function: ${func.name}`}
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Function Info */}
|
||||||
|
<Paper withBorder p="sm" bg="gray.0">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500}>{func.name}</Text>
|
||||||
|
<Text size="xs" c="dimmed">{func.runtime} • {func.memory}MB • {func.timeout}</Text>
|
||||||
|
</div>
|
||||||
|
<Badge variant="light">
|
||||||
|
{func.runtime}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<JsonInput
|
||||||
|
label="Function Input (JSON)"
|
||||||
|
placeholder='{"key": "value"}'
|
||||||
|
value={input}
|
||||||
|
onChange={setInput}
|
||||||
|
minRows={4}
|
||||||
|
maxRows={8}
|
||||||
|
validationError="Invalid JSON"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Execution Options */}
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Switch
|
||||||
|
label="Asynchronous execution"
|
||||||
|
description="Execute in background"
|
||||||
|
checked={async}
|
||||||
|
onChange={(event) => setAsync(event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlay size={16} />}
|
||||||
|
onClick={handleExecute}
|
||||||
|
loading={executing}
|
||||||
|
disabled={executing}
|
||||||
|
>
|
||||||
|
{async ? 'Invoke' : 'Execute'}
|
||||||
|
</Button>
|
||||||
|
{result && async && execution?.status === 'running' && (
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlayerStop size={16} />}
|
||||||
|
color="orange"
|
||||||
|
variant="light"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{result && (
|
||||||
|
<>
|
||||||
|
<Divider label="Execution Result" labelPosition="center" />
|
||||||
|
|
||||||
|
<Paper withBorder p="md">
|
||||||
|
<Group justify="space-between" mb="sm">
|
||||||
|
<Text fw={500}>Execution #{result.execution_id.slice(0, 8)}...</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Badge color={getStatusColor(execution?.status || result.status)}>
|
||||||
|
{execution?.status || result.status}
|
||||||
|
</Badge>
|
||||||
|
{result.duration && (
|
||||||
|
<Badge variant="light">
|
||||||
|
{result.duration}ms
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{result.memory_used && (
|
||||||
|
<Badge variant="light">
|
||||||
|
{result.memory_used}MB
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Output */}
|
||||||
|
{(result.output || execution?.output) && (
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>Output:</Text>
|
||||||
|
<Tooltip label="Copy output">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify(result.output || execution?.output, null, 2))}
|
||||||
|
>
|
||||||
|
<IconCopy size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Paper bg="gray.1" p="sm">
|
||||||
|
<Text size="sm" component="pre" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{JSON.stringify(result.output || execution?.output, null, 2)}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{(result.error || execution?.error) && (
|
||||||
|
<Alert color="red" mt="sm">
|
||||||
|
<Text size="sm">{result.error || execution?.error}</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logs */}
|
||||||
|
{async && (
|
||||||
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>Logs:</Text>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconRefresh size={12} />}
|
||||||
|
onClick={() => result.execution_id && loadLogs(result.execution_id)}
|
||||||
|
loading={loadingLogs}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Paper bg="gray.9" p="sm" mah={200} style={{ overflow: 'auto' }}>
|
||||||
|
{loadingLogs ? (
|
||||||
|
<Group justify="center">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Group>
|
||||||
|
) : logs.length > 0 ? (
|
||||||
|
<Text size="xs" c="white" component="pre">
|
||||||
|
{logs.join('\n')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text size="xs" c="gray.5">No logs available</Text>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
299
faas/web/src/components/FunctionForm.tsx
Normal file
299
faas/web/src/components/FunctionForm.tsx
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
Select,
|
||||||
|
NumberInput,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Paper,
|
||||||
|
Divider,
|
||||||
|
JsonInput,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { functionApi } from '../services/api';
|
||||||
|
import { FunctionDefinition, CreateFunctionRequest, UpdateFunctionRequest, RuntimeType } from '../types';
|
||||||
|
|
||||||
|
interface FunctionFormProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
editFunction?: FunctionDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const FunctionForm: React.FC<FunctionFormProps> = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
editFunction,
|
||||||
|
}) => {
|
||||||
|
const isEditing = !!editFunction;
|
||||||
|
const [runtimeOptions, setRuntimeOptions] = useState<Array<{value: string; label: string}>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch available runtimes from backend
|
||||||
|
const fetchRuntimes = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:8083/api/runtimes');
|
||||||
|
const data = await response.json();
|
||||||
|
setRuntimeOptions(data.runtimes || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch runtimes:', error);
|
||||||
|
// Fallback to default options
|
||||||
|
setRuntimeOptions([
|
||||||
|
{ value: 'nodejs18', label: 'Node.js 18.x' },
|
||||||
|
{ value: 'python3.9', label: 'Python 3.9' },
|
||||||
|
{ value: 'go1.20', label: 'Go 1.20' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opened) {
|
||||||
|
fetchRuntimes();
|
||||||
|
}
|
||||||
|
}, [opened]);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
name: editFunction?.name || '',
|
||||||
|
app_id: editFunction?.app_id || 'default',
|
||||||
|
runtime: editFunction?.runtime || 'nodejs18' as RuntimeType,
|
||||||
|
image: editFunction?.image || '',
|
||||||
|
handler: editFunction?.handler || 'index.handler',
|
||||||
|
code: editFunction?.code || '',
|
||||||
|
environment: editFunction?.environment ? JSON.stringify(editFunction.environment, null, 2) : '{}',
|
||||||
|
timeout: editFunction?.timeout || '30s',
|
||||||
|
memory: editFunction?.memory || 128,
|
||||||
|
owner: {
|
||||||
|
type: editFunction?.owner?.type || 'team' as const,
|
||||||
|
name: editFunction?.owner?.name || 'FaaS Team',
|
||||||
|
owner: editFunction?.owner?.owner || 'admin@example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: (value) => value.length < 1 ? 'Name is required' : null,
|
||||||
|
app_id: (value) => value.length < 1 ? 'App ID is required' : null,
|
||||||
|
runtime: (value) => !value ? 'Runtime is required' : null,
|
||||||
|
image: (value) => value.length < 1 ? 'Image is required' : null,
|
||||||
|
handler: (value) => value.length < 1 ? 'Handler is required' : null,
|
||||||
|
timeout: (value) => !value.match(/^\d+[smh]$/) ? 'Timeout must be in format like 30s, 5m, 1h' : null,
|
||||||
|
memory: (value) => value < 64 || value > 3008 ? 'Memory must be between 64 and 3008 MB' : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRuntimeChange = (runtime: string | null) => {
|
||||||
|
// if (runtime && DEFAULT_IMAGES[runtime]) {
|
||||||
|
// form.setFieldValue('image', DEFAULT_IMAGES[runtime]);
|
||||||
|
// }
|
||||||
|
form.setFieldValue('runtime', runtime as RuntimeType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
|
try {
|
||||||
|
// Parse environment variables JSON
|
||||||
|
let parsedEnvironment;
|
||||||
|
try {
|
||||||
|
parsedEnvironment = values.environment ? JSON.parse(values.environment) : undefined;
|
||||||
|
} catch (error) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Invalid JSON in environment variables',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isEditing && editFunction) {
|
||||||
|
const updateData: UpdateFunctionRequest = {
|
||||||
|
name: values.name,
|
||||||
|
runtime: values.runtime,
|
||||||
|
image: values.image,
|
||||||
|
handler: values.handler,
|
||||||
|
code: values.code || undefined,
|
||||||
|
environment: parsedEnvironment,
|
||||||
|
timeout: values.timeout,
|
||||||
|
memory: values.memory,
|
||||||
|
owner: values.owner,
|
||||||
|
};
|
||||||
|
await functionApi.update(editFunction.id, updateData);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Function updated successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const createData: CreateFunctionRequest = {
|
||||||
|
name: values.name,
|
||||||
|
app_id: values.app_id,
|
||||||
|
runtime: values.runtime,
|
||||||
|
image: values.image,
|
||||||
|
handler: values.handler,
|
||||||
|
code: values.code || undefined,
|
||||||
|
environment: parsedEnvironment,
|
||||||
|
timeout: values.timeout,
|
||||||
|
memory: values.memory,
|
||||||
|
owner: values.owner,
|
||||||
|
};
|
||||||
|
await functionApi.create(createData);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Function created successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onSuccess();
|
||||||
|
onClose();
|
||||||
|
form.reset();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving function:', error);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: `Failed to ${isEditing ? 'update' : 'create'} function`,
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
title={isEditing ? 'Edit Function' : 'Create Function'}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group grow>
|
||||||
|
<TextInput
|
||||||
|
label="Function Name"
|
||||||
|
placeholder="my-function"
|
||||||
|
required
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="App ID"
|
||||||
|
placeholder="my-app"
|
||||||
|
required
|
||||||
|
disabled={isEditing}
|
||||||
|
{...form.getInputProps('app_id')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group grow>
|
||||||
|
<Select
|
||||||
|
label="Runtime"
|
||||||
|
placeholder="Select runtime"
|
||||||
|
required
|
||||||
|
data={runtimeOptions}
|
||||||
|
{...form.getInputProps('runtime')}
|
||||||
|
onChange={handleRuntimeChange}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Memory (MB)"
|
||||||
|
placeholder="128"
|
||||||
|
required
|
||||||
|
min={64}
|
||||||
|
max={3008}
|
||||||
|
{...form.getInputProps('memory')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group grow>
|
||||||
|
<TextInput
|
||||||
|
label="Timeout"
|
||||||
|
placeholder="30s"
|
||||||
|
required
|
||||||
|
{...form.getInputProps('timeout')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Handler"
|
||||||
|
description="The entry point for your function (e.g., 'index.handler' means handler function in index file)"
|
||||||
|
placeholder="index.handler"
|
||||||
|
required
|
||||||
|
{...form.getInputProps('handler')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Function Code"
|
||||||
|
rows={16}
|
||||||
|
resize={"vertical"}
|
||||||
|
placeholder={`// Node.js example:
|
||||||
|
exports.handler = async (event, context) => {
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
body: JSON.stringify({ message: 'Hello World!' })
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Python example:
|
||||||
|
def handler(event, context):
|
||||||
|
return {
|
||||||
|
'statusCode': 200,
|
||||||
|
'body': json.dumps({'message': 'Hello World!'})
|
||||||
|
}`}
|
||||||
|
minRows={16}
|
||||||
|
{...form.getInputProps('code')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<JsonInput
|
||||||
|
label="Environment Variables"
|
||||||
|
description="JSON object with key-value pairs that will be available in your function runtime"
|
||||||
|
placeholder={`{
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"API_URL": "https://api.example.com",
|
||||||
|
"DATABASE_HOST": "db.example.com",
|
||||||
|
"LOG_LEVEL": "info"
|
||||||
|
}`}
|
||||||
|
validationError="Invalid JSON - please check your syntax"
|
||||||
|
formatOnBlur
|
||||||
|
autosize
|
||||||
|
minRows={4}
|
||||||
|
{...form.getInputProps('environment')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" bg="gray.0">
|
||||||
|
<Text size="sm" fw={500} mb="xs">Owner Information</Text>
|
||||||
|
<Group grow>
|
||||||
|
<Select
|
||||||
|
label="Owner Type"
|
||||||
|
data={[
|
||||||
|
{ value: 'individual', label: 'Individual' },
|
||||||
|
{ value: 'team', label: 'Team' },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps('owner.type')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Owner Name"
|
||||||
|
placeholder="Team Name"
|
||||||
|
{...form.getInputProps('owner.name')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
label="Owner Email"
|
||||||
|
placeholder="owner@example.com"
|
||||||
|
mt="xs"
|
||||||
|
{...form.getInputProps('owner.owner')}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button variant="light" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{isEditing ? 'Update' : 'Create'} Function
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
271
faas/web/src/components/FunctionList.tsx
Normal file
271
faas/web/src/components/FunctionList.tsx
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
ActionIcon,
|
||||||
|
Menu,
|
||||||
|
Paper,
|
||||||
|
Title,
|
||||||
|
Alert,
|
||||||
|
Loader,
|
||||||
|
Center,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconPlay,
|
||||||
|
IconSettings,
|
||||||
|
IconTrash,
|
||||||
|
IconRocket,
|
||||||
|
IconCode,
|
||||||
|
IconDots,
|
||||||
|
IconPlus,
|
||||||
|
IconRefresh,
|
||||||
|
IconExclamationCircle,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { functionApi } from '../services/api';
|
||||||
|
import { FunctionDefinition } from '../types';
|
||||||
|
|
||||||
|
interface FunctionListProps {
|
||||||
|
onCreateFunction: () => void;
|
||||||
|
onEditFunction: (func: FunctionDefinition) => void;
|
||||||
|
onExecuteFunction: (func: FunctionDefinition) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FunctionList: React.FC<FunctionListProps> = ({
|
||||||
|
onCreateFunction,
|
||||||
|
onEditFunction,
|
||||||
|
onExecuteFunction,
|
||||||
|
}) => {
|
||||||
|
const [functions, setFunctions] = useState<FunctionDefinition[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadFunctions = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await functionApi.list();
|
||||||
|
setFunctions(response.data.functions || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load functions:', err);
|
||||||
|
setError('Failed to load functions');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFunctions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = async (func: FunctionDefinition) => {
|
||||||
|
if (!confirm(`Are you sure you want to delete function "${func.name}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await functionApi.delete(func.id);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: `Function "${func.name}" deleted successfully`,
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
loadFunctions();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete function:', err);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: `Failed to delete function "${func.name}"`,
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeploy = async (func: FunctionDefinition) => {
|
||||||
|
try {
|
||||||
|
await functionApi.deploy(func.id);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message: `Function "${func.name}" deployed successfully`,
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to deploy function:', err);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: `Failed to deploy function "${func.name}"`,
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRuntimeColor = (runtime: string) => {
|
||||||
|
switch (runtime) {
|
||||||
|
case 'nodejs18': return 'green';
|
||||||
|
case 'python3.9': return 'blue';
|
||||||
|
case 'go1.20': return 'cyan';
|
||||||
|
default: return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Center py={60}>
|
||||||
|
<Loader size="lg" />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Alert icon={<IconExclamationCircle size={16} />} title="Error" color="red" mb="md">
|
||||||
|
{error}
|
||||||
|
<Button variant="light" size="xs" mt="sm" onClick={loadFunctions}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper shadow="xs" p="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={2}>Functions</Title>
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconRefresh size={14} />}
|
||||||
|
variant="light"
|
||||||
|
onClick={loadFunctions}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={14} />}
|
||||||
|
onClick={onCreateFunction}
|
||||||
|
>
|
||||||
|
Create Function
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{functions.length === 0 ? (
|
||||||
|
<Center py={40}>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<IconCode size={48} color="gray" />
|
||||||
|
<Text size="lg" mt="md" c="dimmed">
|
||||||
|
No functions found
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
Create your first serverless function to get started
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={14} />}
|
||||||
|
onClick={onCreateFunction}
|
||||||
|
>
|
||||||
|
Create Function
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Table striped>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Name</Table.Th>
|
||||||
|
<Table.Th>Runtime</Table.Th>
|
||||||
|
<Table.Th>Image</Table.Th>
|
||||||
|
<Table.Th>Memory</Table.Th>
|
||||||
|
<Table.Th>Timeout</Table.Th>
|
||||||
|
<Table.Th>Owner</Table.Th>
|
||||||
|
<Table.Th>Created</Table.Th>
|
||||||
|
<Table.Th>Actions</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{functions.map((func) => (
|
||||||
|
<Table.Tr key={func.id}>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fw={500}>{func.name}</Text>
|
||||||
|
<Text size="xs" c="dimmed">{func.handler}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge color={getRuntimeColor(func.runtime)} variant="light">
|
||||||
|
{func.runtime}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{func.image}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm">{func.memory} MB</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm">{func.timeout}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm">
|
||||||
|
{func.owner.name}
|
||||||
|
<Text size="xs" c="dimmed">({func.owner.type})</Text>
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(func.created_at).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Tooltip label="Execute Function">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="green"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onExecuteFunction(func)}
|
||||||
|
>
|
||||||
|
<IconPlay size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu position="bottom-end">
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon variant="light" size="sm">
|
||||||
|
<IconDots size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconSettings size={16} />}
|
||||||
|
onClick={() => onEditFunction(func)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconRocket size={16} />}
|
||||||
|
onClick={() => handleDeploy(func)}
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconTrash size={16} />}
|
||||||
|
color="red"
|
||||||
|
onClick={() => handleDelete(func)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
9
faas/web/src/index.tsx
Normal file
9
faas/web/src/index.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
if (container) {
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<App />);
|
||||||
|
}
|
||||||
88
faas/web/src/services/api.ts
Normal file
88
faas/web/src/services/api.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import {
|
||||||
|
FunctionDefinition,
|
||||||
|
FunctionExecution,
|
||||||
|
CreateFunctionRequest,
|
||||||
|
UpdateFunctionRequest,
|
||||||
|
ExecuteFunctionRequest,
|
||||||
|
ExecuteFunctionResponse,
|
||||||
|
RuntimeInfo,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||||||
|
? '/api/faas/api'
|
||||||
|
: 'http://localhost:8083/api';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Email': 'admin@example.com', // Mock auth header
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add response interceptor for error handling
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
console.error('API Error:', error.response?.data || error.message);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const functionApi = {
|
||||||
|
// Function management
|
||||||
|
list: (appId?: string, limit = 50, offset = 0) =>
|
||||||
|
api.get<{ functions: FunctionDefinition[] }>('/functions', {
|
||||||
|
params: { app_id: appId, limit, offset },
|
||||||
|
}),
|
||||||
|
|
||||||
|
create: (data: CreateFunctionRequest) =>
|
||||||
|
api.post<FunctionDefinition>('/functions', data),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
api.get<FunctionDefinition>(`/functions/${id}`),
|
||||||
|
|
||||||
|
update: (id: string, data: UpdateFunctionRequest) =>
|
||||||
|
api.put<FunctionDefinition>(`/functions/${id}`, data),
|
||||||
|
|
||||||
|
delete: (id: string) =>
|
||||||
|
api.delete(`/functions/${id}`),
|
||||||
|
|
||||||
|
deploy: (id: string, force = false) =>
|
||||||
|
api.post(`/functions/${id}/deploy`, { force }),
|
||||||
|
|
||||||
|
// Function execution
|
||||||
|
execute: (id: string, data: Omit<ExecuteFunctionRequest, 'function_id'>) =>
|
||||||
|
api.post<ExecuteFunctionResponse>(`/functions/${id}/execute`, data),
|
||||||
|
|
||||||
|
invoke: (id: string, data?: { input?: any }) =>
|
||||||
|
api.post<ExecuteFunctionResponse>(`/functions/${id}/invoke`, data),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const executionApi = {
|
||||||
|
// Execution management
|
||||||
|
list: (functionId?: string, limit = 50, offset = 0) =>
|
||||||
|
api.get<{ executions: FunctionExecution[] }>('/executions', {
|
||||||
|
params: { function_id: functionId, limit, offset },
|
||||||
|
}),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
api.get<FunctionExecution>(`/executions/${id}`),
|
||||||
|
|
||||||
|
cancel: (id: string) =>
|
||||||
|
api.delete(`/executions/${id}`),
|
||||||
|
|
||||||
|
getLogs: (id: string) =>
|
||||||
|
api.get<{ logs: string[] }>(`/executions/${id}/logs`),
|
||||||
|
|
||||||
|
getRunning: () =>
|
||||||
|
api.get<{ executions: FunctionExecution[]; count: number }>('/executions/running'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const healthApi = {
|
||||||
|
health: () => api.get('/health'),
|
||||||
|
ready: () => api.get('/ready'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
91
faas/web/src/types/index.ts
Normal file
91
faas/web/src/types/index.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
export type RuntimeType = 'nodejs18' | 'python3.9' | 'go1.20' | 'custom';
|
||||||
|
|
||||||
|
export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'timeout' | 'canceled';
|
||||||
|
|
||||||
|
export type OwnerType = 'individual' | 'team';
|
||||||
|
|
||||||
|
export interface Owner {
|
||||||
|
type: OwnerType;
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
app_id: string;
|
||||||
|
runtime: RuntimeType;
|
||||||
|
image: string;
|
||||||
|
handler: string;
|
||||||
|
code?: string;
|
||||||
|
environment?: Record<string, string>;
|
||||||
|
timeout: string;
|
||||||
|
memory: number;
|
||||||
|
owner: Owner;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionExecution {
|
||||||
|
id: string;
|
||||||
|
function_id: string;
|
||||||
|
status: ExecutionStatus;
|
||||||
|
input?: any;
|
||||||
|
output?: any;
|
||||||
|
error?: string;
|
||||||
|
duration?: number;
|
||||||
|
memory_used?: number;
|
||||||
|
container_id?: string;
|
||||||
|
executor_id: string;
|
||||||
|
created_at: string;
|
||||||
|
started_at?: string;
|
||||||
|
completed_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateFunctionRequest {
|
||||||
|
name: string;
|
||||||
|
app_id: string;
|
||||||
|
runtime: RuntimeType;
|
||||||
|
image: string;
|
||||||
|
handler: string;
|
||||||
|
code?: string;
|
||||||
|
environment?: Record<string, string>;
|
||||||
|
timeout: string;
|
||||||
|
memory: number;
|
||||||
|
owner: Owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateFunctionRequest {
|
||||||
|
name?: string;
|
||||||
|
runtime?: RuntimeType;
|
||||||
|
image?: string;
|
||||||
|
handler?: string;
|
||||||
|
code?: string;
|
||||||
|
environment?: Record<string, string>;
|
||||||
|
timeout?: string;
|
||||||
|
memory?: number;
|
||||||
|
owner?: Owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteFunctionRequest {
|
||||||
|
function_id: string;
|
||||||
|
input?: any;
|
||||||
|
async?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteFunctionResponse {
|
||||||
|
execution_id: string;
|
||||||
|
status: ExecutionStatus;
|
||||||
|
output?: any;
|
||||||
|
error?: string;
|
||||||
|
duration?: number;
|
||||||
|
memory_used?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeInfo {
|
||||||
|
type: RuntimeType;
|
||||||
|
version: string;
|
||||||
|
available: boolean;
|
||||||
|
default_image: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
88
faas/web/webpack.config.js
Normal file
88
faas/web/webpack.config.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const { ModuleFederationPlugin } = require('webpack').container;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'development',
|
||||||
|
entry: './src/index.tsx',
|
||||||
|
devServer: {
|
||||||
|
port: 3003,
|
||||||
|
historyApiFallback: true,
|
||||||
|
static: {
|
||||||
|
directory: './public',
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
publicPath: 'auto',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js', '.jsx'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx|ts|tsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: [
|
||||||
|
'@babel/preset-react',
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ['style-loader', 'css-loader'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new ModuleFederationPlugin({
|
||||||
|
name: 'faas',
|
||||||
|
filename: 'remoteEntry.js',
|
||||||
|
exposes: {
|
||||||
|
'./FaaSApp': './src/App',
|
||||||
|
},
|
||||||
|
shared: {
|
||||||
|
react: {
|
||||||
|
singleton: true,
|
||||||
|
requiredVersion: '^18.2.0',
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
|
'react-dom': {
|
||||||
|
singleton: true,
|
||||||
|
requiredVersion: '^18.2.0',
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
|
'@mantine/core': {
|
||||||
|
singleton: true,
|
||||||
|
requiredVersion: '^7.0.0',
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
|
'@mantine/hooks': {
|
||||||
|
singleton: true,
|
||||||
|
requiredVersion: '^7.0.0',
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
|
'@mantine/notifications': {
|
||||||
|
singleton: true,
|
||||||
|
requiredVersion: '^7.0.0',
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
|
'@tabler/icons-react': {
|
||||||
|
singleton: true,
|
||||||
|
requiredVersion: '^2.40.0',
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './public/index.html',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
108
nginx/default.conf
Normal file
108
nginx/default.conf
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
|
||||||
|
# CORS headers for Module Federation
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-User-Email" always;
|
||||||
|
|
||||||
|
# Handle preflight OPTIONS requests
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header Access-Control-Allow-Origin "*";
|
||||||
|
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
||||||
|
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-User-Email";
|
||||||
|
add_header Access-Control-Max-Age 1728000;
|
||||||
|
add_header Content-Type "text/plain; charset=utf-8";
|
||||||
|
add_header Content-Length 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main shell dashboard
|
||||||
|
location / {
|
||||||
|
proxy_pass http://shell-dashboard:80;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-User-Email $http_x_user_email;
|
||||||
|
}
|
||||||
|
|
||||||
|
# KMS API
|
||||||
|
location /api/kms/ {
|
||||||
|
rewrite ^/api/kms/(.*) /$1 break;
|
||||||
|
proxy_pass http://kms-api-service:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-User-Email $http_x_user_email;
|
||||||
|
}
|
||||||
|
|
||||||
|
# FaaS API
|
||||||
|
location /api/faas/ {
|
||||||
|
rewrite ^/api/faas/(.*) /$1 break;
|
||||||
|
proxy_pass http://faas-api-service:8082;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-User-Email $http_x_user_email;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Demo module assets
|
||||||
|
location /demo/ {
|
||||||
|
proxy_pass http://demo-module:80/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# KMS module assets
|
||||||
|
location /kms/ {
|
||||||
|
proxy_pass http://kms-frontend:80/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# FaaS module assets
|
||||||
|
location /faas/ {
|
||||||
|
proxy_pass http://faas-frontend:80/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health checks
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/kms/health {
|
||||||
|
proxy_pass http://kms-api-service:8080/health;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/faas/health {
|
||||||
|
proxy_pass http://faas-api-service:8082/health;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
nginx/nginx.conf
Normal file
32
nginx/nginx.conf
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
# Basic settings
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
# Include server configurations
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import Breadcrumbs from './Breadcrumbs';
|
|||||||
|
|
||||||
const DemoApp = React.lazy(() => import('demo/App'));
|
const DemoApp = React.lazy(() => import('demo/App'));
|
||||||
const KMSApp = React.lazy(() => import('kms/App'));
|
const KMSApp = React.lazy(() => import('kms/App'));
|
||||||
|
const FaaSApp = React.lazy(() => import('faas/FaaSApp'));
|
||||||
|
|
||||||
const AppLoader: React.FC = () => {
|
const AppLoader: React.FC = () => {
|
||||||
const { appName } = useParams<{ appName: string }>();
|
const { appName } = useParams<{ appName: string }>();
|
||||||
@ -44,6 +45,12 @@ const AppLoader: React.FC = () => {
|
|||||||
<KMSApp />
|
<KMSApp />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
case 'faas':
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
|
<FaaSApp />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<ErrorFallback
|
<ErrorFallback
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
IconKey,
|
IconKey,
|
||||||
|
IconFunction,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const Navigation: React.FC = () => {
|
const Navigation: React.FC = () => {
|
||||||
@ -22,6 +23,11 @@ const Navigation: React.FC = () => {
|
|||||||
icon: IconKey,
|
icon: IconKey,
|
||||||
path: '/app/kms',
|
path: '/app/kms',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Functions',
|
||||||
|
icon: IconFunction,
|
||||||
|
path: '/app/faas',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define which apps are favorited (you could make this dynamic later)
|
// Define which apps are favorited (you could make this dynamic later)
|
||||||
|
|||||||
@ -47,6 +47,7 @@ module.exports = {
|
|||||||
remotes: {
|
remotes: {
|
||||||
demo: 'demo@http://localhost:3001/remoteEntry.js',
|
demo: 'demo@http://localhost:3001/remoteEntry.js',
|
||||||
kms: 'kms@http://localhost:3002/remoteEntry.js',
|
kms: 'kms@http://localhost:3002/remoteEntry.js',
|
||||||
|
faas: 'faas@http://localhost:3003/remoteEntry.js',
|
||||||
},
|
},
|
||||||
shared: {
|
shared: {
|
||||||
react: {
|
react: {
|
||||||
|
|||||||
Reference in New Issue
Block a user