package main import ( "bytes" "context" "fmt" "net" "os" "time" "golang.org/x/crypto/ssh" ) // sshRun executes one command on a lab VM as the harness user (key from Secret Manager, design 6.11). // Host resolution: IP as-is, "legacy:N" or pet VM short name via the compute API. func (a *App) sshRun(ctx context.Context, host, command string) (string, error) { ip := host if net.ParseIP(host) == nil { in, err := a.resolveInstance(ctx, host) if err != nil { return "", err } ip = in.IP } key, err := os.ReadFile(a.cfg.SSHKeyFile) if err != nil { return "", fmt.Errorf("ssh key: %w", err) } signer, err := ssh.ParsePrivateKey(key) if err != nil { return "", fmt.Errorf("ssh key parse: %w", err) } cfg := &ssh.ClientConfig{ User: a.cfg.SSHUser, Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // lab VMs are recreated by autohealing; host keys are not pinned Timeout: 15 * time.Second, } d := net.Dialer{Timeout: 15 * time.Second} conn, err := d.DialContext(ctx, "tcp", ip+":22") if err != nil { return "", fmt.Errorf("dial %s: %w", ip, err) } c, chans, reqs, err := ssh.NewClientConn(conn, ip+":22", cfg) if err != nil { conn.Close() return "", fmt.Errorf("ssh %s: %w", ip, err) } client := ssh.NewClient(c, chans, reqs) defer client.Close() sess, err := client.NewSession() if err != nil { return "", err } defer sess.Close() var out bytes.Buffer sess.Stdout, sess.Stderr = &out, &out done := make(chan error, 1) go func() { done <- sess.Run(command) }() select { case err := <-done: if err != nil { return out.String(), fmt.Errorf("%s: %w: %s", command, err, out.String()) } return out.String(), nil case <-ctx.Done(): return out.String(), ctx.Err() } }