diff --git a/core/pkg/cli/cmd/pushcmd/push.go b/core/pkg/cli/cmd/pushcmd/push.go index 48bdb61..c280e04 100644 --- a/core/pkg/cli/cmd/pushcmd/push.go +++ b/core/pkg/cli/cmd/pushcmd/push.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "sort" + "strings" "github.com/DeBrosOfficial/network/pkg/cli" "github.com/DeBrosOfficial/network/pkg/cli/noderesolver" @@ -14,9 +15,10 @@ import ( ) var ( - envFlag string - ipFlag string - userFlag string + envFlag string + ipFlag string + userFlag string + fanoutFlag bool ) // Cmd is the top-level "push" command — upload binary archive to nodes. @@ -25,14 +27,13 @@ var Cmd = &cobra.Command{ Short: "Push binary archive to your nodes", Long: `Upload the pre-built binary archive to nodes and extract it. -Use --ip to push to a single node, or omit it to push to all nodes -in the active environment. +By default, uploads from your machine to each node sequentially. +Use --fanout to upload to one node, then fan out server-to-server (faster). Examples: orama push --ip 1.2.3.4 # Push to one node - orama push --ip 1.2.3.4 --user ubuntu # Push with specific SSH user - orama push --env devnet # Push to all devnet nodes - orama push --env devnet --ip 1.2.3.4 # Push to one devnet node`, + orama push --env devnet # Sequential push to all devnet nodes + orama push --env devnet --fanout # Fan out server-to-server (faster)`, RunE: func(cmd *cobra.Command, args []string) error { archivePath := findNewestArchive() if archivePath == "" { @@ -47,7 +48,6 @@ Examples: var nodes []inspector.Node if ipFlag != "" { - // Single node push user := userFlag if user == "" { user = "root" @@ -55,8 +55,8 @@ Examples: vaultTarget := fmt.Sprintf("%s/%s", ipFlag, user) env := envFlag if env == "" { - active, err := cli.GetActiveEnvironment() - if err == nil { + active, _ := cli.GetActiveEnvironment() + if active != nil { env = active.Name } } @@ -64,13 +64,9 @@ Examples: vaultTarget = "sandbox/root" } nodes = []inspector.Node{{ - Host: ipFlag, - User: user, - VaultTarget: vaultTarget, - Environment: env, + Host: ipFlag, User: user, VaultTarget: vaultTarget, Environment: env, }} } else { - // All nodes in environment env := envFlag if env == "" { active, err := cli.GetActiveEnvironment() @@ -79,7 +75,6 @@ Examples: } env = active.Name } - resolved, err := noderesolver.ResolveNodes(env) if err != nil { return fmt.Errorf("failed to resolve nodes: %w", err) @@ -97,27 +92,13 @@ Examples: } defer cleanup() - fmt.Printf("Pushing to %d node(s)...\n\n", len(nodes)) - - remotePath := "/tmp/" + filepath.Base(archivePath) - extractCmd := fmt.Sprintf("sudo bash -c 'mkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s && /opt/orama/bin/orama version'", remotePath, remotePath) - - for _, n := range nodes { - fmt.Printf(" %s: uploading...", n.Host) - if err := remotessh.UploadFile(n, archivePath, remotePath); err != nil { - fmt.Printf(" FAILED (%v)\n", err) - continue - } - fmt.Printf(" extracting...") - if err := remotessh.RunSSHStreaming(n, extractCmd); err != nil { - fmt.Printf(" FAILED (%v)\n", err) - continue - } - fmt.Println(" OK") + // Single node or default: upload sequentially + if len(nodes) == 1 || !fanoutFlag { + return pushDirect(nodes, archivePath) } - fmt.Println("\nPush complete") - return nil + // Multi-node with --fanout: use agent forwarding + return pushFanout(nodes, archivePath) }, } @@ -125,6 +106,88 @@ func init() { Cmd.Flags().StringVar(&envFlag, "env", "", "Target environment (default: active)") Cmd.Flags().StringVar(&ipFlag, "ip", "", "Push to a single node by IP") Cmd.Flags().StringVar(&userFlag, "user", "", "SSH user (default: root)") + Cmd.Flags().BoolVar(&fanoutFlag, "fanout", false, "Upload to first node, then fan out server-to-server (faster)") +} + +// pushDirect uploads the archive from local machine to each node sequentially. +func pushDirect(nodes []inspector.Node, archivePath string) error { + fmt.Printf("Pushing to %d node(s) (direct)...\n\n", len(nodes)) + + remotePath := "/tmp/" + filepath.Base(archivePath) + extractCmd := fmt.Sprintf("sudo bash -c 'mkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s && /opt/orama/bin/orama version'", remotePath, remotePath) + + for _, n := range nodes { + fmt.Printf(" %s: uploading...", n.Host) + if err := remotessh.UploadFile(n, archivePath, remotePath); err != nil { + fmt.Printf(" FAILED (%v)\n", err) + continue + } + fmt.Printf(" extracting...") + if err := remotessh.RunSSHStreaming(n, extractCmd); err != nil { + fmt.Printf(" FAILED (%v)\n", err) + continue + } + fmt.Println(" OK") + } + + fmt.Println("\nPush complete") + return nil +} + +// pushFanout uploads the archive to the first node, then fans out server-to-server +// using SSH agent forwarding. +func pushFanout(nodes []inspector.Node, archivePath string) error { + fmt.Printf("Pushing to %d node(s) (fanout)...\n\n", len(nodes)) + + hub := nodes[0] + targets := nodes[1:] + remotePath := "/tmp/" + filepath.Base(archivePath) + extractCmd := fmt.Sprintf("mkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s", remotePath, remotePath) + + // Load SSH keys into the system ssh-agent for agent forwarding + fmt.Println(" Loading SSH keys into agent...") + if err := remotessh.LoadAgentKeys(nodes); err != nil { + fmt.Printf(" Warning: failed to load agent keys: %v\n", err) + fmt.Println(" Falling back to direct push...") + return pushDirect(nodes, archivePath) + } + + // Upload archive to hub + fmt.Printf(" %s (hub): uploading...", hub.Host) + if err := remotessh.UploadFile(hub, archivePath, remotePath); err != nil { + return fmt.Errorf("failed to upload to hub %s: %w", hub.Host, err) + } + fmt.Printf(" extracting...") + if err := remotessh.RunSSHStreaming(hub, "sudo bash -c '"+extractCmd+"'"); err != nil { + return fmt.Errorf("failed to extract on hub %s: %w", hub.Host, err) + } + fmt.Println(" OK") + + // Build the fanout command — hub SCPs to all targets in parallel + var fanoutParts []string + for _, t := range targets { + scpCmd := fmt.Sprintf( + "scp -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=no %s %s@%s:%s && ssh -o StrictHostKeyChecking=accept-new %s@%s 'sudo bash -c \"%s\"' && echo '%s: done'", + remotePath, t.User, t.Host, remotePath, + t.User, t.Host, extractCmd, + t.Host, + ) + fanoutParts = append(fanoutParts, "("+scpCmd+") &") + } + fanoutParts = append(fanoutParts, "wait", "echo 'Fanout complete'") + fanoutScript := strings.Join(fanoutParts, "\n") + + fmt.Printf(" Fanning out to %d nodes from %s...\n", len(targets), hub.Host) + if err := remotessh.RunSSHStreaming(hub, "bash -c '"+fanoutScript+"'", remotessh.WithAgentForward()); err != nil { + fmt.Printf(" Fanout failed: %v\n", err) + fmt.Println(" Some nodes may not have been updated") + } + + // Clean up archive on hub + remotessh.RunSSHStreaming(hub, "rm -f "+remotePath) + + fmt.Println("\nPush complete") + return nil } func findNewestArchive() string { diff --git a/website/src/components/landing/about-hero-scene.tsx b/website/src/components/landing/about-hero-scene.tsx index 7d2d754..8dab338 100644 --- a/website/src/components/landing/about-hero-scene.tsx +++ b/website/src/components/landing/about-hero-scene.tsx @@ -254,7 +254,7 @@ function AboutNetwork() { export function AboutHeroScene() { return ( -