package orchestrator import ( "context" "fmt" "log" "os" "time" "golang.org/x/crypto/ssh" ) type ClusterJoiner struct { ExistingNode string ClusterName string JoinFingerprint string SSHKeyPath string } func (c *ClusterJoiner) Join(ctx context.Context, hostIP string) error { client, err := c.connect(hostIP) if err != nil { return fmt.Errorf("ssh connect to %s: %w", hostIP, err) } defer client.Close() cmd := fmt.Sprintf("pvecm add %s --force", c.ExistingNode) log.Printf("cluster: running on %s: %s", hostIP, cmd) session, err := client.NewSession() if err != nil { return fmt.Errorf("ssh session: %w", err) } defer session.Close() output, err := session.CombinedOutput(cmd) if err != nil { return fmt.Errorf("pvecm add failed: %w\noutput: %s", err, string(output)) } log.Printf("cluster: %s joined successfully", hostIP) return nil } func (c *ClusterJoiner) connect(hostIP string) (*ssh.Client, error) { keyData, err := os.ReadFile(c.SSHKeyPath) if err != nil { return nil, fmt.Errorf("read ssh key: %w", err) } signer, err := ssh.ParsePrivateKey(keyData) if err != nil { return nil, fmt.Errorf("parse ssh key: %w", err) } config := &ssh.ClientConfig{ User: "root", Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 30 * time.Second, } return ssh.Dial("tcp", hostIP+":22", config) }