diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 7cbcf53..c947477 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -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 - Backup database to IPFS\n") fmt.Printf(" db backups - 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") diff --git a/pkg/cli/namespace_commands.go b/pkg/cli/namespace_commands.go new file mode 100644 index 0000000..137942e --- /dev/null +++ b/pkg/cli/namespace_commands.go @@ -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 \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") +} diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 1074cbc..b31c0ac 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -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 diff --git a/pkg/gateway/handlers/namespace/delete_handler.go b/pkg/gateway/handlers/namespace/delete_handler.go new file mode 100644 index 0000000..9aae838 --- /dev/null +++ b/pkg/gateway/handlers/namespace/delete_handler.go @@ -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) +} diff --git a/pkg/gateway/routes.go b/pkg/gateway/routes.go index f2f6820..4885c3b 100644 --- a/pkg/gateway/routes.go +++ b/pkg/gateway/routes.go @@ -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) diff --git a/pkg/node/gateway.go b/pkg/node/gateway.go index b3da3ad..d4abe1a 100644 --- a/pkg/node/gateway.go +++ b/pkg/node/gateway.go @@ -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))