mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 03:33:01 +00:00
Bug fixing
This commit is contained in:
parent
83804422c4
commit
4f1709e136
@ -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)
|
||||
|
||||
@ -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 <namespace>",
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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 <namespace_name>\n")
|
||||
@ -48,12 +50,14 @@ func showNamespaceHelp() {
|
||||
fmt.Printf("Namespace Management Commands\n\n")
|
||||
fmt.Printf("Usage: orama namespace <subcommand>\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 <namespace> - 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")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
91
pkg/gateway/handlers/namespace/list_handler.go
Normal file
91
pkg/gateway/handlers/namespace/list_handler.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user