The setup screens can reach a database machine by a join code instead of ssh: they listen on 7444 and show a short code; llmgw join dials out, checks the setup by fingerprint, admits itself with the code's secret, and runs what the setup sends until it is done. A joined machine is one more onboard.Remote, so checks and the Patroni install run over it unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
127 lines
3.4 KiB
Go
127 lines
3.4 KiB
Go
package join
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Serve dials the setup a code names and runs what it sends until it says it
|
|
// is done. Each script is reported to log by its first line, so whoever ran
|
|
// `llmgw join` sees what is being done to their machine.
|
|
func Serve(ctx context.Context, c Code, log func(string)) error {
|
|
d := tls.Dialer{
|
|
NetDialer: &net.Dialer{Timeout: 10 * time.Second},
|
|
Config: &tls.Config{
|
|
MinVersion: tls.VersionTLS13,
|
|
// Trusted by the fingerprint in the code, not by an authority:
|
|
// the listener's certificate is made for this setup alone.
|
|
InsecureSkipVerify: true, //nolint:gosec // verified below, by fingerprint
|
|
},
|
|
}
|
|
want := c.Fingerprint[:]
|
|
d.Config.VerifyPeerCertificate = func(raw [][]byte, _ [][]*x509.Certificate) error {
|
|
if len(raw) == 0 {
|
|
return errors.New("the setup sent no certificate")
|
|
}
|
|
sum := sha256.Sum256(raw[0])
|
|
if subtle.ConstantTimeCompare(sum[:16], want) != 1 {
|
|
return errors.New("the machine at that address is not the setup this code is for")
|
|
}
|
|
return nil
|
|
}
|
|
conn, err := d.DialContext(ctx, "tcp", c.Addr)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot reach the setup at %s: %w; it listens there only while it waits for this machine", c.Addr, err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
host, _ := os.Hostname()
|
|
local, _, _ := net.SplitHostPort(conn.LocalAddr().String())
|
|
b, _ := json.Marshal(hello{Secret: c.Secret[:], Hostname: host, Addr: local})
|
|
if _, err := conn.Write(append(b, '\n')); err != nil {
|
|
return err
|
|
}
|
|
r := bufio.NewReader(conn)
|
|
line, err := r.ReadBytes('\n')
|
|
if err != nil {
|
|
return fmt.Errorf("the setup closed the connection: %w", err)
|
|
}
|
|
var ack response
|
|
if err := json.Unmarshal(line, &ack); err != nil {
|
|
return errors.New("the setup answered something unreadable")
|
|
}
|
|
if ack.Error != "" {
|
|
return errors.New(ack.Error)
|
|
}
|
|
log(fmt.Sprintf("joined the setup at %s; it is setting this machine up", c.Addr))
|
|
|
|
for {
|
|
line, err := r.ReadBytes('\n')
|
|
if err != nil {
|
|
return fmt.Errorf("the setup went away before it said it was done: %w", err)
|
|
}
|
|
var req request
|
|
if err := json.Unmarshal(line, &req); err != nil {
|
|
return errors.New("the setup sent something unreadable")
|
|
}
|
|
if req.Bye {
|
|
log("the setup is done with this machine")
|
|
return nil
|
|
}
|
|
log(summary(req.Script))
|
|
resp := run(ctx, req)
|
|
out, _ := json.Marshal(resp)
|
|
if _, err := conn.Write(append(out, '\n')); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// run runs one script as the setup asked.
|
|
func run(ctx context.Context, req request) response {
|
|
cmd := exec.CommandContext(ctx, "sh", "-c", req.Script)
|
|
if req.Stdin != nil {
|
|
cmd.Stdin = bytes.NewReader(req.Stdin)
|
|
}
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
err := cmd.Run()
|
|
resp := response{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}
|
|
var exit *exec.ExitError
|
|
switch {
|
|
case errors.As(err, &exit):
|
|
resp.Exit = exit.ExitCode()
|
|
case err != nil:
|
|
resp.Error = err.Error()
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// summary is a script's first line that says something.
|
|
func summary(script string) string {
|
|
for _, l := range strings.Split(script, "\n") {
|
|
l = strings.TrimSpace(l)
|
|
if l == "" || l == "set -e" || strings.HasPrefix(l, "#") {
|
|
continue
|
|
}
|
|
if len(l) > 100 {
|
|
l = l[:100] + "…"
|
|
}
|
|
return " " + l
|
|
}
|
|
return " (an empty script)"
|
|
}
|