mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 01:37:05 +00:00
feat(snapshots): preserve stack dossiers with fleet snapshots (#1339)
* feat(snapshots): preserve stack dossiers with fleet snapshots Fleet snapshots can now optionally capture each stack's Dossier notes alongside its compose and .env files, so a recovery restores the operational knowledge around a stack, not just its configuration. - Opt-in global setting "snapshot_documentation" (default off), toggled from the renamed Fleet settings section. - Capture reads local dossiers from the database and remote dossiers over the Distributed API proxy; only stacks with notes are recorded, and secret values are never included. - Captured notes are stored encrypted at rest in a new fleet_snapshots column and surfaced in the snapshot detail view behind a badge. - Cloud and downloaded archives gain a documentation.json (archive_version 2). - Restore stays conservative: dossier notes are written back only when the operator explicitly opts in, on both single-stack and restore-all paths. - Existing snapshots and archives remain valid; behavior is unchanged when the setting is off. * fix(snapshots): harden dossier-notes restore against bad input and partial failures Address review findings on the documentation-snapshots restore path: - Parse `restoreNotes` strictly (=== true) on single-stack restore, matching restore-all, so a stray non-boolean can never opt in to overwriting notes. - Guard findSnapshotDossier: require an array of stacks and real dossier content, so a malformed or all-blank entry can't clobber current notes. - Make the dossier-notes write non-fatal relative to the file restore: a notes failure (e.g. a remote dossier PUT) is caught, reported via `notesError`, and no longer 500s the single restore or fails the stack in restore-all once the files are already written. - Surface the partial outcome in the UI: a warning toast on single restore, a summary note on restore-all, and gate the "Documentation captured" badge and restore-all notes control on captured stacks while rendering capture warnings. Adds tests for strict parsing, malformed/blank blobs, remote notes restore (success + non-fatal failure, single and bulk), and scheduled capture-on. * fix(snapshots): drop unused binding in restore-all remote notes test The restore-all remote notes test destructured a node id it never uses (restore-all is driven by snapshot id alone), tripping no-unused-vars and failing the lint step. Bind only the snapshot id.
This commit is contained in:
@@ -252,11 +252,12 @@ export class CloudBackupService {
|
||||
const snapshot = db.getSnapshot(snapshotId);
|
||||
if (!snapshot) throw new Error(`Snapshot ${snapshotId} not found.`);
|
||||
const files = db.getSnapshotFiles(snapshotId);
|
||||
const documentation = db.getSnapshotDocumentation(snapshotId);
|
||||
const objectKey = this.buildObjectKey(cfg, snapshot.id, snapshot.description, snapshot.created_at);
|
||||
|
||||
this.setStatus(snapshotId, { status: 'uploading', objectKey, updatedAt: Date.now() });
|
||||
try {
|
||||
const archive = await this.buildArchive(snapshot, files);
|
||||
const archive = await this.buildArchive(snapshot, files, documentation);
|
||||
const { client, sdk } = await this.buildS3Client(cfg);
|
||||
await client.send(new sdk.PutObjectCommand({
|
||||
Bucket: cfg.bucket,
|
||||
@@ -364,6 +365,7 @@ export class CloudBackupService {
|
||||
private async buildArchive(
|
||||
snapshot: { id: number; description: string; created_by: string; node_count: number; stack_count: number; skipped_nodes: string; created_at: number },
|
||||
files: FleetSnapshotFile[],
|
||||
documentation = '',
|
||||
): Promise<Buffer> {
|
||||
const pack = tar.pack();
|
||||
const metadata = {
|
||||
@@ -374,11 +376,19 @@ export class CloudBackupService {
|
||||
node_count: snapshot.node_count,
|
||||
stack_count: snapshot.stack_count,
|
||||
skipped_nodes: safeParseJson(snapshot.skipped_nodes, []),
|
||||
has_documentation: documentation !== '',
|
||||
instance_id: this.getInstanceId(),
|
||||
archive_version: 1,
|
||||
// Version 2 adds the optional documentation.json entry; readers of
|
||||
// version 1 archives simply will not find that file.
|
||||
archive_version: 2,
|
||||
};
|
||||
pack.entry({ name: 'metadata.json' }, JSON.stringify(metadata, null, 2));
|
||||
|
||||
// Captured Stack Dossier metadata travels with the archive when present.
|
||||
if (documentation !== '') {
|
||||
pack.entry({ name: 'documentation.json' }, documentation);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const safeNodeName = sanitizePathSegment(file.node_name);
|
||||
const safeStackName = sanitizePathSegment(file.stack_name);
|
||||
|
||||
Reference in New Issue
Block a user