diff --git a/cmd/uncloud/machine/add.go b/cmd/uncloud/machine/add.go index 463b43e1..479cb9a7 100644 --- a/cmd/uncloud/machine/add.go +++ b/cmd/uncloud/machine/add.go @@ -2,8 +2,6 @@ package machine import ( "context" - "errors" - "fmt" "github.com/spf13/cobra" "uncloud/internal/cli" ) @@ -19,6 +17,7 @@ type addOptions struct { func NewAddCommand() *cobra.Command { opts := addOptions{} cmd := &cobra.Command{ + // TODO: add support for [USER@]HOST[:PORT] syntax Use: "add HOST", Short: "Add a new machine to a cluster.", Args: cobra.ExactArgs(1), @@ -38,45 +37,5 @@ func NewAddCommand() *cobra.Command { } func add(ctx context.Context, uncli *cli.CLI, host string, opts addOptions) error { - var ( - cluster *cli.Cluster - err error - ) - if opts.cluster == "" { - // If the cluster is not specified, use the current cluster. If there are no clusters, create a default one. - cluster, err = uncli.GetCurrentCluster() - if err != nil { - if errors.Is(err, cli.ErrNotFound) { - // Do not create a default cluster if there are already clusters but the current cluster is not set. - clusters, cErr := uncli.ListClusters() - if cErr != nil { - return fmt.Errorf("list clusters: %w", cErr) - } - if len(clusters) > 0 { - return errors.New("the current cluster is not set in the Uncloud config. " + - "Please specify a cluster with the --cluster flag or set current_cluster in the config") - } - - cluster, err = uncli.CreateDefaultCluster() - if err != nil { - return fmt.Errorf("create default cluster: %w", err) - } - fmt.Printf("Created %q cluster\n", cluster.Name) - } else { - return fmt.Errorf("get current cluster: %w", err) - } - } - } else { - cluster, err = uncli.GetCluster(opts.cluster) - if err != nil { - return fmt.Errorf("get cluster %q: %w", opts.cluster, err) - } - } - - name, err := cluster.AddMachine(ctx, opts.name, opts.user, host, opts.port, opts.sshKey) - if err != nil { - return fmt.Errorf("add machine to cluster %q: %w", cluster.Name, err) - } - fmt.Printf("Machine %q added to cluster %q\n", name, cluster.Name) - return nil + return uncli.AddMachine(ctx, opts.cluster, opts.name, opts.user, host, opts.port, opts.sshKey) } diff --git a/cmd/uncloud/machine/init.go b/cmd/uncloud/machine/init.go index 99456b13..2c37fea1 100644 --- a/cmd/uncloud/machine/init.go +++ b/cmd/uncloud/machine/init.go @@ -4,66 +4,67 @@ import ( "fmt" "github.com/spf13/cobra" "net/netip" - "uncloud/internal/machine" - "uncloud/internal/machine/api/pb" + "uncloud/internal/cli" + "uncloud/internal/cli/config" "uncloud/internal/machine/network" - "uncloud/internal/secret" ) type initOptions struct { name string network string userPublicKey string - dataDir string + + sshKey string + cluster string } func NewInitCommand() *cobra.Command { opts := initOptions{} cmd := &cobra.Command{ - Use: "init", + Use: "init [USER@HOST:PORT]", + Args: cobra.MaximumNArgs(1), + // TODO: include usage examples of initialising a local and remote machine. Short: "Initialise a new cluster that consists of the local or remote machine.", RunE: func(cmd *cobra.Command, args []string) error { + uncli := cmd.Context().Value("cli").(*cli.CLI) + + var remoteMachine *cli.RemoteMachine + if len(args) > 0 { + user, host, port, err := config.SSHDestination(args[0]).Parse() + if err != nil { + return fmt.Errorf("parse remote machine: %w", err) + } + remoteMachine = &cli.RemoteMachine{ + User: user, + Host: host, + Port: port, + KeyPath: opts.sshKey, + } + } netPrefix, err := netip.ParsePrefix(opts.network) if err != nil { return fmt.Errorf("parse network CIDR: %w", err) } - var users []*pb.User - if opts.userPublicKey != "" { - pubKey, uErr := secret.FromHexString(opts.userPublicKey) - if uErr != nil { - return fmt.Errorf("parse user's public key: %w", uErr) - } - user := &pb.User{ - Network: &pb.NetworkConfig{ - ManagementIp: pb.NewIP(network.ManagementIP(pubKey)), - PublicKey: pubKey, - }, - } - users = append(users, user) - } - - // TODO: ideally this should be an RPC call to the machine API via unix socket. - config := &machine.Config{DataDir: opts.dataDir} - mach, err := machine.NewMachine(config) - if err != nil { - return fmt.Errorf("init machine: %w", err) - } - if err = mach.InitCluster(opts.name, netPrefix, users); err != nil { - return fmt.Errorf("initialise cluster: %w", err) - } - return nil + return uncli.InitCluster(cmd.Context(), remoteMachine, opts.cluster, opts.name, netPrefix) }, } cmd.Flags().StringVarP(&opts.name, "name", "n", "", "Assign a name to the machine") - cmd.Flags().StringVar(&opts.network, "network", network.DefaultNetwork.String(), - "IPv4 network CIDR to use for machines and services") - cmd.Flags().StringVarP(&opts.userPublicKey, "user-pubkey", "u", "", - "User's public key which will be able to access the cluster (hex-encoded)") + cmd.Flags().StringVar( + &opts.network, "network", network.DefaultNetwork.String(), + "IPv4 network CIDR to use for machines and services", + ) + //cmd.Flags().StringVar(&opts.userPublicKey, "user-pubkey", "", + // "User's public key which will be able to access the cluster (hex-encoded)") - cmd.Flags().StringVarP(&opts.dataDir, "data-dir", "d", machine.DefaultDataDir, - "Directory for storing persistent machine state") - _ = cmd.MarkFlagDirname("data-dir") + cmd.Flags().StringVarP( + &opts.sshKey, "ssh-key", "i", "", + "path to SSH private key for SSH remote login (default ~/.ssh/id_*)", + ) + cmd.Flags().StringVarP( + &opts.cluster, "cluster", "c", "", + "Name of the cluster in the local config if initialising a remote machine", + ) return cmd } diff --git a/internal/sshexec/executor.go b/internal/sshexec/executor.go index dbed6657..dfe1ed8c 100644 --- a/internal/sshexec/executor.go +++ b/internal/sshexec/executor.go @@ -1,4 +1,4 @@ -package cmdexec +package sshexec import ( "context" diff --git a/internal/sshexec/remote.go b/internal/sshexec/remote.go index 33ddd117..cdb2c312 100644 --- a/internal/sshexec/remote.go +++ b/internal/sshexec/remote.go @@ -1,4 +1,4 @@ -package cmdexec +package sshexec import ( "context" @@ -11,6 +11,10 @@ type Remote struct { client *ssh.Client } +func NewRemote(client *ssh.Client) *Remote { + return &Remote{client: client} +} + // Run runs the command on the remote host and returns its output with all leading and trailing // white space removed. func (r *Remote) Run(ctx context.Context, cmd string) (string, error) { diff --git a/internal/sshexec/ssh.go b/internal/sshexec/ssh.go index c858b197..8e741b63 100644 --- a/internal/sshexec/ssh.go +++ b/internal/sshexec/ssh.go @@ -1,4 +1,4 @@ -package cmdexec +package sshexec import ( "fmt" @@ -10,7 +10,7 @@ import ( "time" ) -func Connect(user, host string, port int, sshKeyPath string) (*Remote, error) { +func Connect(user, host string, port int, sshKeyPath string) (*ssh.Client, error) { addr := net.JoinHostPort(host, strconv.Itoa(port)) // Try to connect using SSH agent only. agentAuth, agentClose, agentErr := sshAgentAuth() @@ -24,9 +24,7 @@ func Connect(user, host string, port int, sshKeyPath string) (*Remote, error) { } var client *ssh.Client if client, agentErr = ssh.Dial("tcp", addr, config); agentErr == nil { - return &Remote{ - client: client, - }, nil + return client, nil } } // Fall back to using private key as the connection attempt using SSH agent failed. @@ -47,9 +45,7 @@ func Connect(user, host string, port int, sshKeyPath string) (*Remote, error) { return nil, fmt.Errorf("connect using private key %q: %w", sshKeyPath, err) } - return &Remote{ - client: client, - }, nil + return client, nil } func sshAgentAuth() (ssh.AuthMethod, func(), error) {