chore: add --version flag for 'machine init|add' to specify uncloud daemon version

This commit is contained in:
Pavel Sviderski
2025-04-23 14:40:04 +10:00
parent 33550be65e
commit 02a418aafb
4 changed files with 93 additions and 56 deletions
+13 -1
View File
@@ -25,6 +25,7 @@ type addOptions struct {
publicIP string publicIP string
sshKey string sshKey string
context string context string
version string
} }
func NewAddCommand() *cobra.Command { func NewAddCommand() *cobra.Command {
@@ -64,10 +65,15 @@ func NewAddCommand() *cobra.Command {
&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().StringVar(
&opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.",
)
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.context, "context", "c", "", &opts.context, "context", "c", "",
"Name of the cluster context to add the machine to. (default is the current context)", "Name of the cluster context to add the machine to. (default is the current context)",
) )
return cmd return cmd
} }
@@ -86,7 +92,13 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine cli.RemoteMachine, o
publicIP = &ip publicIP = &ip
} }
machineClient, err := uncli.AddMachine(ctx, remoteMachine, opts.context, opts.name, publicIP) machineClient, err := uncli.AddMachine(ctx, cli.AddMachineOptions{
Context: opts.context,
MachineName: opts.name,
PublicIP: publicIP,
RemoteMachine: remoteMachine,
Version: opts.version,
})
if err != nil { if err != nil {
return err return err
} }
+13 -1
View File
@@ -24,6 +24,7 @@ type initOptions struct {
noDNS bool noDNS bool
publicIP string publicIP string
sshKey string sshKey string
version string
context string context string
} }
@@ -82,6 +83,10 @@ func NewInitCommand() *cobra.Command {
&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().StringVar(
&opts.version, "version", "latest",
"Version of the Uncloud daemon to install on the machine.",
)
cmd.Flags().StringVarP( cmd.Flags().StringVarP(
&opts.context, "context", "c", "default", &opts.context, "context", "c", "default",
"Name of the created context for the initialised cluster in the Uncloud config.", "Name of the created context for the initialised cluster in the Uncloud config.",
@@ -110,7 +115,14 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
publicIP = &ip publicIP = &ip
} }
client, err := uncli.InitCluster(ctx, remoteMachine, opts.context, opts.name, netPrefix, publicIP) client, err := uncli.InitCluster(ctx, cli.InitClusterOptions{
Context: opts.context,
MachineName: opts.name,
Network: netPrefix,
PublicIP: publicIP,
RemoteMachine: remoteMachine,
Version: opts.version,
})
if err != nil { if err != nil {
return err return err
} }
+47 -38
View File
@@ -144,31 +144,27 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
return nil, errors.New("connection configuration is invalid") return nil, errors.New("connection configuration is invalid")
} }
type InitClusterOptions struct {
Context string
MachineName string
Network netip.Prefix
PublicIP *netip.Addr
RemoteMachine *RemoteMachine
Version string
}
// 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.
// The client should be closed after use by the caller. // The client should be closed after use by the caller.
func (cli *CLI) InitCluster( func (cli *CLI) InitCluster(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
ctx context.Context, if opts.RemoteMachine != nil {
remoteMachine *RemoteMachine, return cli.initRemoteMachine(ctx, opts)
contextName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if remoteMachine != nil {
return cli.initRemoteMachine(ctx, *remoteMachine, contextName, machineName, netPrefix, publicIP)
} }
// TODO: implement local machine initialisation // TODO: implement local machine initialisation
return nil, fmt.Errorf("local machine initialisation is not implemented yet") return nil, fmt.Errorf("local machine initialisation is not implemented yet")
} }
func (cli *CLI) initRemoteMachine( func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) (*client.Client, error) {
ctx context.Context, contextName := opts.Context
remoteMachine RemoteMachine,
contextName,
machineName string,
netPrefix netip.Prefix,
publicIP *netip.Addr,
) (*client.Client, error) {
if contextName == "" { if contextName == "" {
contextName = defaultContextName contextName = defaultContextName
} }
@@ -176,7 +172,7 @@ func (cli *CLI) initRemoteMachine(
return nil, fmt.Errorf("cluster context '%s' already exists", contextName) return nil, fmt.Errorf("cluster context '%s' already exists", contextName)
} }
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine) machineClient, err := cli.provisionRemoteMachine(ctx, *opts.RemoteMachine, opts.Version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -199,12 +195,12 @@ func (cli *CLI) initRemoteMachine(
} }
req := &pb.InitClusterRequest{ req := &pb.InitClusterRequest{
MachineName: machineName, MachineName: opts.MachineName,
Network: pb.NewIPPrefix(netPrefix), Network: pb.NewIPPrefix(opts.Network),
} }
if publicIP != nil { if opts.PublicIP != nil {
if publicIP.IsValid() { if opts.PublicIP.IsValid() {
req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*publicIP)} req.PublicIpConfig = &pb.InitClusterRequest_PublicIp{PublicIp: pb.NewIP(*opts.PublicIP)}
} else { } else {
// Invalid or in other words zero IP means to automatically detect the public IP. // Invalid or in other words zero IP means to automatically detect the public IP.
req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true} req.PublicIpConfig = &pb.InitClusterRequest_PublicIpAuto{PublicIpAuto: true}
@@ -227,8 +223,8 @@ func (cli *CLI) initRemoteMachine(
// Save the machine's SSH connection details in the context config. // Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath, SSHKeyFile: opts.RemoteMachine.KeyPath,
} }
cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg) cli.Config.Contexts[contextName].Connections = append(cli.Config.Contexts[contextName].Connections, connCfg)
if err = cli.Config.Save(); err != nil { if err = cli.Config.Save(); err != nil {
@@ -237,18 +233,28 @@ func (cli *CLI) initRemoteMachine(
return machineClient, nil return machineClient, nil
} }
type AddMachineOptions struct {
Context string
MachineName string
PublicIP *netip.Addr
RemoteMachine RemoteMachine
Version string
}
// AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine // AddMachine provisions a remote machine and adds it to the cluster. It returns a client to interact with the machine
// which should be closed after use by the caller. // which should be closed after use by the caller.
func (cli *CLI) AddMachine( func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client.Client, error) {
ctx context.Context, remoteMachine RemoteMachine, contextName, machineName string, publicIP *netip.Addr, contextName := opts.Context
) (*client.Client, error) { if contextName == "" {
contextName = cli.Config.CurrentContext
}
c, err := cli.ConnectCluster(ctx, contextName) c, err := cli.ConnectCluster(ctx, contextName)
if err != nil { if err != nil {
return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err) return nil, fmt.Errorf("connect to cluster (context '%s'): %w", contextName, err)
} }
defer c.Close() defer c.Close()
machineClient, err := cli.provisionRemoteMachine(ctx, remoteMachine) machineClient, err := cli.provisionRemoteMachine(ctx, opts.RemoteMachine, opts.Version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -284,15 +290,15 @@ func (cli *CLI) AddMachine(
endpoints[i] = pb.NewIPPort(addrPort) endpoints[i] = pb.NewIPPort(addrPort)
} }
addReq := &pb.AddMachineRequest{ addReq := &pb.AddMachineRequest{
Name: machineName, Name: opts.MachineName,
Network: &pb.NetworkConfig{ Network: &pb.NetworkConfig{
Endpoints: endpoints, Endpoints: endpoints,
PublicKey: token.PublicKey, PublicKey: token.PublicKey,
}, },
} }
if publicIP != nil { if opts.PublicIP != nil {
if publicIP.IsValid() { if opts.PublicIP.IsValid() {
addReq.PublicIp = pb.NewIP(*publicIP) addReq.PublicIp = pb.NewIP(*opts.PublicIP)
} else if token.PublicIP.IsValid() { } else if token.PublicIP.IsValid() {
// Invalid or in other words zero IP means to use an automatically detected public IP from the token. // Invalid or in other words zero IP means to use an automatically detected public IP from the token.
addReq.PublicIp = pb.NewIP(token.PublicIP) addReq.PublicIp = pb.NewIP(token.PublicIP)
@@ -330,8 +336,8 @@ func (cli *CLI) AddMachine(
// Save the machine's SSH connection details in the context config. // Save the machine's SSH connection details in the context config.
connCfg := config.MachineConnection{ connCfg := config.MachineConnection{
SSH: config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), SSH: config.NewSSHDestination(opts.RemoteMachine.User, opts.RemoteMachine.Host, opts.RemoteMachine.Port),
SSHKeyFile: remoteMachine.KeyPath, SSHKeyFile: opts.RemoteMachine.KeyPath,
} }
if contextName == "" { if contextName == "" {
contextName = cli.Config.CurrentContext contextName = cli.Config.CurrentContext
@@ -346,7 +352,10 @@ func (cli *CLI) AddMachine(
// provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns // provisionRemoteMachine installs the Uncloud daemon and dependencies on the remote machine over SSH and returns
// a machine API client to interact with the machine. The client should be closed after use by the caller. // a machine API client to interact with the machine. The client should be closed after use by the caller.
func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine RemoteMachine) (*client.Client, error) { // The version parameter specifies the version of the Uncloud daemon to install. If empty, the latest version is used.
func (cli *CLI) provisionRemoteMachine(
ctx context.Context, remoteMachine RemoteMachine, version string,
) (*client.Client, error) {
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH. // Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath) sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
if err != nil { if err != nil {
@@ -357,7 +366,7 @@ func (cli *CLI) provisionRemoteMachine(ctx context.Context, remoteMachine Remote
} }
exec := sshexec.NewRemote(sshClient) exec := sshexec.NewRemote(sshClient)
// Install and run the Uncloud daemon and dependencies on the remote machine. // Install and run the Uncloud daemon and dependencies on the remote machine.
if err = provisionMachine(ctx, exec); err != nil { if err = provisionMachine(ctx, exec, version); err != nil {
return nil, fmt.Errorf("provision machine: %w", err) return nil, fmt.Errorf("provision machine: %w", err)
} }
+19 -15
View File
@@ -4,6 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/psviderski/uncloud/internal/sshexec" "github.com/psviderski/uncloud/internal/sshexec"
) )
@@ -17,36 +19,38 @@ type RemoteMachine struct {
KeyPath string KeyPath string
} }
func installCmd(user string) string { func installCmd(user string, version string) string {
sudoPrefix := "sudo" sudoPrefix := ""
var env []string
// Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket. // Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket.
env := "UNCLOUD_GROUP_ADD_USER=" + user if user != "root" {
sudoPrefix = "sudo"
curlBashCmd := fmt.Sprintf( env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user))
"curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, sshexec.Quote(env),
)
if user == "root" {
curlBashCmd = fmt.Sprintf(
"curl -fsSL %s | bash", sshexec.Quote(installScriptURL),
)
} }
if version != "" {
env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version))
}
envCmd := strings.Join(env, " ")
curlBashCmd := fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd)
return curlBashCmd return curlBashCmd
} }
// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it. // provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it.
func provisionMachine(ctx context.Context, exec sshexec.Executor) error { // If version is specified, it will be passed to the install script as UNCLOUD_VERSION environment variable.
func provisionMachine(ctx context.Context, exec sshexec.Executor, version string) error {
user, err := exec.Run(ctx, "whoami") user, err := exec.Run(ctx, "whoami")
if err != nil { if err != nil {
return fmt.Errorf("run whoami: %w", err) return fmt.Errorf("run whoami: %w", err)
} }
installCmd := installCmd(user) cmd := installCmd(user, version)
fmt.Println("Downloading Uncloud install script:", installScriptURL) fmt.Println("Downloading Uncloud install script:", installScriptURL)
cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd) cmd = sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+cmd)
if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil { if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("download and run install script: %w", err) return fmt.Errorf("download and run install script: %w", err)
} }