Sync host opencode auth into guest work disks
Refresh guest opencode auth from the host at VM start so guest opencode can reuse the local login without baking secrets into managed images. Reuse the existing work-disk preparation path to copy ~/.local/share/opencode/auth.json into /root/.local/share/opencode/auth.json with mode 0600, and warn and skip when the host file is missing or unreadable so any existing guest auth stays in place. Add daemon coverage for copy, replacement, and warn-and-skip cases, document the restart behavior in the README, and validate with go test ./... plus make build. Existing VMs pick the new auth up on their next restart.
This commit is contained in:
parent
786d235f7f
commit
8bcc767824
4 changed files with 256 additions and 1 deletions
|
|
@ -204,7 +204,10 @@ func (workDiskCapability) PrepareHost(ctx context.Context, d *Daemon, vm *model.
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.ensureAuthorizedKeyOnWorkDisk(ctx, vm, image, prep)
|
||||
if err := d.ensureAuthorizedKeyOnWorkDisk(ctx, vm, image, prep); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.ensureOpencodeAuthOnWorkDisk(ctx, vm)
|
||||
}
|
||||
|
||||
func (workDiskCapability) AddDoctorChecks(_ context.Context, d *Daemon, report *system.Report) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ var (
|
|||
vsockReadyPoll = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
const (
|
||||
workDiskOpencodeAuthDirRelativePath = ".local/share/opencode"
|
||||
workDiskOpencodeAuthRelativePath = workDiskOpencodeAuthDirRelativePath + "/auth.json"
|
||||
hostOpencodeAuthDefaultDisplayPath = "~/" + workDiskOpencodeAuthRelativePath
|
||||
)
|
||||
|
||||
func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm model.VMRecord, err error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
|
@ -927,6 +933,70 @@ func (d *Daemon) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm *model.VM
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *Daemon) ensureOpencodeAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
|
||||
hostAuthPath, err := resolveHostOpencodeAuthPath()
|
||||
if err != nil {
|
||||
d.warnOpencodeAuthSyncSkipped(*vm, hostOpencodeAuthDefaultDisplayPath, err)
|
||||
return nil
|
||||
}
|
||||
authData, err := os.ReadFile(hostAuthPath)
|
||||
if err != nil {
|
||||
d.warnOpencodeAuthSyncSkipped(*vm, hostAuthPath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
vmCreateStage(ctx, "prepare_work_disk", "syncing opencode auth")
|
||||
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
|
||||
}
|
||||
|
||||
authDir := filepath.Join(workMount, workDiskOpencodeAuthDirRelativePath)
|
||||
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", authDir); err != nil {
|
||||
return err
|
||||
}
|
||||
authPath := filepath.Join(workMount, workDiskOpencodeAuthRelativePath)
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "banger-opencode-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 = d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func resolveHostOpencodeAuthPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, workDiskOpencodeAuthRelativePath), nil
|
||||
}
|
||||
|
||||
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 mergeAuthorizedKey(existing, managed []byte) []byte {
|
||||
managedLine := strings.TrimSpace(string(managed))
|
||||
if managedLine == "" {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
|
|
@ -807,6 +808,185 @@ func TestEnsureAuthorizedKeyOnWorkDiskRepairsNestedRootLayout(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEnsureOpencodeAuthOnWorkDiskCopiesHostAuth(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
hostAuthPath := filepath.Join(homeDir, workDiskOpencodeAuthRelativePath)
|
||||
if err := os.MkdirAll(filepath.Dir(hostAuthPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(host auth dir): %v", err)
|
||||
}
|
||||
hostAuth := []byte("{\"provider\":\"openai\"}\n")
|
||||
if err := os.WriteFile(hostAuthPath, hostAuth, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(host auth): %v", err)
|
||||
}
|
||||
|
||||
workDiskDir := t.TempDir()
|
||||
d := &Daemon{runner: &filesystemRunner{t: t}}
|
||||
vm := testVM("auth-sync", "image-auth-sync", "172.16.0.63")
|
||||
vm.Runtime.WorkDiskPath = workDiskDir
|
||||
|
||||
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
|
||||
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
|
||||
}
|
||||
|
||||
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
|
||||
got, err := os.ReadFile(guestAuthPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(guest auth): %v", err)
|
||||
}
|
||||
if string(got) != string(hostAuth) {
|
||||
t.Fatalf("guest auth = %q, want %q", string(got), string(hostAuth))
|
||||
}
|
||||
info, err := os.Stat(guestAuthPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(guest auth): %v", err)
|
||||
}
|
||||
if gotMode := info.Mode().Perm(); gotMode != 0o600 {
|
||||
t.Fatalf("guest auth mode = %o, want 600", gotMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureOpencodeAuthOnWorkDiskReplacesExistingGuestAuth(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
hostAuthPath := filepath.Join(homeDir, workDiskOpencodeAuthRelativePath)
|
||||
if err := os.MkdirAll(filepath.Dir(hostAuthPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(host auth dir): %v", err)
|
||||
}
|
||||
hostAuth := []byte("{\"token\":\"fresh\"}\n")
|
||||
if err := os.WriteFile(hostAuthPath, hostAuth, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(host auth): %v", err)
|
||||
}
|
||||
|
||||
workDiskDir := t.TempDir()
|
||||
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
|
||||
if err := os.MkdirAll(filepath.Dir(guestAuthPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(guest auth dir): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(guestAuthPath, []byte("{\"token\":\"stale\"}\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(guest auth): %v", err)
|
||||
}
|
||||
|
||||
d := &Daemon{runner: &filesystemRunner{t: t}}
|
||||
vm := testVM("auth-replace", "image-auth-replace", "172.16.0.64")
|
||||
vm.Runtime.WorkDiskPath = workDiskDir
|
||||
|
||||
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
|
||||
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(guestAuthPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(guest auth): %v", err)
|
||||
}
|
||||
if string(got) != string(hostAuth) {
|
||||
t.Fatalf("guest auth = %q, want %q", string(got), string(hostAuth))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureOpencodeAuthOnWorkDiskWarnsAndSkipsWhenHostAuthMissing(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
workDiskDir := t.TempDir()
|
||||
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
|
||||
if err := os.MkdirAll(filepath.Dir(guestAuthPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(guest auth dir): %v", err)
|
||||
}
|
||||
original := []byte("{\"token\":\"keep\"}\n")
|
||||
if err := os.WriteFile(guestAuthPath, original, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(guest auth): %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger, _, err := newDaemonLogger(&buf, "info")
|
||||
if err != nil {
|
||||
t.Fatalf("newDaemonLogger: %v", err)
|
||||
}
|
||||
|
||||
d := &Daemon{
|
||||
runner: &filesystemRunner{t: t},
|
||||
logger: logger,
|
||||
}
|
||||
vm := testVM("auth-missing", "image-auth-missing", "172.16.0.65")
|
||||
vm.Runtime.WorkDiskPath = workDiskDir
|
||||
|
||||
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
|
||||
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(guestAuthPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(guest auth): %v", err)
|
||||
}
|
||||
if string(got) != string(original) {
|
||||
t.Fatalf("guest auth = %q, want preserved %q", string(got), string(original))
|
||||
}
|
||||
|
||||
entries := parseLogEntries(t, buf.Bytes())
|
||||
if !hasLogEntry(entries, map[string]string{
|
||||
"msg": "guest opencode auth sync skipped",
|
||||
"vm_name": vm.Name,
|
||||
"host_path": filepath.Join(homeDir, workDiskOpencodeAuthRelativePath),
|
||||
}) {
|
||||
t.Fatalf("expected warn log, got %v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureOpencodeAuthOnWorkDiskWarnsAndSkipsWhenHostAuthUnreadable(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
hostAuthPath := filepath.Join(homeDir, workDiskOpencodeAuthRelativePath)
|
||||
if err := os.MkdirAll(hostAuthPath, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(host auth path as dir): %v", err)
|
||||
}
|
||||
|
||||
workDiskDir := t.TempDir()
|
||||
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
|
||||
if err := os.MkdirAll(filepath.Dir(guestAuthPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(guest auth dir): %v", err)
|
||||
}
|
||||
original := []byte("{\"token\":\"keep\"}\n")
|
||||
if err := os.WriteFile(guestAuthPath, original, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(guest auth): %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger, _, err := newDaemonLogger(&buf, "info")
|
||||
if err != nil {
|
||||
t.Fatalf("newDaemonLogger: %v", err)
|
||||
}
|
||||
|
||||
d := &Daemon{
|
||||
runner: &filesystemRunner{t: t},
|
||||
logger: logger,
|
||||
}
|
||||
vm := testVM("auth-unreadable", "image-auth-unreadable", "172.16.0.66")
|
||||
vm.Runtime.WorkDiskPath = workDiskDir
|
||||
|
||||
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
|
||||
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(guestAuthPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(guest auth): %v", err)
|
||||
}
|
||||
if string(got) != string(original) {
|
||||
t.Fatalf("guest auth = %q, want preserved %q", string(got), string(original))
|
||||
}
|
||||
|
||||
entries := parseLogEntries(t, buf.Bytes())
|
||||
if !hasLogEntry(entries, map[string]string{
|
||||
"msg": "guest opencode auth sync skipped",
|
||||
"vm_name": vm.Name,
|
||||
"host_path": hostAuthPath,
|
||||
"error": "is a directory",
|
||||
}) {
|
||||
t.Fatalf("expected warn log, got %v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateVMRejectsNonPositiveCPUAndMemory(t *testing.T) {
|
||||
d := &Daemon{}
|
||||
if _, err := d.CreateVM(context.Background(), api.VMCreateParams{VCPUCount: ptr(0)}); err == nil || !strings.Contains(err.Error(), "vcpu must be a positive integer") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue