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 }