vm.go (1529 LOC) splits into vm_create, vm_lifecycle, vm_set, vm_stats, vm_disk, vm_authsync; firecracker/DNS/helpers stay in vm.go. guest_sessions.go (1266 LOC) splits into session_controller, session_lifecycle, session_attach, session_stream; scripts and helpers stay in guest_sessions.go. Mechanical move only. No behavior change. Adds doc.go and ARCHITECTURE.md capturing subsystem map and current lock ordering as the baseline for the upcoming subsystem extraction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
353 lines
10 KiB
Go
353 lines
10 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"banger/internal/guest"
|
|
"banger/internal/model"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
const (
|
|
workDiskGitConfigRelativePath = ".gitconfig"
|
|
workDiskOpencodeAuthDirRelativePath = ".local/share/opencode"
|
|
workDiskOpencodeAuthRelativePath = workDiskOpencodeAuthDirRelativePath + "/auth.json"
|
|
workDiskClaudeAuthDirRelativePath = ".claude"
|
|
workDiskClaudeAuthRelativePath = workDiskClaudeAuthDirRelativePath + "/.credentials.json"
|
|
workDiskPiAuthDirRelativePath = ".pi/agent"
|
|
workDiskPiAuthRelativePath = workDiskPiAuthDirRelativePath + "/auth.json"
|
|
hostGlobalGitIdentitySource = "git config --global"
|
|
hostOpencodeAuthDefaultDisplayPath = "~/" + workDiskOpencodeAuthRelativePath
|
|
hostClaudeAuthDefaultDisplayPath = "~/" + workDiskClaudeAuthRelativePath
|
|
hostPiAuthDefaultDisplayPath = "~/" + workDiskPiAuthRelativePath
|
|
)
|
|
|
|
type gitIdentity struct {
|
|
Name string
|
|
Email string
|
|
}
|
|
|
|
func (d *Daemon) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm *model.VMRecord, image model.Image, prep workDiskPreparation) error {
|
|
fingerprint, err := guest.AuthorizedPublicKeyFingerprint(d.config.SSHKeyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("derive authorized ssh key fingerprint: %w", err)
|
|
}
|
|
if prep.ClonedFromSeed && image.SeededSSHPublicKeyFingerprint != "" && image.SeededSSHPublicKeyFingerprint == fingerprint {
|
|
vmCreateStage(ctx, "prepare_work_disk", "using seeded SSH access")
|
|
return nil
|
|
}
|
|
publicKey, err := guest.AuthorizedPublicKey(d.config.SSHKeyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("derive authorized ssh key: %w", err)
|
|
}
|
|
vmCreateStage(ctx, "prepare_work_disk", "repairing SSH access on work disk")
|
|
workMount, cleanupWork, err := system.MountTempDir(ctx, d.runner, vm.Runtime.WorkDiskPath, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanupWork()
|
|
|
|
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
|
|
return err
|
|
}
|
|
|
|
sshDir := filepath.Join(workMount, ".ssh")
|
|
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
|
|
return err
|
|
}
|
|
if _, err := d.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
|
|
existing, err := d.runner.RunSudo(ctx, "cat", authorizedKeysPath)
|
|
if err != nil {
|
|
existing = nil
|
|
}
|
|
merged := mergeAuthorizedKey(existing, publicKey)
|
|
|
|
tmpFile, err := os.CreateTemp("", "banger-authorized-keys-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpPath := tmpFile.Name()
|
|
if _, err := tmpFile.Write(merged); err != nil {
|
|
_ = tmpFile.Close()
|
|
_ = os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
if err := tmpFile.Close(); err != nil {
|
|
_ = os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
defer os.Remove(tmpPath)
|
|
|
|
if _, err := d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
|
|
return err
|
|
}
|
|
if prep.ClonedFromSeed && image.Managed {
|
|
vmCreateStage(ctx, "prepare_work_disk", "refreshing managed work seed")
|
|
if err := d.refreshManagedWorkSeedFingerprint(ctx, image, fingerprint); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Daemon) ensureGitIdentityOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
|
|
runner := d.runner
|
|
if runner == nil {
|
|
runner = system.NewRunner()
|
|
}
|
|
|
|
identity, err := resolveHostGlobalGitIdentity(ctx, runner)
|
|
if err != nil {
|
|
d.warnGitIdentitySyncSkipped(*vm, hostGlobalGitIdentitySource, err)
|
|
return nil
|
|
}
|
|
|
|
vmCreateStage(ctx, "prepare_work_disk", "syncing git identity")
|
|
workMount, cleanupWork, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanupWork()
|
|
|
|
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeGitIdentity(ctx, runner, filepath.Join(workMount, workDiskGitConfigRelativePath), identity)
|
|
}
|
|
|
|
func (d *Daemon) ensureOpencodeAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
|
|
return d.ensureAuthFileOnWorkDisk(
|
|
ctx,
|
|
vm,
|
|
"syncing opencode auth",
|
|
hostOpencodeAuthDefaultDisplayPath,
|
|
resolveHostOpencodeAuthPath,
|
|
workDiskOpencodeAuthRelativePath,
|
|
d.warnOpencodeAuthSyncSkipped,
|
|
)
|
|
}
|
|
|
|
func (d *Daemon) ensureClaudeAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
|
|
return d.ensureAuthFileOnWorkDisk(
|
|
ctx,
|
|
vm,
|
|
"syncing claude auth",
|
|
hostClaudeAuthDefaultDisplayPath,
|
|
resolveHostClaudeAuthPath,
|
|
workDiskClaudeAuthRelativePath,
|
|
d.warnClaudeAuthSyncSkipped,
|
|
)
|
|
}
|
|
|
|
func (d *Daemon) ensurePiAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
|
|
return d.ensureAuthFileOnWorkDisk(
|
|
ctx,
|
|
vm,
|
|
"syncing pi auth",
|
|
hostPiAuthDefaultDisplayPath,
|
|
resolveHostPiAuthPath,
|
|
workDiskPiAuthRelativePath,
|
|
d.warnPiAuthSyncSkipped,
|
|
)
|
|
}
|
|
|
|
func (d *Daemon) ensureAuthFileOnWorkDisk(ctx context.Context, vm *model.VMRecord, stageDetail, defaultDisplayPath string, resolveHostPath func() (string, error), guestRelativePath string, warn func(model.VMRecord, string, error)) error {
|
|
hostAuthPath, err := resolveHostPath()
|
|
if err != nil {
|
|
warn(*vm, defaultDisplayPath, err)
|
|
return nil
|
|
}
|
|
authData, err := os.ReadFile(hostAuthPath)
|
|
if err != nil {
|
|
warn(*vm, hostAuthPath, err)
|
|
return nil
|
|
}
|
|
|
|
runner := d.runner
|
|
if runner == nil {
|
|
runner = system.NewRunner()
|
|
}
|
|
|
|
vmCreateStage(ctx, "prepare_work_disk", stageDetail)
|
|
workMount, cleanupWork, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanupWork()
|
|
|
|
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
|
|
return err
|
|
}
|
|
|
|
authDir := filepath.Join(workMount, filepath.Dir(guestRelativePath))
|
|
if _, err := runner.RunSudo(ctx, "mkdir", "-p", authDir); err != nil {
|
|
return err
|
|
}
|
|
authPath := filepath.Join(workMount, guestRelativePath)
|
|
|
|
tmpFile, err := os.CreateTemp("", "banger-auth-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpPath := tmpFile.Name()
|
|
if _, err := tmpFile.Write(authData); err != nil {
|
|
_ = tmpFile.Close()
|
|
_ = os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
if err := tmpFile.Close(); err != nil {
|
|
_ = os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
defer os.Remove(tmpPath)
|
|
|
|
_, err = runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authPath)
|
|
return err
|
|
}
|
|
|
|
func resolveHostOpencodeAuthPath() (string, error) {
|
|
return resolveHostAuthPath(workDiskOpencodeAuthRelativePath)
|
|
}
|
|
|
|
func resolveHostClaudeAuthPath() (string, error) {
|
|
return resolveHostAuthPath(workDiskClaudeAuthRelativePath)
|
|
}
|
|
|
|
func resolveHostPiAuthPath() (string, error) {
|
|
return resolveHostAuthPath(workDiskPiAuthRelativePath)
|
|
}
|
|
|
|
func resolveHostAuthPath(relativePath string) (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, relativePath), nil
|
|
}
|
|
|
|
func resolveHostGlobalGitIdentity(ctx context.Context, runner system.CommandRunner) (gitIdentity, error) {
|
|
name, err := gitConfigValue(ctx, runner, nil, "user.name")
|
|
if err != nil {
|
|
return gitIdentity{}, err
|
|
}
|
|
if name == "" {
|
|
return gitIdentity{}, errors.New("host git user.name is empty")
|
|
}
|
|
|
|
email, err := gitConfigValue(ctx, runner, nil, "user.email")
|
|
if err != nil {
|
|
return gitIdentity{}, err
|
|
}
|
|
if email == "" {
|
|
return gitIdentity{}, errors.New("host git user.email is empty")
|
|
}
|
|
|
|
return gitIdentity{Name: name, Email: email}, nil
|
|
}
|
|
|
|
func gitConfigValue(ctx context.Context, runner system.CommandRunner, extraArgs []string, key string) (string, error) {
|
|
args := []string{"config"}
|
|
args = append(args, extraArgs...)
|
|
args = append(args, "--default", "", "--get", key)
|
|
out, err := runner.Run(ctx, "git", args...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
func writeGitIdentity(ctx context.Context, runner system.CommandRunner, gitConfigPath string, identity gitIdentity) error {
|
|
existing, err := runner.RunSudo(ctx, "cat", gitConfigPath)
|
|
if err != nil {
|
|
existing = nil
|
|
}
|
|
|
|
tmpFile, err := os.CreateTemp("", "banger-gitconfig-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpPath := tmpFile.Name()
|
|
if _, err := tmpFile.Write(existing); err != nil {
|
|
_ = tmpFile.Close()
|
|
_ = os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
if err := tmpFile.Close(); err != nil {
|
|
_ = os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
defer os.Remove(tmpPath)
|
|
|
|
if _, err := runner.Run(ctx, "git", "config", "--file", tmpPath, "user.name", identity.Name); err != nil {
|
|
return err
|
|
}
|
|
if _, err := runner.Run(ctx, "git", "config", "--file", tmpPath, "user.email", identity.Email); err != nil {
|
|
return err
|
|
}
|
|
_, err = runner.RunSudo(ctx, "install", "-m", "644", tmpPath, gitConfigPath)
|
|
return err
|
|
}
|
|
|
|
func (d *Daemon) warnOpencodeAuthSyncSkipped(vm model.VMRecord, hostPath string, err error) {
|
|
if d.logger == nil || err == nil {
|
|
return
|
|
}
|
|
d.logger.Warn("guest opencode auth sync skipped", append(vmLogAttrs(vm), "host_path", hostPath, "error", err.Error())...)
|
|
}
|
|
|
|
func (d *Daemon) warnClaudeAuthSyncSkipped(vm model.VMRecord, hostPath string, err error) {
|
|
if d.logger == nil || err == nil {
|
|
return
|
|
}
|
|
d.logger.Warn("guest claude auth sync skipped", append(vmLogAttrs(vm), "host_path", hostPath, "error", err.Error())...)
|
|
}
|
|
|
|
func (d *Daemon) warnPiAuthSyncSkipped(vm model.VMRecord, hostPath string, err error) {
|
|
if d.logger == nil || err == nil {
|
|
return
|
|
}
|
|
d.logger.Warn("guest pi auth sync skipped", append(vmLogAttrs(vm), "host_path", hostPath, "error", err.Error())...)
|
|
}
|
|
|
|
func (d *Daemon) warnGitIdentitySyncSkipped(vm model.VMRecord, source string, err error) {
|
|
if d.logger == nil || err == nil {
|
|
return
|
|
}
|
|
d.logger.Warn("guest git identity sync skipped", append(vmLogAttrs(vm), "source", source, "error", err.Error())...)
|
|
}
|
|
|
|
func mergeAuthorizedKey(existing, managed []byte) []byte {
|
|
managedLine := strings.TrimSpace(string(managed))
|
|
if managedLine == "" {
|
|
return append([]byte(nil), existing...)
|
|
}
|
|
|
|
lines := strings.Split(strings.ReplaceAll(string(existing), "\r\n", "\n"), "\n")
|
|
out := make([]string, 0, len(lines)+1)
|
|
found := false
|
|
for _, line := range lines {
|
|
line = strings.TrimRight(line, "\r")
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if trimmed == managedLine {
|
|
found = true
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
if !found {
|
|
out = append(out, managedLine)
|
|
}
|
|
return []byte(strings.Join(out, "\n") + "\n")
|
|
}
|