package deployments import ( "encoding/json" "net/http" "os" "path/filepath" "time" "github.com/DeBrosOfficial/network/pkg/deployments" "github.com/DeBrosOfficial/network/pkg/deployments/process" "github.com/DeBrosOfficial/network/pkg/ipfs" "go.uber.org/zap" ) // ListHandler handles listing deployments type ListHandler struct { service *DeploymentService processManager *process.Manager ipfsClient ipfs.IPFSClient logger *zap.Logger baseDeployPath string } // NewListHandler creates a new list handler func NewListHandler(service *DeploymentService, processManager *process.Manager, ipfsClient ipfs.IPFSClient, logger *zap.Logger, baseDeployPath string) *ListHandler { return &ListHandler{ service: service, processManager: processManager, ipfsClient: ipfsClient, logger: logger, baseDeployPath: baseDeployPath, } } // HandleList lists all deployments for a namespace func (h *ListHandler) HandleList(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 } type deploymentRow struct { ID string `db:"id"` Namespace string `db:"namespace"` Name string `db:"name"` Type string `db:"type"` Version int `db:"version"` Status string `db:"status"` ContentCID string `db:"content_cid"` HomeNodeID string `db:"home_node_id"` Port int `db:"port"` Subdomain string `db:"subdomain"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } var rows []deploymentRow query := ` SELECT id, namespace, name, type, version, status, content_cid, home_node_id, port, subdomain, created_at, updated_at FROM deployments WHERE namespace = ? ORDER BY created_at DESC ` err := h.service.db.Query(ctx, &rows, query, namespace) if err != nil { h.logger.Error("Failed to query deployments", zap.Error(err)) http.Error(w, "Failed to query deployments", http.StatusInternalServerError) return } baseDomain := h.service.BaseDomain() deployments := make([]map[string]interface{}, len(rows)) for i, row := range rows { shortNodeID := GetShortNodeID(row.HomeNodeID) urls := []string{ "https://" + row.Name + "." + shortNodeID + "." + baseDomain, } if row.Subdomain != "" { urls = append(urls, "https://"+row.Subdomain+"."+baseDomain) } deployments[i] = map[string]interface{}{ "id": row.ID, "namespace": row.Namespace, "name": row.Name, "type": row.Type, "version": row.Version, "status": row.Status, "content_cid": row.ContentCID, "home_node_id": row.HomeNodeID, "port": row.Port, "subdomain": row.Subdomain, "urls": urls, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt, } } resp := map[string]interface{}{ "namespace": namespace, "deployments": deployments, "total": len(deployments), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // HandleGet gets a specific deployment func (h *ListHandler) HandleGet(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 } // Support both 'name' and 'id' query parameters name := r.URL.Query().Get("name") id := r.URL.Query().Get("id") if name == "" && id == "" { http.Error(w, "name or id query parameter is required", http.StatusBadRequest) return } var deployment *deployments.Deployment var err error if id != "" { deployment, err = h.service.GetDeploymentByID(ctx, namespace, id) } else { deployment, err = h.service.GetDeployment(ctx, namespace, name) } if err != nil { if err == deployments.ErrDeploymentNotFound { http.Error(w, "Deployment not found", http.StatusNotFound) } else { h.logger.Error("Failed to get deployment", zap.Error(err)) http.Error(w, "Failed to get deployment", http.StatusInternalServerError) } return } urls := h.service.BuildDeploymentURLs(deployment) resp := map[string]interface{}{ "id": deployment.ID, "namespace": deployment.Namespace, "name": deployment.Name, "type": deployment.Type, "version": deployment.Version, "status": deployment.Status, "content_cid": deployment.ContentCID, "build_cid": deployment.BuildCID, "home_node_id": deployment.HomeNodeID, "port": deployment.Port, "subdomain": deployment.Subdomain, "urls": urls, "memory_limit_mb": deployment.MemoryLimitMB, "cpu_limit_percent": deployment.CPULimitPercent, "disk_limit_mb": deployment.DiskLimitMB, "health_check_path": deployment.HealthCheckPath, "health_check_interval": deployment.HealthCheckInterval, "restart_policy": deployment.RestartPolicy, "max_restart_count": deployment.MaxRestartCount, "created_at": deployment.CreatedAt, "updated_at": deployment.UpdatedAt, "deployed_by": deployment.DeployedBy, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // HandleDelete deletes a deployment func (h *ListHandler) HandleDelete(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 } // Support both 'name' and 'id' query parameters name := r.URL.Query().Get("name") id := r.URL.Query().Get("id") if name == "" && id == "" { http.Error(w, "name or id query parameter is required", http.StatusBadRequest) return } h.logger.Info("Deleting deployment", zap.String("namespace", namespace), zap.String("name", name), zap.String("id", id), ) // Get deployment var deployment *deployments.Deployment var err error if id != "" { deployment, err = h.service.GetDeploymentByID(ctx, namespace, id) } else { deployment, err = h.service.GetDeployment(ctx, namespace, name) } if err != nil { if err == deployments.ErrDeploymentNotFound { http.Error(w, "Deployment not found", http.StatusNotFound) } else { http.Error(w, "Failed to get deployment", http.StatusInternalServerError) } return } // 0. Fan out teardown to replica nodes (before local cleanup so replicas can stop processes) h.service.FanOutToReplicas(ctx, deployment, "/v1/internal/deployments/replica/teardown", nil) // 1. Stop systemd service if err := h.processManager.Stop(ctx, deployment); err != nil { h.logger.Warn("Failed to stop deployment service (may not exist)", zap.Error(err), zap.String("name", deployment.Name)) } // 2. Remove deployment files from disk if h.baseDeployPath != "" { deployDir := filepath.Join(h.baseDeployPath, deployment.Namespace, deployment.Name) if err := os.RemoveAll(deployDir); err != nil { h.logger.Warn("Failed to remove deployment files", zap.Error(err), zap.String("path", deployDir)) } } // 3. Unpin IPFS content if deployment.ContentCID != "" { if err := h.ipfsClient.Unpin(ctx, deployment.ContentCID); err != nil { h.logger.Warn("Failed to unpin IPFS content", zap.Error(err), zap.String("cid", deployment.ContentCID)) } } // 4. Delete subdomain registry subdomainQuery := `DELETE FROM global_deployment_subdomains WHERE deployment_id = ?` _, _ = h.service.db.Exec(ctx, subdomainQuery, deployment.ID) // 5. Delete DNS records dnsQuery := `DELETE FROM dns_records WHERE deployment_id = ?` _, _ = h.service.db.Exec(ctx, dnsQuery, deployment.ID) // 6. Delete deployment record query := `DELETE FROM deployments WHERE namespace = ? AND name = ?` _, err = h.service.db.Exec(ctx, query, namespace, deployment.Name) if err != nil { h.logger.Error("Failed to delete deployment", zap.Error(err)) http.Error(w, "Failed to delete deployment", http.StatusInternalServerError) return } h.logger.Info("Deployment deleted", zap.String("id", deployment.ID), zap.String("namespace", namespace), zap.String("name", name), ) resp := map[string]interface{}{ "message": "Deployment deleted successfully", "name": name, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }