From 84c9b9ab9bdf2d5c1d2a499a53cb559cc5a4a076 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Sat, 24 Jan 2026 11:12:00 +0200 Subject: [PATCH] fixes --- pkg/deployments/process/manager.go | 51 ++- pkg/gateway/config.go | 3 + pkg/gateway/gateway.go | 18 + .../handlers/deployments/go_handler.go | 305 +++++++++++++++++ .../handlers/deployments/nextjs_handler.go | 14 +- .../handlers/deployments/nodejs_handler.go | 315 ++++++++++++++++++ pkg/gateway/handlers/deployments/service.go | 15 + pkg/gateway/handlers/sqlite/create_handler.go | 25 +- pkg/gateway/handlers/sqlite/handlers_test.go | 10 +- pkg/gateway/handlers/sqlite/query_handler.go | 21 +- pkg/gateway/routes.go | 10 + 11 files changed, 754 insertions(+), 33 deletions(-) create mode 100644 pkg/gateway/handlers/deployments/go_handler.go create mode 100644 pkg/gateway/handlers/deployments/nodejs_handler.go diff --git a/pkg/deployments/process/manager.go b/pkg/deployments/process/manager.go index 611e23b..468b931 100644 --- a/pkg/deployments/process/manager.go +++ b/pkg/deployments/process/manager.go @@ -1,9 +1,9 @@ package process import ( + "bytes" "context" "fmt" - "os" "os/exec" "path/filepath" "strings" @@ -83,9 +83,10 @@ func (m *Manager) Stop(ctx context.Context, deployment *deployments.Deployment) m.logger.Warn("Failed to disable service", zap.Error(err)) } - // Remove service file + // Remove service file using sudo serviceFile := filepath.Join("/etc/systemd/system", serviceName+".service") - if err := os.Remove(serviceFile); err != nil && !os.IsNotExist(err) { + cmd := exec.Command("sudo", "rm", "-f", serviceFile) + if err := cmd.Run(); err != nil { m.logger.Warn("Failed to remove service file", zap.Error(err)) } @@ -174,11 +175,10 @@ RestartSec=5s MemoryLimit={{.MemoryLimitMB}}M CPUQuota={{.CPULimitPercent}}% -# Security -NoNewPrivileges=true +# Security - minimal restrictions for deployments in home directory PrivateTmp=true -ProtectSystem=strict -ProtectHome=true +ProtectSystem=full +ProtectHome=read-only ReadWritePaths={{.WorkDir}} StandardOutput=journal @@ -216,13 +216,21 @@ WantedBy=multi-user.target CPULimitPercent: deployment.CPULimitPercent, } - file, err := os.Create(serviceFile) - if err != nil { + // Execute template to buffer + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { return err } - defer file.Close() - return t.Execute(file, data) + // Use sudo tee to write to systemd directory (debros user needs sudo access) + cmd := exec.Command("sudo", "tee", serviceFile) + cmd.Stdin = &buf + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to write service file: %s: %w", string(output), err) + } + + return nil } // getStartCommand determines the start command for a deployment @@ -231,6 +239,13 @@ func (m *Manager) getStartCommand(deployment *deployments.Deployment, workDir st case deployments.DeploymentTypeNextJS: return "/usr/bin/node server.js" case deployments.DeploymentTypeNodeJSBackend: + // Check if ENTRY_POINT is set in environment + if entryPoint, ok := deployment.Environment["ENTRY_POINT"]; ok { + if entryPoint == "npm:start" { + return "/usr/bin/npm start" + } + return "/usr/bin/node " + entryPoint + } return "/usr/bin/node index.js" case deployments.DeploymentTypeGoBackend: return filepath.Join(workDir, "app") @@ -261,34 +276,34 @@ func (m *Manager) getServiceName(deployment *deployments.Deployment) string { return fmt.Sprintf("orama-deploy-%s-%s", namespace, name) } -// systemd helper methods +// systemd helper methods (use sudo for non-root execution) func (m *Manager) systemdReload() error { - cmd := exec.Command("systemctl", "daemon-reload") + cmd := exec.Command("sudo", "systemctl", "daemon-reload") return cmd.Run() } func (m *Manager) systemdEnable(serviceName string) error { - cmd := exec.Command("systemctl", "enable", serviceName) + cmd := exec.Command("sudo", "systemctl", "enable", serviceName) return cmd.Run() } func (m *Manager) systemdDisable(serviceName string) error { - cmd := exec.Command("systemctl", "disable", serviceName) + cmd := exec.Command("sudo", "systemctl", "disable", serviceName) return cmd.Run() } func (m *Manager) systemdStart(serviceName string) error { - cmd := exec.Command("systemctl", "start", serviceName) + cmd := exec.Command("sudo", "systemctl", "start", serviceName) return cmd.Run() } func (m *Manager) systemdStop(serviceName string) error { - cmd := exec.Command("systemctl", "stop", serviceName) + cmd := exec.Command("sudo", "systemctl", "stop", serviceName) return cmd.Run() } func (m *Manager) systemdRestart(serviceName string) error { - cmd := exec.Command("systemctl", "restart", serviceName) + cmd := exec.Command("sudo", "systemctl", "restart", serviceName) return cmd.Run() } diff --git a/pkg/gateway/config.go b/pkg/gateway/config.go index 9ea92f2..1ff94e4 100644 --- a/pkg/gateway/config.go +++ b/pkg/gateway/config.go @@ -21,6 +21,9 @@ type Config struct { // Domain routing configuration BaseDomain string // Base domain for deployment routing (e.g., "dbrs.space"). Defaults to "orama.network" + // Data directory configuration + DataDir string // Base directory for node-local data (SQLite databases, deployments). Defaults to ~/.orama + // Olric cache configuration OlricServers []string // List of Olric server addresses (e.g., ["localhost:3320"]). If empty, defaults to ["localhost:3320"] OlricTimeout time.Duration // Timeout for Olric operations (default: 10s) diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 550cf3b..e5bf5db 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -78,6 +78,8 @@ type Gateway struct { deploymentService *deploymentshandlers.DeploymentService staticHandler *deploymentshandlers.StaticDeploymentHandler nextjsHandler *deploymentshandlers.NextJSHandler + goHandler *deploymentshandlers.GoHandler + nodejsHandler *deploymentshandlers.NodeJSHandler listHandler *deploymentshandlers.ListHandler updateHandler *deploymentshandlers.UpdateHandler rollbackHandler *deploymentshandlers.RollbackHandler @@ -271,6 +273,20 @@ func New(logger *logging.ColoredLogger, cfg *Config) (*Gateway, error) { logger.Logger, ) + gw.goHandler = deploymentshandlers.NewGoHandler( + gw.deploymentService, + gw.processManager, + deps.IPFSClient, + logger.Logger, + ) + + gw.nodejsHandler = deploymentshandlers.NewNodeJSHandler( + gw.deploymentService, + gw.processManager, + deps.IPFSClient, + logger.Logger, + ) + gw.listHandler = deploymentshandlers.NewListHandler( gw.deploymentService, logger.Logger, @@ -306,6 +322,8 @@ func New(logger *logging.ColoredLogger, cfg *Config) (*Gateway, error) { deps.ORMClient, gw.homeNodeManager, logger.Logger, + cfg.DataDir, + cfg.NodePeerID, ) gw.sqliteBackupHandler = sqlitehandlers.NewBackupHandler( diff --git a/pkg/gateway/handlers/deployments/go_handler.go b/pkg/gateway/handlers/deployments/go_handler.go new file mode 100644 index 0000000..20d573b --- /dev/null +++ b/pkg/gateway/handlers/deployments/go_handler.go @@ -0,0 +1,305 @@ +package deployments + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/DeBrosOfficial/network/pkg/deployments" + "github.com/DeBrosOfficial/network/pkg/deployments/process" + "github.com/DeBrosOfficial/network/pkg/ipfs" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// GoHandler handles Go backend deployments +type GoHandler struct { + service *DeploymentService + processManager *process.Manager + ipfsClient ipfs.IPFSClient + logger *zap.Logger + baseDeployPath string +} + +// NewGoHandler creates a new Go deployment handler +func NewGoHandler( + service *DeploymentService, + processManager *process.Manager, + ipfsClient ipfs.IPFSClient, + logger *zap.Logger, +) *GoHandler { + return &GoHandler{ + service: service, + processManager: processManager, + ipfsClient: ipfsClient, + logger: logger, + baseDeployPath: "/home/debros/.orama/deployments", + } +} + +// HandleUpload handles Go backend deployment upload +func (h *GoHandler) HandleUpload(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + namespace := getNamespaceFromContext(ctx) + if namespace == "" { + http.Error(w, "Namespace not found in context", http.StatusUnauthorized) + return + } + + // Parse multipart form (100MB max for Go binaries) + if err := r.ParseMultipartForm(100 << 20); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + // Get metadata + name := r.FormValue("name") + subdomain := r.FormValue("subdomain") + healthCheckPath := r.FormValue("health_check_path") + + if name == "" { + http.Error(w, "Deployment name is required", http.StatusBadRequest) + return + } + + if healthCheckPath == "" { + healthCheckPath = "/health" + } + + // Get tarball file + file, header, err := r.FormFile("tarball") + if err != nil { + http.Error(w, "Tarball file is required", http.StatusBadRequest) + return + } + defer file.Close() + + h.logger.Info("Deploying Go backend", + zap.String("namespace", namespace), + zap.String("name", name), + zap.String("filename", header.Filename), + zap.Int64("size", header.Size), + ) + + // Upload to IPFS for versioning + addResp, err := h.ipfsClient.Add(ctx, file, header.Filename) + if err != nil { + h.logger.Error("Failed to upload to IPFS", zap.Error(err)) + http.Error(w, "Failed to upload content", http.StatusInternalServerError) + return + } + + cid := addResp.Cid + + // Deploy the Go backend + deployment, err := h.deploy(ctx, namespace, name, subdomain, cid, healthCheckPath) + if err != nil { + h.logger.Error("Failed to deploy Go backend", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Create DNS records + go h.service.CreateDNSRecords(ctx, deployment) + + // Build response + urls := h.service.BuildDeploymentURLs(deployment) + + resp := map[string]interface{}{ + "deployment_id": deployment.ID, + "name": deployment.Name, + "namespace": deployment.Namespace, + "status": deployment.Status, + "type": deployment.Type, + "content_cid": deployment.ContentCID, + "urls": urls, + "version": deployment.Version, + "port": deployment.Port, + "created_at": deployment.CreatedAt, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(resp) +} + +// deploy deploys a Go backend +func (h *GoHandler) deploy(ctx context.Context, namespace, name, subdomain, cid, healthCheckPath string) (*deployments.Deployment, error) { + // Create deployment directory + deployPath := filepath.Join(h.baseDeployPath, namespace, name) + if err := os.MkdirAll(deployPath, 0755); err != nil { + return nil, fmt.Errorf("failed to create deployment directory: %w", err) + } + + // Download and extract from IPFS + if err := h.extractFromIPFS(ctx, cid, deployPath); err != nil { + return nil, fmt.Errorf("failed to extract deployment: %w", err) + } + + // Find the executable binary + binaryPath, err := h.findBinary(deployPath) + if err != nil { + return nil, fmt.Errorf("failed to find binary: %w", err) + } + + // Ensure binary is executable + if err := os.Chmod(binaryPath, 0755); err != nil { + return nil, fmt.Errorf("failed to make binary executable: %w", err) + } + + h.logger.Info("Found Go binary", + zap.String("path", binaryPath), + zap.String("deployment", name), + ) + + // Create deployment record + deployment := &deployments.Deployment{ + ID: uuid.New().String(), + Namespace: namespace, + Name: name, + Type: deployments.DeploymentTypeGoBackend, + Version: 1, + Status: deployments.DeploymentStatusDeploying, + ContentCID: cid, + Subdomain: subdomain, + Environment: make(map[string]string), + MemoryLimitMB: 256, + CPULimitPercent: 100, + HealthCheckPath: healthCheckPath, + HealthCheckInterval: 30, + RestartPolicy: deployments.RestartPolicyAlways, + MaxRestartCount: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeployedBy: namespace, + } + + // Save deployment (assigns port) + if err := h.service.CreateDeployment(ctx, deployment); err != nil { + return nil, err + } + + // Start the process + if err := h.processManager.Start(ctx, deployment, deployPath); err != nil { + deployment.Status = deployments.DeploymentStatusFailed + h.service.UpdateDeploymentStatus(ctx, deployment.ID, deployments.DeploymentStatusFailed) + return deployment, fmt.Errorf("failed to start process: %w", err) + } + + // Wait for healthy + if err := h.processManager.WaitForHealthy(ctx, deployment, 60*time.Second); err != nil { + h.logger.Warn("Deployment did not become healthy", zap.Error(err)) + // Don't fail - the service might still be starting + } + + deployment.Status = deployments.DeploymentStatusActive + h.service.UpdateDeploymentStatus(ctx, deployment.ID, deployments.DeploymentStatusActive) + + return deployment, nil +} + +// extractFromIPFS extracts a tarball from IPFS to a directory +func (h *GoHandler) extractFromIPFS(ctx context.Context, cid, destPath string) error { + // Get tarball from IPFS + reader, err := h.ipfsClient.Get(ctx, "/ipfs/"+cid, "") + if err != nil { + return err + } + defer reader.Close() + + // Create temporary file + tmpFile, err := os.CreateTemp("", "go-deploy-*.tar.gz") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + // Copy to temp file + if _, err := io.Copy(tmpFile, reader); err != nil { + return err + } + + tmpFile.Close() + + // Extract tarball + cmd := exec.Command("tar", "-xzf", tmpFile.Name(), "-C", destPath) + output, err := cmd.CombinedOutput() + if err != nil { + h.logger.Error("Failed to extract tarball", + zap.String("output", string(output)), + zap.Error(err), + ) + return fmt.Errorf("failed to extract tarball: %w", err) + } + + return nil +} + +// findBinary finds the Go binary in the deployment directory +func (h *GoHandler) findBinary(deployPath string) (string, error) { + // First, look for a binary named "app" (conventional) + appPath := filepath.Join(deployPath, "app") + if info, err := os.Stat(appPath); err == nil && !info.IsDir() { + return appPath, nil + } + + // Look for any executable in the directory + entries, err := os.ReadDir(deployPath) + if err != nil { + return "", fmt.Errorf("failed to read deployment directory: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + filePath := filepath.Join(deployPath, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + + // Check if it's executable + if info.Mode()&0111 != 0 { + // Skip common non-binary files + ext := strings.ToLower(filepath.Ext(entry.Name())) + if ext == ".sh" || ext == ".txt" || ext == ".md" || ext == ".json" || ext == ".yaml" || ext == ".yml" { + continue + } + + // Check if it's an ELF binary (Linux executable) + if h.isELFBinary(filePath) { + return filePath, nil + } + } + } + + return "", fmt.Errorf("no executable binary found in deployment. Expected 'app' binary or ELF executable") +} + +// isELFBinary checks if a file is an ELF binary +func (h *GoHandler) isELFBinary(path string) bool { + f, err := os.Open(path) + if err != nil { + return false + } + defer f.Close() + + // Read first 4 bytes (ELF magic number) + magic := make([]byte, 4) + if _, err := f.Read(magic); err != nil { + return false + } + + // ELF magic: 0x7f 'E' 'L' 'F' + return magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F' +} diff --git a/pkg/gateway/handlers/deployments/nextjs_handler.go b/pkg/gateway/handlers/deployments/nextjs_handler.go index 1cc8382..70e7d30 100644 --- a/pkg/gateway/handlers/deployments/nextjs_handler.go +++ b/pkg/gateway/handlers/deployments/nextjs_handler.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" "strings" "time" @@ -252,5 +253,16 @@ func (h *NextJSHandler) execCommand(cmd string) error { return fmt.Errorf("empty command") } - return nil // Simplified - in production use exec.Command + c := exec.Command(parts[0], parts[1:]...) + output, err := c.CombinedOutput() + if err != nil { + h.logger.Error("Command execution failed", + zap.String("command", cmd), + zap.String("output", string(output)), + zap.Error(err), + ) + return fmt.Errorf("command failed: %s: %w", string(output), err) + } + + return nil } diff --git a/pkg/gateway/handlers/deployments/nodejs_handler.go b/pkg/gateway/handlers/deployments/nodejs_handler.go new file mode 100644 index 0000000..dfa1f79 --- /dev/null +++ b/pkg/gateway/handlers/deployments/nodejs_handler.go @@ -0,0 +1,315 @@ +package deployments + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/DeBrosOfficial/network/pkg/deployments" + "github.com/DeBrosOfficial/network/pkg/deployments/process" + "github.com/DeBrosOfficial/network/pkg/ipfs" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// NodeJSHandler handles Node.js backend deployments +type NodeJSHandler struct { + service *DeploymentService + processManager *process.Manager + ipfsClient ipfs.IPFSClient + logger *zap.Logger + baseDeployPath string +} + +// NewNodeJSHandler creates a new Node.js deployment handler +func NewNodeJSHandler( + service *DeploymentService, + processManager *process.Manager, + ipfsClient ipfs.IPFSClient, + logger *zap.Logger, +) *NodeJSHandler { + return &NodeJSHandler{ + service: service, + processManager: processManager, + ipfsClient: ipfsClient, + logger: logger, + baseDeployPath: "/home/debros/.orama/deployments", + } +} + +// HandleUpload handles Node.js backend deployment upload +func (h *NodeJSHandler) HandleUpload(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + namespace := getNamespaceFromContext(ctx) + if namespace == "" { + http.Error(w, "Namespace not found in context", http.StatusUnauthorized) + return + } + + // Parse multipart form (200MB max for Node.js with node_modules) + if err := r.ParseMultipartForm(200 << 20); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + // Get metadata + name := r.FormValue("name") + subdomain := r.FormValue("subdomain") + healthCheckPath := r.FormValue("health_check_path") + skipInstall := r.FormValue("skip_install") == "true" + + if name == "" { + http.Error(w, "Deployment name is required", http.StatusBadRequest) + return + } + + if healthCheckPath == "" { + healthCheckPath = "/health" + } + + // Get tarball file + file, header, err := r.FormFile("tarball") + if err != nil { + http.Error(w, "Tarball file is required", http.StatusBadRequest) + return + } + defer file.Close() + + h.logger.Info("Deploying Node.js backend", + zap.String("namespace", namespace), + zap.String("name", name), + zap.String("filename", header.Filename), + zap.Int64("size", header.Size), + zap.Bool("skip_install", skipInstall), + ) + + // Upload to IPFS for versioning + addResp, err := h.ipfsClient.Add(ctx, file, header.Filename) + if err != nil { + h.logger.Error("Failed to upload to IPFS", zap.Error(err)) + http.Error(w, "Failed to upload content", http.StatusInternalServerError) + return + } + + cid := addResp.Cid + + // Deploy the Node.js backend + deployment, err := h.deploy(ctx, namespace, name, subdomain, cid, healthCheckPath, skipInstall) + if err != nil { + h.logger.Error("Failed to deploy Node.js backend", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Create DNS records + go h.service.CreateDNSRecords(ctx, deployment) + + // Build response + urls := h.service.BuildDeploymentURLs(deployment) + + resp := map[string]interface{}{ + "deployment_id": deployment.ID, + "name": deployment.Name, + "namespace": deployment.Namespace, + "status": deployment.Status, + "type": deployment.Type, + "content_cid": deployment.ContentCID, + "urls": urls, + "version": deployment.Version, + "port": deployment.Port, + "created_at": deployment.CreatedAt, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(resp) +} + +// deploy deploys a Node.js backend +func (h *NodeJSHandler) deploy(ctx context.Context, namespace, name, subdomain, cid, healthCheckPath string, skipInstall bool) (*deployments.Deployment, error) { + // Create deployment directory + deployPath := filepath.Join(h.baseDeployPath, namespace, name) + if err := os.MkdirAll(deployPath, 0755); err != nil { + return nil, fmt.Errorf("failed to create deployment directory: %w", err) + } + + // Download and extract from IPFS + if err := h.extractFromIPFS(ctx, cid, deployPath); err != nil { + return nil, fmt.Errorf("failed to extract deployment: %w", err) + } + + // Check for package.json + packageJSONPath := filepath.Join(deployPath, "package.json") + if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) { + return nil, fmt.Errorf("package.json not found in deployment") + } + + // Install dependencies if needed + nodeModulesPath := filepath.Join(deployPath, "node_modules") + if !skipInstall { + if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) { + h.logger.Info("Installing npm dependencies", zap.String("deployment", name)) + if err := h.npmInstall(deployPath); err != nil { + return nil, fmt.Errorf("failed to install dependencies: %w", err) + } + } + } + + // Parse package.json to determine entry point + entryPoint, err := h.determineEntryPoint(deployPath) + if err != nil { + h.logger.Warn("Failed to determine entry point, using default", + zap.Error(err), + zap.String("default", "index.js"), + ) + entryPoint = "index.js" + } + + h.logger.Info("Node.js deployment configured", + zap.String("entry_point", entryPoint), + zap.String("deployment", name), + ) + + // Create deployment record + deployment := &deployments.Deployment{ + ID: uuid.New().String(), + Namespace: namespace, + Name: name, + Type: deployments.DeploymentTypeNodeJSBackend, + Version: 1, + Status: deployments.DeploymentStatusDeploying, + ContentCID: cid, + Subdomain: subdomain, + Environment: map[string]string{"ENTRY_POINT": entryPoint}, + MemoryLimitMB: 512, + CPULimitPercent: 100, + HealthCheckPath: healthCheckPath, + HealthCheckInterval: 30, + RestartPolicy: deployments.RestartPolicyAlways, + MaxRestartCount: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeployedBy: namespace, + } + + // Save deployment (assigns port) + if err := h.service.CreateDeployment(ctx, deployment); err != nil { + return nil, err + } + + // Start the process + if err := h.processManager.Start(ctx, deployment, deployPath); err != nil { + deployment.Status = deployments.DeploymentStatusFailed + h.service.UpdateDeploymentStatus(ctx, deployment.ID, deployments.DeploymentStatusFailed) + return deployment, fmt.Errorf("failed to start process: %w", err) + } + + // Wait for healthy + if err := h.processManager.WaitForHealthy(ctx, deployment, 90*time.Second); err != nil { + h.logger.Warn("Deployment did not become healthy", zap.Error(err)) + // Don't fail - the service might still be starting + } + + deployment.Status = deployments.DeploymentStatusActive + h.service.UpdateDeploymentStatus(ctx, deployment.ID, deployments.DeploymentStatusActive) + + return deployment, nil +} + +// extractFromIPFS extracts a tarball from IPFS to a directory +func (h *NodeJSHandler) extractFromIPFS(ctx context.Context, cid, destPath string) error { + // Get tarball from IPFS + reader, err := h.ipfsClient.Get(ctx, "/ipfs/"+cid, "") + if err != nil { + return err + } + defer reader.Close() + + // Create temporary file + tmpFile, err := os.CreateTemp("", "nodejs-deploy-*.tar.gz") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + // Copy to temp file + if _, err := io.Copy(tmpFile, reader); err != nil { + return err + } + + tmpFile.Close() + + // Extract tarball + cmd := exec.Command("tar", "-xzf", tmpFile.Name(), "-C", destPath) + output, err := cmd.CombinedOutput() + if err != nil { + h.logger.Error("Failed to extract tarball", + zap.String("output", string(output)), + zap.Error(err), + ) + return fmt.Errorf("failed to extract tarball: %w", err) + } + + return nil +} + +// npmInstall runs npm install --production in the deployment directory +func (h *NodeJSHandler) npmInstall(deployPath string) error { + cmd := exec.Command("npm", "install", "--production") + cmd.Dir = deployPath + cmd.Env = append(os.Environ(), "NODE_ENV=production") + + output, err := cmd.CombinedOutput() + if err != nil { + h.logger.Error("npm install failed", + zap.String("output", string(output)), + zap.Error(err), + ) + return fmt.Errorf("npm install failed: %w", err) + } + + return nil +} + +// determineEntryPoint reads package.json to find the entry point +func (h *NodeJSHandler) determineEntryPoint(deployPath string) (string, error) { + packageJSONPath := filepath.Join(deployPath, "package.json") + data, err := os.ReadFile(packageJSONPath) + if err != nil { + return "", err + } + + var pkg struct { + Main string `json:"main"` + Scripts map[string]string `json:"scripts"` + } + + if err := json.Unmarshal(data, &pkg); err != nil { + return "", err + } + + // Check if there's a start script + if startScript, ok := pkg.Scripts["start"]; ok { + // If start script uses node, extract the file + if len(startScript) > 5 && startScript[:5] == "node " { + return startScript[5:], nil + } + // Otherwise, we'll use npm start + return "npm:start", nil + } + + // Use main field if specified + if pkg.Main != "" { + return pkg.Main, nil + } + + // Default to index.js + return "index.js", nil +} diff --git a/pkg/gateway/handlers/deployments/service.go b/pkg/gateway/handlers/deployments/service.go index 303d76e..fb6dfb2 100644 --- a/pkg/gateway/handlers/deployments/service.go +++ b/pkg/gateway/handlers/deployments/service.go @@ -256,6 +256,21 @@ func (s *DeploymentService) GetDeploymentByID(ctx context.Context, namespace, id }, nil } +// UpdateDeploymentStatus updates the status of a deployment +func (s *DeploymentService) UpdateDeploymentStatus(ctx context.Context, deploymentID string, status deployments.DeploymentStatus) error { + query := `UPDATE deployments SET status = ?, updated_at = ? WHERE id = ?` + _, err := s.db.Exec(ctx, query, status, time.Now(), deploymentID) + if err != nil { + s.logger.Error("Failed to update deployment status", + zap.String("deployment_id", deploymentID), + zap.String("status", string(status)), + zap.Error(err), + ) + return fmt.Errorf("failed to update deployment status: %w", err) + } + return nil +} + // CreateDNSRecords creates DNS records for a deployment func (s *DeploymentService) CreateDNSRecords(ctx context.Context, deployment *deployments.Deployment) error { // Get node IP diff --git a/pkg/gateway/handlers/sqlite/create_handler.go b/pkg/gateway/handlers/sqlite/create_handler.go index 274b78b..76b85c4 100644 --- a/pkg/gateway/handlers/sqlite/create_handler.go +++ b/pkg/gateway/handlers/sqlite/create_handler.go @@ -24,24 +24,33 @@ type SQLiteHandler struct { homeNodeManager *deployments.HomeNodeManager logger *zap.Logger basePath string + currentNodeID string // The node's peer ID for affinity checks } // NewSQLiteHandler creates a new SQLite handler -func NewSQLiteHandler(db rqlite.Client, homeNodeManager *deployments.HomeNodeManager, logger *zap.Logger) *SQLiteHandler { - // Use user's home directory for cross-platform compatibility - homeDir, err := os.UserHomeDir() - if err != nil { - logger.Error("Failed to get user home directory", zap.Error(err)) - homeDir = os.Getenv("HOME") - } +// dataDir: Base directory for node-local data (if empty, defaults to ~/.orama) +// nodeID: The node's peer ID for affinity checks (can be empty for single-node setups) +func NewSQLiteHandler(db rqlite.Client, homeNodeManager *deployments.HomeNodeManager, logger *zap.Logger, dataDir string, nodeID string) *SQLiteHandler { + var basePath string - basePath := filepath.Join(homeDir, ".orama", "sqlite") + if dataDir != "" { + basePath = filepath.Join(dataDir, "sqlite") + } else { + // Use user's home directory for cross-platform compatibility + homeDir, err := os.UserHomeDir() + if err != nil { + logger.Error("Failed to get user home directory", zap.Error(err)) + homeDir = os.Getenv("HOME") + } + basePath = filepath.Join(homeDir, ".orama", "sqlite") + } return &SQLiteHandler{ db: db, homeNodeManager: homeNodeManager, logger: logger, basePath: basePath, + currentNodeID: nodeID, } } diff --git a/pkg/gateway/handlers/sqlite/handlers_test.go b/pkg/gateway/handlers/sqlite/handlers_test.go index 3fafe28..8209de5 100644 --- a/pkg/gateway/handlers/sqlite/handlers_test.go +++ b/pkg/gateway/handlers/sqlite/handlers_test.go @@ -210,7 +210,7 @@ func TestCreateDatabase_Success(t *testing.T) { portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop()) homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop()) - handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop()) + handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "") handler.basePath = tmpDir reqBody := map[string]string{ @@ -291,7 +291,7 @@ func TestCreateDatabase_DuplicateName(t *testing.T) { portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop()) homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop()) - handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop()) + handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "") handler.basePath = tmpDir reqBody := map[string]string{ @@ -320,7 +320,7 @@ func TestCreateDatabase_InvalidName(t *testing.T) { portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop()) homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop()) - handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop()) + handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "") handler.basePath = tmpDir invalidNames := []string{ @@ -363,7 +363,7 @@ func TestListDatabases(t *testing.T) { portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop()) homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop()) - handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop()) + handler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "") req := httptest.NewRequest("GET", "/v1/db/sqlite/list", nil) ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-namespace") @@ -450,7 +450,7 @@ func TestBackupDatabase(t *testing.T) { portAlloc := deployments.NewPortAllocator(mockDB, zap.NewNop()) homeNodeMgr := deployments.NewHomeNodeManager(mockDB, portAlloc, zap.NewNop()) - sqliteHandler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop()) + sqliteHandler := NewSQLiteHandler(mockDB, homeNodeMgr, zap.NewNop(), "", "") backupHandler := NewBackupHandler(sqliteHandler, mockIPFS, zap.NewNop()) diff --git a/pkg/gateway/handlers/sqlite/query_handler.go b/pkg/gateway/handlers/sqlite/query_handler.go index 2c0904c..248a9cd 100644 --- a/pkg/gateway/handlers/sqlite/query_handler.go +++ b/pkg/gateway/handlers/sqlite/query_handler.go @@ -60,11 +60,30 @@ func (h *SQLiteHandler) QueryDatabase(w http.ResponseWriter, r *http.Request) { return } + // Check node affinity - ensure we're on the correct node for this database + homeNodeID, _ := dbMeta["home_node_id"].(string) + if h.currentNodeID != "" && homeNodeID != "" && homeNodeID != h.currentNodeID { + // This request hit the wrong node - the database lives on a different node + w.Header().Set("X-Orama-Home-Node", homeNodeID) + h.logger.Warn("Database query hit wrong node", + zap.String("database", req.DatabaseName), + zap.String("home_node", homeNodeID), + zap.String("current_node", h.currentNodeID), + ) + http.Error(w, "Database is on a different node. Use node-specific URL or wait for routing implementation.", http.StatusMisdirectedRequest) + return + } + filePath := dbMeta["file_path"].(string) // Check if database file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { - http.Error(w, "Database file not found", http.StatusNotFound) + h.logger.Error("Database file not found on filesystem", + zap.String("path", filePath), + zap.String("namespace", namespace), + zap.String("database", req.DatabaseName), + ) + http.Error(w, "Database file not found on this node", http.StatusNotFound) return } diff --git a/pkg/gateway/routes.go b/pkg/gateway/routes.go index af4aaca..222ee6d 100644 --- a/pkg/gateway/routes.go +++ b/pkg/gateway/routes.go @@ -89,6 +89,16 @@ func (g *Gateway) Routes() http.Handler { mux.HandleFunc("/v1/deployments/nextjs/upload", g.nextjsHandler.HandleUpload) mux.HandleFunc("/v1/deployments/nextjs/update", g.updateHandler.HandleUpdate) + // Go backend deployments + if g.goHandler != nil { + mux.HandleFunc("/v1/deployments/go/upload", g.goHandler.HandleUpload) + } + + // Node.js backend deployments + if g.nodejsHandler != nil { + mux.HandleFunc("/v1/deployments/nodejs/upload", g.nodejsHandler.HandleUpload) + } + // Deployment management mux.HandleFunc("/v1/deployments/list", g.listHandler.HandleList) mux.HandleFunc("/v1/deployments/get", g.listHandler.HandleGet)