feat(breakglass): add "back up a world now" console peer (§B4 Sync)

Adds a break-glass console operation that snapshots a stopped world by
calling the felis-api internal face while the API is alive, rather than
rendering the backup Job locally: the Job needs felis-api deployment
coordinates the console does not hold.

The peer resolves the felis-api-internal ClusterIP Service + service
token from the control namespace, POSTs the internal backup endpoint
with the operator os_user for audit attribution, and maps 409/503/404
to friendly outcome cards. Core decision logic lives in backupnow.go
(unit-tested against a fake client + httptest); tui_backupnow.go is the
untested bubbletea glue mirroring tui_halt.go.
This commit is contained in:
flyemoji committed 2026-07-07 19:32:13 +09:00
1 parent 4918d93412
commit fc748d3462
9 files changed
+594 -2

No files matched your search

+20
View File
@@ -5,6 +5,7 @@ import (
"strings"
"felis.lolicon.best/internal/cfsetup"
"felis.lolicon.best/internal/platform"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@@ -223,6 +224,10 @@ func (m *rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.adopt(newOperatorModel(m.ctx, m.store, m.osUser))
case bgHaltServer:
return m.adopt(newHaltModel(m.ctx, m.store, m.namespace, m.osUser))
case bgSyncBackup:
// The picker lists workload servers (m.namespace); the felis-api-internal
// Service + service token the peer dials live in the control namespace.
return m.adopt(newBackupModel(m.ctx, m.namespace, platform.DefaultControlNamespace, m.osUser))
default:
return m.adopt(newOwnerModel(m.ctx, m.store, m.osUser, m.adminExists))
}
@@ -248,6 +253,21 @@ func (m *rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, tea.Quit
case backupResultMsg:
// Sync backup is terminal in break-glass, mirroring halt: record the durable
// summary and quit. A resolve/HTTP failure routes through the error path; an
// empty fleet or cancel leaves backedUp false.
if msg.err != nil {
m.err = msg.err
return m, tea.Quit
}
if msg.done {
m.result.backedUp = true
m.result.backupServer = msg.outcome.name
m.result.backupStatus = msg.outcome.status
}
return m, tea.Quit
case ownerResultMsg:
if msg.err != nil {
m.err = msg.err