mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
214 lines
5.5 KiB
Go
214 lines
5.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"uncloud/internal/cli/config"
|
|
"uncloud/internal/cmdexec"
|
|
"uncloud/internal/machine"
|
|
"uncloud/internal/machine/network"
|
|
"uncloud/internal/secret"
|
|
)
|
|
|
|
var (
|
|
ErrNotFound = errors.New("not found")
|
|
)
|
|
|
|
type Cluster struct {
|
|
Name string
|
|
privateKey ed25519.PrivateKey
|
|
|
|
config *config.Config
|
|
}
|
|
|
|
func (c *Cluster) User() (*User, error) {
|
|
userKey := c.config.Clusters[c.Name].UserKey
|
|
if userKey == nil {
|
|
return nil, errors.New("cluster user_key must be set in the config")
|
|
}
|
|
return NewUser(userKey)
|
|
}
|
|
|
|
func (c *Cluster) Machines() []config.MachineConnection {
|
|
cfg, ok := c.config.Clusters[c.Name]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return cfg.Machines
|
|
}
|
|
|
|
func (c *Cluster) toConfig() *config.Cluster {
|
|
return &config.Cluster{
|
|
Name: c.Name,
|
|
Secret: c.privateKey.Seed(),
|
|
}
|
|
}
|
|
|
|
func (cli *CLI) CreateCluster(name string, privateKey ed25519.PrivateKey, userPrivateKey secret.Secret) (*Cluster, error) {
|
|
if _, ok := cli.config.Clusters[name]; ok {
|
|
return nil, fmt.Errorf("cluster %q already exists", name)
|
|
}
|
|
if privateKey == nil {
|
|
var err error
|
|
_, privateKey, err = ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate cluster secret: %w", err)
|
|
}
|
|
}
|
|
if userPrivateKey == nil {
|
|
user, err := NewUser(nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate user: %w", err)
|
|
}
|
|
userPrivateKey = user.PrivateKey()
|
|
}
|
|
|
|
c := &Cluster{
|
|
Name: name,
|
|
privateKey: privateKey,
|
|
config: cli.config,
|
|
}
|
|
cfg := c.toConfig()
|
|
cfg.UserKey = userPrivateKey
|
|
cli.config.Clusters[name] = cfg
|
|
if err := cli.config.Save(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (cli *CLI) CreateDefaultCluster() (*Cluster, error) {
|
|
c, err := cli.CreateCluster("default", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = cli.SetCurrentCluster(c.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (cli *CLI) GetCluster(name string) (*Cluster, error) {
|
|
clusterCfg, ok := cli.config.Clusters[name]
|
|
if !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
privateKey, err := privateKeyFromSecret(clusterCfg.Secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Cluster{
|
|
Name: name,
|
|
privateKey: privateKey,
|
|
config: cli.config,
|
|
}, nil
|
|
}
|
|
|
|
func (cli *CLI) GetCurrentCluster() (*Cluster, error) {
|
|
return cli.GetCluster(cli.config.CurrentCluster)
|
|
}
|
|
|
|
func (cli *CLI) SetCurrentCluster(name string) error {
|
|
if _, ok := cli.config.Clusters[name]; !ok {
|
|
return ErrNotFound
|
|
}
|
|
cli.config.CurrentCluster = name
|
|
return cli.config.Save()
|
|
}
|
|
|
|
func (cli *CLI) ListClusters() ([]*Cluster, error) {
|
|
var clusters []*Cluster
|
|
for name := range cli.config.Clusters {
|
|
c, err := cli.GetCluster(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get cluster %q: %w", name, err)
|
|
}
|
|
clusters = append(clusters, c)
|
|
}
|
|
return clusters, nil
|
|
}
|
|
|
|
func (c *Cluster) AddMachine(ctx context.Context, name, user, host string, port int, sshKeyPath string) (string, error) {
|
|
exec, err := cmdexec.Connect(user, host, port, sshKeyPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("SSH login to %s@%s:%d: %w", user, host, port, err)
|
|
}
|
|
defer func() {
|
|
_ = exec.Close()
|
|
}()
|
|
|
|
clusterUser, err := c.User()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
userPeerCfg := network.PeerConfig{
|
|
ManagementIP: clusterUser.ManagementIP(),
|
|
PublicKey: clusterUser.PublicKey(),
|
|
}
|
|
mcfg, err := machine.NewBootstrapConfig(name, netip.Prefix{}, userPeerCfg)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate machine bootstrap config: %w", err)
|
|
}
|
|
|
|
sudoPrefix := ""
|
|
if user != "root" {
|
|
sudoPrefix = "sudo"
|
|
}
|
|
|
|
_, err = exec.Run(ctx, cmdexec.QuoteCommand(sudoPrefix, "mkdir", "-p", machine.DefaultDataDir))
|
|
if err != nil {
|
|
return "", fmt.Errorf("create data directory %q: %w", machine.DefaultDataDir, err)
|
|
}
|
|
|
|
// TODO: Check if the machine is already provisioned and ask the user to reset it first.
|
|
// Write the machine config to /var/lib/uncloud/machine.json by piping the JSON data to the file.
|
|
mcfgData, err := mcfg.Encode()
|
|
if err != nil {
|
|
return "", fmt.Errorf("encode machine config: %w", err)
|
|
}
|
|
mcfgPath := cmdexec.Quote(machine.ConfigPath(machine.DefaultDataDir))
|
|
createFileCmd := fmt.Sprintf("%s touch %s && %s chmod 600 %s", sudoPrefix, mcfgPath, sudoPrefix, mcfgPath)
|
|
_, err = exec.Run(ctx, fmt.Sprintf("%s && echo %s | %s tee %s > /dev/null",
|
|
createFileCmd, cmdexec.Quote(string(mcfgData)), sudoPrefix, mcfgPath))
|
|
if err != nil {
|
|
return "", fmt.Errorf("write machine config to %q: %w", mcfgPath, err)
|
|
}
|
|
fmt.Println("Machine config written to", mcfgPath)
|
|
|
|
// TODO: download and install the latest uncloudd binary by running the install shell script from GitHub.
|
|
// For now upload the binary using scp manually.
|
|
|
|
out, err := exec.Run(ctx, cmdexec.QuoteCommand(sudoPrefix, "systemctl", "restart", "uncloudd"))
|
|
if err != nil {
|
|
return "", fmt.Errorf("start uncloudd: %w: %s", err, out)
|
|
}
|
|
fmt.Println("uncloudd daemon started")
|
|
|
|
connConfig := config.MachineConnection{
|
|
User: user,
|
|
Host: host,
|
|
Port: port,
|
|
SSHKey: sshKeyPath,
|
|
PublicKey: mcfg.Network.PublicKey,
|
|
}
|
|
c.config.Clusters[c.Name].Machines = append(c.config.Clusters[c.Name].Machines, connConfig)
|
|
if err = c.config.Save(); err != nil {
|
|
return "", fmt.Errorf("save config: %w", err)
|
|
}
|
|
|
|
return mcfg.Name, nil
|
|
}
|
|
|
|
func privateKeyFromSecret(s secret.Secret) (ed25519.PrivateKey, error) {
|
|
// Cluster secret in the config is a hex-encoded private key seed.
|
|
if len(s) != ed25519.SeedSize {
|
|
return nil, fmt.Errorf("invalid cluster secret length")
|
|
}
|
|
return ed25519.NewKeyFromSeed(s), nil
|
|
}
|