feat(connect): add --connect flag to connect to remote cluster machine without using config

This commit is contained in:
Pavel Sviderski
2025-03-18 14:01:38 +10:00
parent b799b7518e
commit a0a66f34c4
4 changed files with 65 additions and 14 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ func NewInitCommand() *cobra.Command {
) )
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.sshKey, "ssh-key", "i", "", &opts.sshKey, "ssh-key", "i", "",
"path to SSH private key for SSH remote login. (default ~/.ssh/id_*)", "Path to SSH private key for SSH remote login. (default ~/.ssh/id_*)",
) )
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.cluster, "cluster", "c", "", &opts.cluster, "cluster", "c", "",
+35 -10
View File
@@ -4,32 +4,53 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os" "net/netip"
"strings" "strings"
"uncloud/cmd/uncloud/caddy" "uncloud/cmd/uncloud/caddy"
"uncloud/cmd/uncloud/dns" "uncloud/cmd/uncloud/dns"
"uncloud/cmd/uncloud/machine" "uncloud/cmd/uncloud/machine"
"uncloud/cmd/uncloud/service" "uncloud/cmd/uncloud/service"
"uncloud/internal/cli" "uncloud/internal/cli"
"uncloud/internal/cli/config"
"uncloud/internal/fs"
) )
type globalOptions struct {
configPath string
connect string
}
func main() { func main() {
var configPath string opts := globalOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uncloud", Use: "uncloud",
Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.", Short: "A CLI tool for managing Uncloud resources such as clusters, machines, and services.",
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if strings.HasPrefix(configPath, "~/") { var conn *config.MachineConnection
home, err := os.UserHomeDir() if opts.connect != "" {
if err != nil { if strings.HasPrefix(opts.connect, "tcp://") {
return fmt.Errorf("get user home directory to resolve %q: %w", configPath, err) addrPort, err := netip.ParseAddrPort(opts.connect[len("tcp://"):])
if err != nil {
return fmt.Errorf("parse TCP address: %w", err)
}
conn = &config.MachineConnection{
TCP: addrPort,
}
} else {
dest := opts.connect
if strings.HasPrefix(dest, "ssh://") {
dest = dest[len("ssh://"):]
}
conn = &config.MachineConnection{
SSH: config.SSHDestination(dest),
}
} }
configPath = strings.Replace(configPath, "~", home, 1)
} }
uncli, err := cli.New(configPath) configPath := fs.ExpandHomeDir(opts.configPath)
uncli, err := cli.New(configPath, conn)
if err != nil { if err != nil {
return fmt.Errorf("initialize CLI: %w", err) return fmt.Errorf("initialize CLI: %w", err)
} }
@@ -37,9 +58,13 @@ func main() {
return nil return nil
}, },
} }
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
"Connect to a remote cluster machine without using the Uncloud configuration file.\n"+
"Format: [ssh://]user@host[:port] or tcp://host:port")
// TODO: allow to override using UNCLOUD_CONFIG env var. // TODO: allow to override using UNCLOUD_CONFIG env var.
cmd.PersistentFlags().StringVar(&configPath, "uncloud-config", "~/.config/uncloud/config.toml", cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.toml",
"path to the Uncloud configuration file.") "Path to the Uncloud configuration file.")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml") _ = cmd.MarkPersistentFlagFilename("uncloud-config", "toml")
cmd.AddCommand( cmd.AddCommand(
+28 -2
View File
@@ -23,13 +23,22 @@ const defaultClusterName = "default"
type CLI struct { type CLI struct {
config *config.Config config *config.Config
conn *config.MachineConnection
} }
func New(configPath string) (*CLI, error) { // New creates a new CLI instance with the given config path or remote machine connection.
// If the connection is provided, the config is ignored for all operations which is useful for interacting with
// a cluster without creating a config.
func New(configPath string, conn *config.MachineConnection) (*CLI, error) {
if conn != nil {
return &CLI{conn: conn}, nil
}
cfg, err := config.NewFromFile(configPath) cfg, err := config.NewFromFile(configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("read Uncloud config: %w", err) return nil, fmt.Errorf("read Uncloud config: %w", err)
} }
return &CLI{ return &CLI{
config: cfg, config: cfg,
}, nil }, nil
@@ -53,7 +62,13 @@ func (cli *CLI) SetCurrentCluster(name string) error {
return cli.config.Save() return cli.config.Save()
} }
// ConnectCluster connects to a cluster using the given cluster name or the current cluster if not specified.
// If the CLI was initialised with a machine connection, the config is ignored and the connection is used instead.
func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) { func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client.Client, error) {
if cli.conn != nil {
return connectCluster(ctx, *cli.conn)
}
if len(cli.config.Clusters) == 0 { if len(cli.config.Clusters) == 0 {
return nil, errors.New( return nil, errors.New(
"no clusters found in the Uncloud config. " + "no clusters found in the Uncloud config. " +
@@ -88,6 +103,16 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
// TODO: iterate over all connections and try to connect to the cluster using the first successful connection. // TODO: iterate over all connections and try to connect to the cluster using the first successful connection.
conn := cfg.Connections[0] conn := cfg.Connections[0]
c, err := connectCluster(ctx, conn)
if err != nil {
return nil, errors.New("no valid connection configuration found for the cluster")
}
return c, nil
}
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
if conn.SSH != "" { if conn.SSH != "" {
user, host, port, err := conn.SSH.Parse() user, host, port, err := conn.SSH.Parse()
if err != nil { if err != nil {
@@ -106,7 +131,8 @@ func (cli *CLI) ConnectCluster(ctx context.Context, clusterName string) (*client
} else if conn.TCP.IsValid() { } else if conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(conn.TCP)) return client.New(ctx, connector.NewTCPConnector(conn.TCP))
} }
return nil, errors.New("no valid connection configuration found for the cluster")
return nil, errors.New("connection configuration is invalid")
} }
// InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster. // InitCluster initialises a new cluster on a remote machine and returns a client to interact with the cluster.
+1 -1
View File
@@ -15,10 +15,10 @@ const (
type MachineConnection struct { type MachineConnection struct {
SSH SSHDestination `toml:"ssh,omitempty"` SSH SSHDestination `toml:"ssh,omitempty"`
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
TCP netip.AddrPort `toml:"tcp,omitempty"` TCP netip.AddrPort `toml:"tcp,omitempty"`
Host string `toml:"host,omitempty"` Host string `toml:"host,omitempty"`
PublicKey secret.Secret `toml:"public_key,omitempty"` PublicKey secret.Secret `toml:"public_key,omitempty"`
SSHKeyFile string `toml:"ssh_key_file,omitempty"`
} }
// SSHDestination represents an SSH destination string in the canonical form of "user@host:port". // SSHDestination represents an SSH destination string in the canonical form of "user@host:port".