diff --git a/pkg/auth/enhanced_auth.go b/pkg/auth/enhanced_auth.go index 5efed25..fc5de13 100644 --- a/pkg/auth/enhanced_auth.go +++ b/pkg/auth/enhanced_auth.go @@ -218,6 +218,37 @@ func (store *EnhancedCredentialStore) SetDefaultCredential(gatewayURL string, in return true } +// RemoveCredentialByNamespace removes the credential for a specific namespace from a gateway. +// Returns true if a credential was removed. +func (store *EnhancedCredentialStore) RemoveCredentialByNamespace(gatewayURL, namespace string) bool { + gwCreds := store.Gateways[gatewayURL] + if gwCreds == nil || len(gwCreds.Credentials) == 0 { + return false + } + + for i, cred := range gwCreds.Credentials { + if cred.Namespace == namespace { + // Remove this credential from the slice + gwCreds.Credentials = append(gwCreds.Credentials[:i], gwCreds.Credentials[i+1:]...) + + // Fix indices if they now point beyond the slice + if len(gwCreds.Credentials) == 0 { + gwCreds.DefaultIndex = 0 + gwCreds.LastUsedIndex = 0 + } else { + if gwCreds.DefaultIndex >= len(gwCreds.Credentials) { + gwCreds.DefaultIndex = len(gwCreds.Credentials) - 1 + } + if gwCreds.LastUsedIndex >= len(gwCreds.Credentials) { + gwCreds.LastUsedIndex = gwCreds.DefaultIndex + } + } + return true + } + } + return false +} + // ClearAllCredentials removes all credentials func (store *EnhancedCredentialStore) ClearAllCredentials() { store.Gateways = make(map[string]*GatewayCredentials) diff --git a/pkg/cli/cmd/namespacecmd/namespace.go b/pkg/cli/cmd/namespacecmd/namespace.go index 7b9afb1..1807f74 100644 --- a/pkg/cli/cmd/namespacecmd/namespace.go +++ b/pkg/cli/cmd/namespacecmd/namespace.go @@ -27,6 +27,15 @@ var deleteCmd = &cobra.Command{ }, } +var listCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List namespaces owned by the current wallet", + Run: func(cmd *cobra.Command, args []string) { + cli.HandleNamespaceCommand([]string{"list"}) + }, +} + var repairCmd = &cobra.Command{ Use: "repair ", Short: "Repair an under-provisioned namespace cluster", @@ -39,6 +48,7 @@ var repairCmd = &cobra.Command{ func init() { deleteCmd.Flags().Bool("force", false, "Skip confirmation prompt") + Cmd.AddCommand(listCmd) Cmd.AddCommand(deleteCmd) Cmd.AddCommand(repairCmd) } diff --git a/pkg/cli/namespace_commands.go b/pkg/cli/namespace_commands.go index 73d3680..7ed8c7f 100644 --- a/pkg/cli/namespace_commands.go +++ b/pkg/cli/namespace_commands.go @@ -29,6 +29,8 @@ func HandleNamespaceCommand(args []string) { fs.BoolVar(&force, "force", false, "Skip confirmation prompt") _ = fs.Parse(args[1:]) handleNamespaceDelete(force) + case "list": + handleNamespaceList() case "repair": if len(args) < 2 { fmt.Fprintf(os.Stderr, "Usage: orama namespace repair \n") @@ -48,12 +50,14 @@ func showNamespaceHelp() { fmt.Printf("Namespace Management Commands\n\n") fmt.Printf("Usage: orama namespace \n\n") fmt.Printf("Subcommands:\n") + fmt.Printf(" list - List namespaces owned by the current wallet\n") fmt.Printf(" delete - Delete the current namespace and all its resources\n") fmt.Printf(" repair - Repair an under-provisioned namespace cluster (add missing nodes)\n") fmt.Printf(" help - Show this help message\n\n") fmt.Printf("Flags:\n") fmt.Printf(" --force - Skip confirmation prompt (delete only)\n\n") fmt.Printf("Examples:\n") + fmt.Printf(" orama namespace list\n") fmt.Printf(" orama namespace delete\n") fmt.Printf(" orama namespace delete --force\n") fmt.Printf(" orama namespace repair anchat\n") @@ -122,10 +126,12 @@ func handleNamespaceDelete(force bool) { // Confirm deletion if !force { fmt.Printf("This will permanently delete namespace '%s' and all its resources:\n", namespace) + fmt.Printf(" - All deployments and their processes\n") fmt.Printf(" - RQLite cluster (3 nodes)\n") fmt.Printf(" - Olric cache cluster (3 nodes)\n") fmt.Printf(" - Gateway instances\n") - fmt.Printf(" - API keys and credentials\n\n") + fmt.Printf(" - API keys and credentials\n") + fmt.Printf(" - IPFS content and DNS records\n\n") fmt.Printf("Type the namespace name to confirm: ") scanner := bufio.NewScanner(os.Stdin) @@ -174,5 +180,88 @@ func handleNamespaceDelete(force bool) { } fmt.Printf("Namespace '%s' deleted successfully.\n", namespace) + + // Clean up local credentials for the deleted namespace + if store.RemoveCredentialByNamespace(gatewayURL, namespace) { + if err := store.Save(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to clean up local credentials: %v\n", err) + } else { + fmt.Printf("Local credentials for '%s' cleared.\n", namespace) + } + } + fmt.Printf("Run 'orama auth login' to create a new namespace.\n") } + +func handleNamespaceList() { + // Load credentials + store, err := auth.LoadEnhancedCredentials() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load credentials: %v\n", err) + os.Exit(1) + } + + gatewayURL := getGatewayURL() + creds := store.GetDefaultCredential(gatewayURL) + + if creds == nil || !creds.IsValid() { + fmt.Fprintf(os.Stderr, "Not authenticated. Run 'orama auth login' first.\n") + os.Exit(1) + } + + // Make GET request to namespace list endpoint + url := fmt.Sprintf("%s/v1/namespace/list", gatewayURL) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create request: %v\n", err) + os.Exit(1) + } + req.Header.Set("Authorization", "Bearer "+creds.APIKey) + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to connect to gateway: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + + if resp.StatusCode != http.StatusOK { + errMsg := "unknown error" + if e, ok := result["error"].(string); ok { + errMsg = e + } + fmt.Fprintf(os.Stderr, "Failed to list namespaces: %s\n", errMsg) + os.Exit(1) + } + + namespaces, _ := result["namespaces"].([]interface{}) + if len(namespaces) == 0 { + fmt.Println("No namespaces found.") + return + } + + activeNS := creds.Namespace + + fmt.Printf("Namespaces (%d):\n\n", len(namespaces)) + for _, ns := range namespaces { + nsMap, _ := ns.(map[string]interface{}) + name, _ := nsMap["name"].(string) + status, _ := nsMap["cluster_status"].(string) + + marker := " " + if name == activeNS { + marker = "* " + } + + fmt.Printf("%s%-20s cluster: %s\n", marker, name, status) + } + fmt.Printf("\n* = active namespace\n") +} diff --git a/pkg/environments/production/installers/utils.go b/pkg/environments/production/installers/utils.go index a76e694..dfd0e5c 100644 --- a/pkg/environments/production/installers/utils.go +++ b/pkg/environments/production/installers/utils.go @@ -10,7 +10,7 @@ import ( // DownloadFile downloads a file from a URL to a destination path func DownloadFile(url, dest string) error { - cmd := exec.Command("wget", "-q", url, "-O", dest) + cmd := exec.Command("wget", "-q", "-4", url, "-O", dest) if err := cmd.Run(); err != nil { return fmt.Errorf("download failed: %w", err) } diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index 9223d56..b50930d 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -149,6 +149,28 @@ func (ps *ProductionSetup) IsAnyoneClient() bool { return ps.isAnyoneClient } +// disableConflictingAnyoneService stops, disables, and removes a conflicting +// Anyone service file. A node must run either relay or client, never both. +// This is best-effort: errors are logged but do not abort the operation. +func (ps *ProductionSetup) disableConflictingAnyoneService(serviceName string) { + unitPath := filepath.Join("/etc/systemd/system", serviceName) + if _, err := os.Stat(unitPath); os.IsNotExist(err) { + return // Nothing to clean up + } + + ps.logf(" Removing conflicting Anyone service: %s", serviceName) + + if err := ps.serviceController.StopService(serviceName); err != nil { + ps.logf(" ⚠️ Warning: failed to stop %s: %v", serviceName, err) + } + if err := ps.serviceController.DisableService(serviceName); err != nil { + ps.logf(" ⚠️ Warning: failed to disable %s: %v", serviceName, err) + } + if err := ps.serviceController.RemoveServiceUnit(serviceName); err != nil { + ps.logf(" ⚠️ Warning: failed to remove %s: %v", serviceName, err) + } +} + // Phase1CheckPrerequisites performs initial environment validation func (ps *ProductionSetup) Phase1CheckPrerequisites() error { ps.logf("Phase 1: Checking prerequisites...") @@ -605,18 +627,27 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(enableHTTPS bool) error { ps.logf(" ✓ Node service created: orama-node.service (with embedded gateway)") // Anyone Relay service (only created when --anyone-relay flag is used) + // A node must run EITHER relay OR client, never both. When writing one + // mode's service, we remove the other to prevent conflicts (they share + // the same anon binary and would fight over ports). if ps.IsAnyoneRelay() { anyoneUnit := ps.serviceGenerator.GenerateAnyoneRelayService() if err := ps.serviceController.WriteServiceUnit("orama-anyone-relay.service", anyoneUnit); err != nil { return fmt.Errorf("failed to write Anyone Relay service: %w", err) } ps.logf(" ✓ Anyone Relay service created (operator mode, ORPort: %d)", ps.anyoneRelayConfig.ORPort) + ps.disableConflictingAnyoneService("orama-anyone-client.service") } else if ps.IsAnyoneClient() { anyoneUnit := ps.serviceGenerator.GenerateAnyoneClientService() if err := ps.serviceController.WriteServiceUnit("orama-anyone-client.service", anyoneUnit); err != nil { return fmt.Errorf("failed to write Anyone client service: %w", err) } ps.logf(" ✓ Anyone client service created (SocksPort 9050)") + ps.disableConflictingAnyoneService("orama-anyone-relay.service") + } else { + // Neither mode configured — clean up both + ps.disableConflictingAnyoneService("orama-anyone-client.service") + ps.disableConflictingAnyoneService("orama-anyone-relay.service") } // CoreDNS service (only for nameserver nodes) diff --git a/pkg/environments/production/services.go b/pkg/environments/production/services.go index 459f68b..0070cb9 100644 --- a/pkg/environments/production/services.go +++ b/pkg/environments/production/services.go @@ -433,6 +433,24 @@ func (sc *SystemdController) StopService(name string) error { return nil } +// DisableService disables a service from starting on boot +func (sc *SystemdController) DisableService(name string) error { + cmd := exec.Command("systemctl", "disable", name) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to disable service %s: %w", name, err) + } + return nil +} + +// RemoveServiceUnit removes a systemd unit file from disk +func (sc *SystemdController) RemoveServiceUnit(name string) error { + unitPath := filepath.Join(sc.systemdDir, name) + if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove unit file %s: %w", name, err) + } + return nil +} + // StatusService gets the status of a service func (sc *SystemdController) StatusService(name string) (bool, error) { cmd := exec.Command("systemctl", "is-active", "--quiet", name) diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 7194bb8..9fd3a05 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -136,6 +136,9 @@ type Gateway struct { // Namespace delete handler namespaceDeleteHandler http.Handler + // Namespace list handler + namespaceListHandler http.Handler + // Peer discovery for namespace gateways (libp2p mesh formation) peerDiscovery *PeerDiscovery @@ -630,11 +633,21 @@ func (g *Gateway) SetNamespaceDeleteHandler(h http.Handler) { g.namespaceDeleteHandler = h } +// SetNamespaceListHandler sets the handler for namespace list requests. +func (g *Gateway) SetNamespaceListHandler(h http.Handler) { + g.namespaceListHandler = h +} + // GetORMClient returns the RQLite ORM client for external use (e.g., by ClusterManager) func (g *Gateway) GetORMClient() rqlite.Client { return g.ormClient } +// GetIPFSClient returns the IPFS client for external use (e.g., by namespace delete handler) +func (g *Gateway) GetIPFSClient() ipfs.IPFSClient { + return g.ipfsClient +} + // setOlricClient atomically sets the Olric client and reinitializes cache handlers. func (g *Gateway) setOlricClient(client *olric.Client) { g.olricMu.Lock() diff --git a/pkg/gateway/handlers/namespace/delete_handler.go b/pkg/gateway/handlers/namespace/delete_handler.go index 9aae838..f3418f3 100644 --- a/pkg/gateway/handlers/namespace/delete_handler.go +++ b/pkg/gateway/handlers/namespace/delete_handler.go @@ -3,9 +3,11 @@ package namespace import ( "context" "encoding/json" + "fmt" "net/http" "github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys" + "github.com/DeBrosOfficial/network/pkg/ipfs" "github.com/DeBrosOfficial/network/pkg/rqlite" "go.uber.org/zap" ) @@ -19,14 +21,16 @@ type NamespaceDeprovisioner interface { type DeleteHandler struct { deprovisioner NamespaceDeprovisioner ormClient rqlite.Client + ipfsClient ipfs.IPFSClient // can be nil logger *zap.Logger } // NewDeleteHandler creates a new delete handler -func NewDeleteHandler(dp NamespaceDeprovisioner, orm rqlite.Client, logger *zap.Logger) *DeleteHandler { +func NewDeleteHandler(dp NamespaceDeprovisioner, orm rqlite.Client, ipfsClient ipfs.IPFSClient, logger *zap.Logger) *DeleteHandler { return &DeleteHandler{ deprovisioner: dp, ormClient: orm, + ipfsClient: ipfsClient, logger: logger.With(zap.String("component", "namespace-delete-handler")), } } @@ -80,14 +84,20 @@ func (h *DeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { zap.Int64("namespace_id", namespaceID), ) - // Deprovision the cluster (stops processes, deallocates ports, deletes DB records) + // 1. Deprovision the cluster (stops infra + deployment processes, deallocates ports, deletes DNS) if err := h.deprovisioner.DeprovisionCluster(r.Context(), namespaceID); err != nil { h.logger.Error("Failed to deprovision cluster", zap.Error(err)) writeDeleteResponse(w, http.StatusInternalServerError, map[string]interface{}{"error": err.Error()}) return } - // Delete API keys, ownership records, and namespace record + // 2. Unpin IPFS content (must run before global table cleanup to read CID list) + h.unpinNamespaceContent(r.Context(), ns) + + // 3. Clean up global tables that use namespace TEXT (not FK cascade) + h.cleanupGlobalTables(r.Context(), ns) + + // 4. Delete API keys, ownership records, and namespace record (FK cascade handles children) h.ormClient.Exec(r.Context(), "DELETE FROM wallet_api_keys WHERE namespace_id = ?", namespaceID) h.ormClient.Exec(r.Context(), "DELETE FROM api_keys WHERE namespace_id = ?", namespaceID) h.ormClient.Exec(r.Context(), "DELETE FROM namespace_ownership WHERE namespace_id = ?", namespaceID) @@ -101,6 +111,71 @@ func (h *DeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) } +// unpinNamespaceContent unpins all IPFS content owned by the namespace. +// Best-effort: individual failures are logged but do not abort deletion. +func (h *DeleteHandler) unpinNamespaceContent(ctx context.Context, ns string) { + if h.ipfsClient == nil { + h.logger.Debug("IPFS client not available, skipping IPFS cleanup") + return + } + + type cidRow struct { + CID string `db:"cid"` + } + var rows []cidRow + if err := h.ormClient.Query(ctx, &rows, + "SELECT cid FROM ipfs_content_ownership WHERE namespace = ?", ns); err != nil { + h.logger.Warn("Failed to query IPFS content for namespace", + zap.String("namespace", ns), zap.Error(err)) + return + } + + if len(rows) == 0 { + return + } + + h.logger.Info("Unpinning IPFS content for namespace", + zap.String("namespace", ns), + zap.Int("cid_count", len(rows))) + + for _, row := range rows { + if err := h.ipfsClient.Unpin(ctx, row.CID); err != nil { + h.logger.Warn("Failed to unpin CID (best-effort)", + zap.String("cid", row.CID), + zap.String("namespace", ns), + zap.Error(err)) + } + } +} + +// cleanupGlobalTables deletes orphaned records from global tables that reference +// the namespace by TEXT name (not by integer FK, so CASCADE doesn't help). +// Best-effort: individual failures are logged but do not abort deletion. +func (h *DeleteHandler) cleanupGlobalTables(ctx context.Context, ns string) { + tables := []struct { + table string + column string + }{ + {"global_deployment_subdomains", "namespace"}, + {"ipfs_content_ownership", "namespace"}, + {"functions", "namespace"}, + {"function_secrets", "namespace"}, + {"namespace_sqlite_databases", "namespace"}, + {"namespace_quotas", "namespace"}, + {"home_node_assignments", "namespace"}, + } + + for _, t := range tables { + query := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", t.table, t.column) + if _, err := h.ormClient.Exec(ctx, query, ns); err != nil { + h.logger.Warn("Failed to clean up global table (best-effort)", + zap.String("table", t.table), + zap.String("namespace", ns), + zap.Error(err)) + } + } +} + func writeDeleteResponse(w http.ResponseWriter, status int, resp map[string]interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/pkg/gateway/handlers/namespace/list_handler.go b/pkg/gateway/handlers/namespace/list_handler.go new file mode 100644 index 0000000..19b955a --- /dev/null +++ b/pkg/gateway/handlers/namespace/list_handler.go @@ -0,0 +1,91 @@ +package namespace + +import ( + "encoding/json" + "net/http" + + "github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys" + "github.com/DeBrosOfficial/network/pkg/rqlite" + "go.uber.org/zap" +) + +// ListHandler handles namespace list requests +type ListHandler struct { + ormClient rqlite.Client + logger *zap.Logger +} + +// NewListHandler creates a new namespace list handler +func NewListHandler(orm rqlite.Client, logger *zap.Logger) *ListHandler { + return &ListHandler{ + ormClient: orm, + logger: logger.With(zap.String("component", "namespace-list-handler")), + } +} + +// ServeHTTP handles GET /v1/namespace/list +func (h *ListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeListResponse(w, http.StatusMethodNotAllowed, map[string]interface{}{"error": "method not allowed"}) + return + } + + // Get current namespace from auth context + ns := "" + if v := r.Context().Value(ctxkeys.NamespaceOverride); v != nil { + if s, ok := v.(string); ok { + ns = s + } + } + if ns == "" { + writeListResponse(w, http.StatusUnauthorized, map[string]interface{}{"error": "not authenticated"}) + return + } + + // Look up the owner wallet from the current namespace + type ownerRow struct { + OwnerID string `db:"owner_id"` + } + var owners []ownerRow + if err := h.ormClient.Query(r.Context(), &owners, + `SELECT owner_id FROM namespace_ownership + WHERE namespace_id = (SELECT id FROM namespaces WHERE name = ? LIMIT 1) + LIMIT 1`, ns); err != nil || len(owners) == 0 { + h.logger.Warn("Failed to resolve namespace owner", + zap.String("namespace", ns), zap.Error(err)) + writeListResponse(w, http.StatusInternalServerError, map[string]interface{}{"error": "failed to resolve namespace owner"}) + return + } + + ownerID := owners[0].OwnerID + + // Query all namespaces owned by this wallet + type nsRow struct { + Name string `db:"name" json:"name"` + CreatedAt string `db:"created_at" json:"created_at"` + ClusterStatus string `db:"cluster_status" json:"cluster_status"` + } + var namespaces []nsRow + if err := h.ormClient.Query(r.Context(), &namespaces, + `SELECT n.name, n.created_at, COALESCE(nc.status, 'none') as cluster_status + FROM namespaces n + JOIN namespace_ownership no2 ON no2.namespace_id = n.id + LEFT JOIN namespace_clusters nc ON nc.namespace_id = n.id + WHERE no2.owner_id = ? + ORDER BY n.created_at DESC`, ownerID); err != nil { + h.logger.Error("Failed to list namespaces", zap.Error(err)) + writeListResponse(w, http.StatusInternalServerError, map[string]interface{}{"error": "failed to list namespaces"}) + return + } + + writeListResponse(w, http.StatusOK, map[string]interface{}{ + "namespaces": namespaces, + "count": len(namespaces), + }) +} + +func writeListResponse(w http.ResponseWriter, status int, resp map[string]interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(resp) +} diff --git a/pkg/gateway/routes.go b/pkg/gateway/routes.go index bc9f805..19530f6 100644 --- a/pkg/gateway/routes.go +++ b/pkg/gateway/routes.go @@ -81,6 +81,11 @@ func (g *Gateway) Routes() http.Handler { mux.Handle("/v1/namespace/delete", g.namespaceDeleteHandler) } + // namespace list (authenticated — lists namespaces owned by the current wallet) + if g.namespaceListHandler != nil { + mux.Handle("/v1/namespace/list", g.namespaceListHandler) + } + // network mux.HandleFunc("/v1/network/status", g.networkStatusHandler) mux.HandleFunc("/v1/network/peers", g.networkPeersHandler) diff --git a/pkg/namespace/systemd_spawner.go b/pkg/namespace/systemd_spawner.go index 6392cb4..e873b7e 100644 --- a/pkg/namespace/systemd_spawner.go +++ b/pkg/namespace/systemd_spawner.go @@ -306,11 +306,15 @@ func (s *SystemdSpawner) SaveClusterState(namespace string, data []byte) error { return nil } -// StopAll stops all services for a namespace +// StopAll stops all services for a namespace, including deployment processes func (s *SystemdSpawner) StopAll(ctx context.Context, namespace string) error { s.logger.Info("Stopping all namespace services via systemd", zap.String("namespace", namespace)) + // Stop deployment processes first (they depend on the cluster services) + s.systemdMgr.StopDeploymentServicesForNamespace(namespace) + + // Then stop infrastructure services (Gateway → Olric → RQLite) return s.systemdMgr.StopAllNamespaceServices(namespace) } diff --git a/pkg/node/gateway.go b/pkg/node/gateway.go index afbded4..5ef9807 100644 --- a/pkg/node/gateway.go +++ b/pkg/node/gateway.go @@ -88,10 +88,14 @@ func (n *Node) startHTTPGateway(ctx context.Context) error { spawnHandler := namespacehandlers.NewSpawnHandler(systemdSpawner, n.logger.Logger) apiGateway.SetSpawnHandler(spawnHandler) - // Wire namespace delete handler - deleteHandler := namespacehandlers.NewDeleteHandler(clusterManager, ormClient, n.logger.Logger) + // Wire namespace delete handler (with IPFS client for content unpinning) + deleteHandler := namespacehandlers.NewDeleteHandler(clusterManager, ormClient, apiGateway.GetIPFSClient(), n.logger.Logger) apiGateway.SetNamespaceDeleteHandler(deleteHandler) + // Wire namespace list handler + nsListHandler := namespacehandlers.NewListHandler(ormClient, n.logger.Logger) + apiGateway.SetNamespaceListHandler(nsListHandler) + n.logger.ComponentInfo(logging.ComponentNode, "Namespace cluster provisioning enabled", zap.String("base_domain", clusterCfg.BaseDomain), zap.String("base_data_dir", baseDataDir)) diff --git a/pkg/systemd/manager.go b/pkg/systemd/manager.go index 3ac0412..384eb7c 100644 --- a/pkg/systemd/manager.go +++ b/pkg/systemd/manager.go @@ -273,6 +273,79 @@ func (m *Manager) StopAllNamespaceServicesGlobally() error { return nil } +// StopDeploymentServicesForNamespace stops all deployment systemd units for a given namespace. +// Deployment units follow the naming pattern: orama-deploy-{namespace}-{name}.service +// (with dots replaced by hyphens, matching process/manager.go:getServiceName). +// This is best-effort: individual failures are logged but do not abort the operation. +func (m *Manager) StopDeploymentServicesForNamespace(namespace string) { + // Match the sanitization from deployments/process/manager.go:getServiceName + sanitizedNS := strings.ReplaceAll(namespace, ".", "-") + pattern := fmt.Sprintf("orama-deploy-%s-*", sanitizedNS) + + m.logger.Info("Stopping deployment services for namespace", + zap.String("namespace", namespace), + zap.String("pattern", pattern)) + + cmd := exec.Command("systemctl", "list-units", "--type=service", "--all", "--no-pager", "--no-legend", pattern) + output, err := cmd.CombinedOutput() + if err != nil { + m.logger.Warn("Failed to list deployment services", + zap.String("namespace", namespace), + zap.Error(err)) + return + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + stopped := 0 + for _, line := range lines { + if line == "" { + continue + } + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + svc := fields[0] + + // Stop the service + if stopOut, stopErr := exec.Command("systemctl", "stop", svc).CombinedOutput(); stopErr != nil { + m.logger.Warn("Failed to stop deployment service", + zap.String("service", svc), + zap.Error(stopErr), + zap.String("output", string(stopOut))) + } + + // Disable the service + if disOut, disErr := exec.Command("systemctl", "disable", svc).CombinedOutput(); disErr != nil { + m.logger.Warn("Failed to disable deployment service", + zap.String("service", svc), + zap.Error(disErr), + zap.String("output", string(disOut))) + } + + // Remove the service file + serviceFile := filepath.Join(m.systemdDir, svc) + if !strings.HasSuffix(serviceFile, ".service") { + serviceFile += ".service" + } + if rmErr := os.Remove(serviceFile); rmErr != nil && !os.IsNotExist(rmErr) { + m.logger.Warn("Failed to remove deployment service file", + zap.String("file", serviceFile), + zap.Error(rmErr)) + } + + stopped++ + m.logger.Info("Stopped deployment service", zap.String("service", svc)) + } + + if stopped > 0 { + m.ReloadDaemon() + m.logger.Info("Deployment services cleanup complete", + zap.String("namespace", namespace), + zap.Int("stopped", stopped)) + } +} + // CleanupOrphanedProcesses finds and kills any orphaned namespace processes not managed by systemd // This is for cleaning up after migration from old exec.Command approach func (m *Manager) CleanupOrphanedProcesses() error { diff --git a/systemd/orama-namespace-olric@.service b/systemd/orama-namespace-olric@.service index c8beb0d..a0b3d97 100644 --- a/systemd/orama-namespace-olric@.service +++ b/systemd/orama-namespace-olric@.service @@ -12,7 +12,7 @@ WorkingDirectory=/opt/orama # Olric reads config from environment variable (set in env file) EnvironmentFile=/opt/orama/.orama/data/namespaces/%i/olric.env -ExecStart=/opt/orama/bin/olric-server +ExecStart=/usr/local/bin/olric-server TimeoutStopSec=30s KillMode=mixed