Added delete namespace handler

This commit is contained in:
anonpenguin23 2026-01-31 13:13:09 +02:00
parent 16eaf9a129
commit b5109f1ee8
6 changed files with 263 additions and 0 deletions

View File

@ -88,6 +88,10 @@ func main() {
case "db":
cli.HandleDBCommand(args)
// Namespace management
case "namespace":
cli.HandleNamespaceCommand(args)
// Environment management
case "env":
cli.HandleEnvCommand(args)
@ -166,6 +170,9 @@ func showHelp() {
fmt.Printf(" db backup <name> - Backup database to IPFS\n")
fmt.Printf(" db backups <name> - List database backups\n\n")
fmt.Printf("🏢 Namespaces:\n")
fmt.Printf(" namespace delete - Delete current namespace and all resources\n\n")
fmt.Printf("🌍 Environments:\n")
fmt.Printf(" env list - List all environments\n")
fmt.Printf(" env current - Show current environment\n")

View File

@ -0,0 +1,131 @@
package cli
import (
"bufio"
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"strings"
"github.com/DeBrosOfficial/network/pkg/auth"
)
// HandleNamespaceCommand handles namespace management commands
func HandleNamespaceCommand(args []string) {
if len(args) == 0 {
showNamespaceHelp()
return
}
subcommand := args[0]
switch subcommand {
case "delete":
var force bool
fs := flag.NewFlagSet("namespace delete", flag.ExitOnError)
fs.BoolVar(&force, "force", false, "Skip confirmation prompt")
_ = fs.Parse(args[1:])
handleNamespaceDelete(force)
case "help":
showNamespaceHelp()
default:
fmt.Fprintf(os.Stderr, "Unknown namespace command: %s\n", subcommand)
showNamespaceHelp()
os.Exit(1)
}
}
func showNamespaceHelp() {
fmt.Printf("Namespace Management Commands\n\n")
fmt.Printf("Usage: orama namespace <subcommand>\n\n")
fmt.Printf("Subcommands:\n")
fmt.Printf(" delete - Delete the current namespace and all its resources\n")
fmt.Printf(" help - Show this help message\n\n")
fmt.Printf("Flags:\n")
fmt.Printf(" --force - Skip confirmation prompt\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" orama namespace delete\n")
fmt.Printf(" orama namespace delete --force\n")
}
func handleNamespaceDelete(force bool) {
// 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)
}
namespace := creds.Namespace
if namespace == "" || namespace == "default" {
fmt.Fprintf(os.Stderr, "Cannot delete default namespace.\n")
os.Exit(1)
}
// Confirm deletion
if !force {
fmt.Printf("This will permanently delete namespace '%s' and all its resources:\n", namespace)
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("Type the namespace name to confirm: ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
input := strings.TrimSpace(scanner.Text())
if input != namespace {
fmt.Println("Aborted - namespace name did not match.")
os.Exit(1)
}
}
fmt.Printf("Deleting namespace '%s'...\n", namespace)
// Make DELETE request to gateway
url := fmt.Sprintf("%s/v1/namespace/delete", gatewayURL)
req, err := http.NewRequest(http.MethodDelete, 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 delete namespace: %s\n", errMsg)
os.Exit(1)
}
fmt.Printf("Namespace '%s' deleted successfully.\n", namespace)
fmt.Printf("Run 'orama auth login' to create a new namespace.\n")
}

View File

@ -114,6 +114,9 @@ type Gateway struct {
// Namespace instance spawn handler (for distributed provisioning)
spawnHandler http.Handler
// Namespace delete handler
namespaceDeleteHandler http.Handler
}
// localSubscriber represents a WebSocket subscriber for local message delivery
@ -444,6 +447,11 @@ func (g *Gateway) SetSpawnHandler(h http.Handler) {
g.spawnHandler = h
}
// SetNamespaceDeleteHandler sets the handler for namespace deletion requests.
func (g *Gateway) SetNamespaceDeleteHandler(h http.Handler) {
g.namespaceDeleteHandler = h
}
// GetORMClient returns the RQLite ORM client for external use (e.g., by ClusterManager)
func (g *Gateway) GetORMClient() rqlite.Client {
return g.ormClient

View File

@ -0,0 +1,108 @@
package namespace
import (
"context"
"encoding/json"
"net/http"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"github.com/DeBrosOfficial/network/pkg/rqlite"
"go.uber.org/zap"
)
// NamespaceDeprovisioner is the interface for deprovisioning namespace clusters
type NamespaceDeprovisioner interface {
DeprovisionCluster(ctx context.Context, namespaceID int64) error
}
// DeleteHandler handles namespace deletion requests
type DeleteHandler struct {
deprovisioner NamespaceDeprovisioner
ormClient rqlite.Client
logger *zap.Logger
}
// NewDeleteHandler creates a new delete handler
func NewDeleteHandler(dp NamespaceDeprovisioner, orm rqlite.Client, logger *zap.Logger) *DeleteHandler {
return &DeleteHandler{
deprovisioner: dp,
ormClient: orm,
logger: logger.With(zap.String("component", "namespace-delete-handler")),
}
}
// ServeHTTP handles DELETE /v1/namespace/delete
func (h *DeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete && r.Method != http.MethodPost {
writeDeleteResponse(w, http.StatusMethodNotAllowed, map[string]interface{}{"error": "method not allowed"})
return
}
// Get namespace from context (set by auth middleware — already ownership-verified)
ns := ""
if v := r.Context().Value(ctxkeys.NamespaceOverride); v != nil {
if s, ok := v.(string); ok {
ns = s
}
}
if ns == "" || ns == "default" {
writeDeleteResponse(w, http.StatusBadRequest, map[string]interface{}{"error": "cannot delete default namespace"})
return
}
if h.deprovisioner == nil {
writeDeleteResponse(w, http.StatusServiceUnavailable, map[string]interface{}{"error": "cluster provisioning not enabled"})
return
}
// Resolve namespace ID
var rows []map[string]interface{}
if err := h.ormClient.Query(r.Context(), &rows, "SELECT id FROM namespaces WHERE name = ? LIMIT 1", ns); err != nil || len(rows) == 0 {
writeDeleteResponse(w, http.StatusNotFound, map[string]interface{}{"error": "namespace not found"})
return
}
var namespaceID int64
switch v := rows[0]["id"].(type) {
case float64:
namespaceID = int64(v)
case int64:
namespaceID = v
case int:
namespaceID = int64(v)
default:
writeDeleteResponse(w, http.StatusInternalServerError, map[string]interface{}{"error": "invalid namespace ID type"})
return
}
h.logger.Info("Deprovisioning namespace cluster",
zap.String("namespace", ns),
zap.Int64("namespace_id", namespaceID),
)
// Deprovision the cluster (stops processes, deallocates ports, deletes DB records)
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
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)
h.ormClient.Exec(r.Context(), "DELETE FROM namespaces WHERE id = ?", namespaceID)
h.logger.Info("Namespace deleted successfully", zap.String("namespace", ns))
writeDeleteResponse(w, http.StatusOK, map[string]interface{}{
"status": "deleted",
"namespace": ns,
})
}
func writeDeleteResponse(w http.ResponseWriter, status int, resp map[string]interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(resp)
}

View File

@ -67,6 +67,11 @@ func (g *Gateway) Routes() http.Handler {
// namespace cluster status (public endpoint for polling during provisioning)
mux.HandleFunc("/v1/namespace/status", g.namespaceClusterStatusHandler)
// namespace delete (authenticated — goes through auth middleware)
if g.namespaceDeleteHandler != nil {
mux.Handle("/v1/namespace/delete", g.namespaceDeleteHandler)
}
// network
mux.HandleFunc("/v1/network/status", g.networkStatusHandler)
mux.HandleFunc("/v1/network/peers", g.networkPeersHandler)

View File

@ -84,6 +84,10 @@ func (n *Node) startHTTPGateway(ctx context.Context) error {
spawnHandler := namespacehandlers.NewSpawnHandler(rqliteSpawner, olricSpawner, n.logger.Logger)
apiGateway.SetSpawnHandler(spawnHandler)
// Wire namespace delete handler
deleteHandler := namespacehandlers.NewDeleteHandler(clusterManager, ormClient, n.logger.Logger)
apiGateway.SetNamespaceDeleteHandler(deleteHandler)
n.logger.ComponentInfo(logging.ComponentNode, "Namespace cluster provisioning enabled",
zap.String("base_domain", clusterCfg.BaseDomain),
zap.String("base_data_dir", baseDataDir))