diff --git a/assets/design-export/json/DPrYX.json b/assets/design-export/json/DPrYX.json index b21abe449..8dcec0e2c 100644 --- a/assets/design-export/json/DPrYX.json +++ b/assets/design-export/json/DPrYX.json @@ -1,277 +1,447 @@ -{ - "type": "frame", - "id": "DPrYX", - "x": 0, - "y": 59782, - "name": "Screen/Backups — Index", - "theme": { - "semantic": "dark" - }, - "clip": true, - "width": 1440, - "height": 900, - "fill": "$background/background", - "children": [ - { - "id": "PdJtd", - "type": "ref", - "ref": "kKFX9", - "name": "App Sidebar", - "height": "fill_container", - "descendants": { - "iTbd8": { - "fill": "#00000000" - }, - "iTbd8/ghUsw": { - "fill": "$foreground/foreground" - }, - "iTbd8/bpSQ7": { - "fill": "$foreground/foreground" - }, - "NADGL": { - "fill": "$accent/soft" - }, - "NADGL/Sr2Q5": { - "fill": "$accent/soft-foreground" - }, - "NADGL/Ra9NK": { - "fill": "$accent/soft-foreground" - } - } - }, - { - "type": "frame", - "id": "vMQ5B", - "name": "Main", - "width": "fill_container", - "height": "fill_container", - "fill": "$background/background", - "layout": "vertical", - "children": [ - { - "id": "G3j7E", - "type": "ref", - "ref": "gu5WY", - "name": "Top Bar", - "width": "fill_container", - "descendants": { - "Gl7AY": { - "content": "Backups" - } - } - }, - { - "type": "frame", - "id": "dtc83", - "name": "Body", - "width": "fill_container", - "height": "fill_container", - "fill": "$background/background", - "layout": "vertical", - "gap": 20, - "padding": 24, - "children": [ - { - "id": "hQCtf", - "type": "ref", - "ref": "xCDF7", - "name": "Page Header", - "width": "fill_container", - "descendants": { - "spCmk": { - "content": "Backups" - }, - "VbHwU": { - "content": "Snapshots, schedules, and restores across your authorized clusters." - }, - "TgdLz": { - "type": "frame", - "id": "TFxXl", - "name": "phActions", - "height": 36, - "gap": 12, - "alignItems": "center", - "children": [ - { - "id": "hCmIr", - "type": "ref", - "ref": "tpKRk", - "name": "Back Up Now Button", - "descendants": { - "q58In/Xpn7K": { - "library": "lucide", - "icon": "plus" - }, - "q58In/U6q13B": { - "content": "Back up now" - } - } - } - ] - } - } - }, - { - "id": "Y6HHT", - "type": "ref", - "ref": "oIJGu", - "name": "tabsBar" - }, - { - "id": "qmZz8", - "type": "ref", - "ref": "wKe4F", - "name": "Filter Row", - "width": "fill_container" - }, - { - "id": "KYn2g", - "type": "ref", - "ref": "ECbbo", - "name": "Backups Table", - "width": "fill_container", - "height": "fit_content", - "children": [ - { - "id": "O8swk", - "type": "ref", - "ref": "eafUs", - "name": "Head Row", - "width": "fill_container", - "children": [ - { - "id": "h45qq6", - "type": "ref", - "ref": "DYYKO", - "name": "HC NAME", - "width": "fill_container", - "descendants": { - "SIu74": { - "content": "NAME", - "fontSize": 11, - "fontWeight": "700", - "letterSpacing": 0.55 - } - } - }, - { - "id": "pEKZ7", - "type": "ref", - "ref": "DYYKO", - "name": "HC SERVER", - "width": 216, - "descendants": { - "SIu74": { - "content": "SERVER", - "fontSize": 11, - "fontWeight": "700", - "letterSpacing": 0.55 - } - } - }, - { - "id": "FIahb", - "type": "ref", - "ref": "DYYKO", - "name": "HC PHASE", - "width": 196, - "descendants": { - "SIu74": { - "content": "PHASE", - "fontSize": 11, - "fontWeight": "700", - "letterSpacing": 0.55 - } - } - }, - { - "id": "oJZzx", - "type": "ref", - "ref": "DYYKO", - "name": "HC SIZE", - "width": 160, - "descendants": { - "SIu74": { - "content": "SIZE", - "fontSize": 11, - "fontWeight": "700", - "letterSpacing": 0.55 - } - } - }, - { - "id": "i2SHb", - "type": "ref", - "ref": "DYYKO", - "name": "HC COMPLETED", - "width": 288, - "descendants": { - "SIu74": { - "content": "COMPLETED", - "fontSize": 11, - "fontWeight": "700", - "letterSpacing": 0.55 - } - } - }, - { - "id": "qRhCw", - "type": "ref", - "ref": "DYYKO", - "name": "HC ACTIONS", - "width": 88, - "descendants": { - "SIu74": { - "content": "", - "fontSize": 11, - "fontWeight": "700", - "letterSpacing": 0.55 - } - } - } - ] - }, - { - "id": "tIfUL", - "type": "ref", - "ref": "GUHQo", - "name": "Body", - "width": "fill_container", - "children": [ - { - "id": "hQAfw", - "type": "ref", - "ref": "tDY4O", - "name": "Empty Row", - "width": "fill_container", - "children": [ - { - "id": "AhRTF", - "type": "ref", - "ref": "TDRJE", - "name": "Empty Cell", - "width": "fill_container", - "padding": [ - 40, - 16 - ], - "justifyContent": "center", - "descendants": { - "HNAOu": { - "content": "No backups yet. Use “Back up now” to create the first one.", - "fill": "$muted" - } - } - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] -} +{ + "type": "frame", + "id": "DPrYX", + "x": 0, + "y": 59782, + "name": "Screen/Backups — Index", + "theme": { + "semantic": "dark" + }, + "clip": true, + "width": 1440, + "height": 900, + "fill": "$background/background", + "children": [ + { + "id": "PdJtd", + "type": "ref", + "ref": "kKFX9", + "name": "App Sidebar", + "height": "fill_container", + "descendants": { + "iTbd8": { + "fill": "#00000000" + }, + "iTbd8/ghUsw": { + "fill": "$foreground/foreground" + }, + "iTbd8/bpSQ7": { + "fill": "$foreground/foreground" + }, + "NADGL": { + "fill": "$accent/soft" + }, + "NADGL/Sr2Q5": { + "fill": "$accent/soft-foreground" + }, + "NADGL/Ra9NK": { + "fill": "$accent/soft-foreground" + } + } + }, + { + "type": "frame", + "id": "vMQ5B", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$background/background", + "layout": "vertical", + "children": [ + { + "id": "G3j7E", + "type": "ref", + "ref": "gu5WY", + "name": "Top Bar", + "width": "fill_container", + "descendants": { + "Gl7AY": { + "content": "Backups" + } + } + }, + { + "type": "frame", + "id": "dtc83", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "fill": "$background/background", + "layout": "vertical", + "gap": 20, + "padding": 24, + "children": [ + { + "id": "hQCtf", + "type": "ref", + "ref": "xCDF7", + "name": "Page Header", + "width": "fill_container", + "descendants": { + "spCmk": { + "content": "Backups" + }, + "VbHwU": { + "content": "Snapshots, schedules, and restores across your authorized clusters." + }, + "TgdLz": { + "type": "frame", + "id": "TFxXl", + "name": "phActions", + "height": 36, + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "hCmIr", + "type": "ref", + "ref": "tpKRk", + "name": "Back Up Now Button", + "descendants": { + "q58In/Xpn7K": { + "library": "lucide", + "icon": "plus" + }, + "q58In/U6q13B": { + "content": "Back up now" + } + } + } + ] + } + } + }, + { + "id": "Y6HHT", + "type": "ref", + "ref": "oIJGu", + "name": "tabsBar" + }, + { + "id": "qmZz8", + "type": "ref", + "ref": "wKe4F", + "name": "Filter Row", + "width": "fill_container", + "descendants": { + "g9iDB": { + "content": "1 of 1 backups" + } + } + }, + { + "id": "KYn2g", + "type": "ref", + "ref": "ECbbo", + "name": "Backups Table", + "width": "fill_container", + "height": "fit_content", + "children": [ + { + "id": "O8swk", + "type": "ref", + "ref": "eafUs", + "name": "Head Row", + "width": "fill_container", + "children": [ + { + "id": "h45qq6", + "type": "ref", + "ref": "DYYKO", + "name": "HC NAME", + "width": "fill_container", + "descendants": { + "SIu74": { + "content": "NAME", + "fontSize": 11, + "fontWeight": "700", + "letterSpacing": 0.55 + } + } + }, + { + "id": "pEKZ7", + "type": "ref", + "ref": "DYYKO", + "name": "HC SERVER", + "width": 216, + "descendants": { + "SIu74": { + "content": "SERVER", + "fontSize": 11, + "fontWeight": "700", + "letterSpacing": 0.55 + } + } + }, + { + "id": "FIahb", + "type": "ref", + "ref": "DYYKO", + "name": "HC PHASE", + "width": 196, + "descendants": { + "SIu74": { + "content": "PHASE", + "fontSize": 11, + "fontWeight": "700", + "letterSpacing": 0.55 + } + } + }, + { + "id": "oJZzx", + "type": "ref", + "ref": "DYYKO", + "name": "HC SIZE", + "width": 160, + "descendants": { + "SIu74": { + "content": "SIZE", + "fontSize": 11, + "fontWeight": "700", + "letterSpacing": 0.55 + } + } + }, + { + "id": "i2SHb", + "type": "ref", + "ref": "DYYKO", + "name": "HC COMPLETED", + "width": 288, + "descendants": { + "SIu74": { + "content": "COMPLETED", + "fontSize": 11, + "fontWeight": "700", + "letterSpacing": 0.55 + } + } + }, + { + "id": "qRhCw", + "type": "ref", + "ref": "DYYKO", + "name": "HC ACTIONS", + "width": 88, + "descendants": { + "SIu74": { + "content": "", + "fontSize": 11, + "fontWeight": "700", + "letterSpacing": 0.55 + } + } + } + ] + }, + { + "id": "tIfUL", + "type": "ref", + "ref": "GUHQo", + "name": "Body", + "width": "fill_container", + "children": [ + { + "id": "hQAfw", + "type": "ref", + "ref": "tDY4O", + "name": "Empty Row", + "width": "fill_container(1124)", + "enabled": false, + "children": [ + { + "id": "AhRTF", + "type": "ref", + "ref": "TDRJE", + "name": "Empty Cell", + "width": "fill_container", + "padding": [ + 40, + 16 + ], + "justifyContent": "center", + "descendants": { + "HNAOu": { + "content": "No backups yet. Use “Back up now” to create the first one.", + "fill": "$muted" + } + } + } + ] + }, + { + "type": "frame", + "id": "lhfrX", + "name": "Completed Restic backup", + "width": "fill_container", + "height": 54, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "iccWL", + "name": "Backup name", + "width": "fill_container", + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l4BIu", + "name": "Backup name", + "fill": "$foreground/foreground", + "content": "survival-nightly", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jBIfp", + "name": "Server", + "width": 216, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "u0T4s", + "name": "Server", + "fill": "$foreground/foreground", + "content": "mc-survival", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ciPec", + "name": "Phase", + "width": 196, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f3V71r", + "name": "Phase", + "fill": "$foreground/foreground", + "content": "Succeeded", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Wc15b", + "name": "Logical size and packed bytes added", + "width": 160, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "children": [ + { + "type": "text", + "id": "lpzAb", + "name": "Logical size", + "fill": "$foreground/foreground", + "content": "1434Mi", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dYsvp", + "name": "Packed bytes added", + "fill": "$muted", + "content": "14 KB added", + "fontFamily": "$typography/font-mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D7aND", + "name": "Completed", + "width": 288, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ha4wo", + "name": "Completed", + "fill": "$foreground/foreground", + "content": "3h ago", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "aiBeF", + "name": "Action", + "width": 88, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I7Enl", + "name": "Action", + "fill": "$foreground/foreground", + "content": "Restore", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/assets/design-export/json/pssCT.json b/assets/design-export/json/pssCT.json index 1c3bc869a..ae73577a9 100644 --- a/assets/design-export/json/pssCT.json +++ b/assets/design-export/json/pssCT.json @@ -143,7 +143,7 @@ "fill": "$accent/accent" }, "vj2Ru": { - "content": "—" + "content": "3h ago" }, "h0n7zo": { "enabled": false @@ -199,7 +199,7 @@ "fill": "#895AF6" }, "vj2Ru": { - "content": "0" + "content": "1" }, "h0n7zo": { "enabled": false @@ -214,7 +214,7 @@ "width": "fill_container", "descendants": { "Nd4Wf": { - "content": "TOTAL SIZE" + "content": "TOTAL LOGICAL SIZE" }, "QdcEm": { "type": "icon", @@ -227,7 +227,7 @@ "fill": "$warning/warning" }, "vj2Ru": { - "content": "—" + "content": "1.4 GB" }, "h0n7zo": { "enabled": false @@ -419,6 +419,131 @@ } } ] + }, + { + "type": "frame", + "id": "i5P60A", + "name": "Backups", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "larZ1", + "name": "Completed Restic backup", + "width": "fill_container", + "height": 54, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LWyaJ", + "name": "Backup name", + "width": "fill_container", + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "swtSj", + "name": "Backup name", + "fill": "$foreground/foreground", + "content": "survival-nightly", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "SAxWl", + "name": "Phase", + "width": 120, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BxNZC", + "name": "Phase", + "fill": "$foreground/foreground", + "content": "Succeeded", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b7ipb", + "name": "Logical size and packed bytes added", + "width": 100, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "children": [ + { + "type": "text", + "id": "hjNfI", + "name": "Logical size", + "fill": "$foreground/foreground", + "content": "1434Mi", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yXBnm", + "name": "Packed bytes added", + "fill": "$muted", + "content": "14 KB added", + "fontFamily": "$typography/font-mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "vL5Li", + "name": "Completed", + "width": 140, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t8xv6", + "name": "Completed", + "fill": "$foreground/foreground", + "content": "3h ago", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] } ] } diff --git a/assets/design-export/json/zhLZN.json b/assets/design-export/json/zhLZN.json index 47781a21f..6357ba5f4 100644 --- a/assets/design-export/json/zhLZN.json +++ b/assets/design-export/json/zhLZN.json @@ -319,11 +319,21 @@ "type": "text", "id": "ws91g", "fill": "$foreground/foreground", - "content": "1.4 GiB", + "content": "1434Mi", "lineHeight": 1.4285714285714286, "fontFamily": "$typography/font-sans", "fontSize": 14, "fontWeight": "normal" + }, + { + "type": "text", + "id": "nzp8v", + "name": "Packed data added by this run", + "fill": "$muted", + "content": "14 KB added", + "fontFamily": "$typography/font-mono", + "fontSize": 12, + "fontWeight": "normal" } ] }, diff --git a/assets/design-export/screenshots/DPrYX.png b/assets/design-export/screenshots/DPrYX.png index 223509a89..9dc308f32 100644 Binary files a/assets/design-export/screenshots/DPrYX.png and b/assets/design-export/screenshots/DPrYX.png differ diff --git a/assets/design-export/screenshots/pssCT.png b/assets/design-export/screenshots/pssCT.png index 66b56f083..7ded443ef 100644 Binary files a/assets/design-export/screenshots/pssCT.png and b/assets/design-export/screenshots/pssCT.png differ diff --git a/assets/design-export/screenshots/zhLZN.png b/assets/design-export/screenshots/zhLZN.png index 553c40186..7009cba0c 100644 Binary files a/assets/design-export/screenshots/zhLZN.png and b/assets/design-export/screenshots/zhLZN.png differ diff --git a/changelog/unreleased/933-restic-backup-accounting.md b/changelog/unreleased/933-restic-backup-accounting.md new file mode 100644 index 000000000..297611310 --- /dev/null +++ b/changelog/unreleased/933-restic-backup-accounting.md @@ -0,0 +1,3 @@ +# Restic backup accounting + +Restic can reuse the previous snapshot for the same server after a worker or game pod is replaced. Backup lists and details show the packed data added by each new Restic run alongside logical snapshot size. diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backups.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backups.yaml index 63d29c4f4..15ef749eb 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backups.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backups.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: backups.gameplane.local spec: @@ -147,6 +147,15 @@ spec: status: description: BackupStatus is the observed state of a Backup. properties: + bytesAdded: + anyOf: + - type: integer + - type: string + description: |- + BytesAdded is the packed data Restic added during this backup. Zero + is a measured result. CSI and older Restic summaries omit this field. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true captureManifest: description: |- CaptureManifest is recorded once, before the first capture worker starts. @@ -320,8 +329,8 @@ spec: - type: integer - type: string description: |- - Size is the on-disk size of the snapshot, as reported by the - backup driver. Not always exact for incremental snapshots. + Size is the logical size reported by the driver (total bytes processed + for Restic, restore size for a CSI snapshot). pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true snapshotID: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backupschedules.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backupschedules.yaml index 4beb31166..aacd72160 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backupschedules.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backupschedules.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: backupschedules.gameplane.local spec: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_clusters.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_clusters.yaml index 18b3b8f55..300c7765f 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_clusters.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_clusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: clusters.gameplane.local spec: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gameservers.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gameservers.yaml index c9f4e670b..4b757c3d3 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gameservers.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gameservers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: gameservers.gameplane.local spec: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gametemplates.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gametemplates.yaml index fb1cca85b..24fe58f14 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gametemplates.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gametemplates.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: gametemplates.gameplane.local spec: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modules.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modules.yaml index 5d74582b1..1957ff80b 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modules.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modules.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: modules.gameplane.local spec: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modulesources.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modulesources.yaml index 8cf33a5ec..6bc4306c7 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modulesources.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modulesources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: modulesources.gameplane.local spec: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_networkcaptures.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_networkcaptures.yaml index f2cd36c44..56faeb1c8 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_networkcaptures.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_networkcaptures.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: networkcaptures.gameplane.local spec: diff --git a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_restores.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_restores.yaml index d2f47f0ec..c822962f9 100644 --- a/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_restores.yaml +++ b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_restores.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: restores.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_backups.yaml b/charts/gameplane/crd-manifests/gameplane.local_backups.yaml index 63d29c4f4..15ef749eb 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_backups.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_backups.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: backups.gameplane.local spec: @@ -147,6 +147,15 @@ spec: status: description: BackupStatus is the observed state of a Backup. properties: + bytesAdded: + anyOf: + - type: integer + - type: string + description: |- + BytesAdded is the packed data Restic added during this backup. Zero + is a measured result. CSI and older Restic summaries omit this field. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true captureManifest: description: |- CaptureManifest is recorded once, before the first capture worker starts. @@ -320,8 +329,8 @@ spec: - type: integer - type: string description: |- - Size is the on-disk size of the snapshot, as reported by the - backup driver. Not always exact for incremental snapshots. + Size is the logical size reported by the driver (total bytes processed + for Restic, restore size for a CSI snapshot). pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true snapshotID: diff --git a/charts/gameplane/crd-manifests/gameplane.local_backupschedules.yaml b/charts/gameplane/crd-manifests/gameplane.local_backupschedules.yaml index 4beb31166..aacd72160 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_backupschedules.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_backupschedules.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: backupschedules.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_clusters.yaml b/charts/gameplane/crd-manifests/gameplane.local_clusters.yaml index 18b3b8f55..300c7765f 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_clusters.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_clusters.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: clusters.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_gameservers.yaml b/charts/gameplane/crd-manifests/gameplane.local_gameservers.yaml index c9f4e670b..4b757c3d3 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_gameservers.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_gameservers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: gameservers.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_gametemplates.yaml b/charts/gameplane/crd-manifests/gameplane.local_gametemplates.yaml index fb1cca85b..24fe58f14 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_gametemplates.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_gametemplates.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: gametemplates.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_modules.yaml b/charts/gameplane/crd-manifests/gameplane.local_modules.yaml index 5d74582b1..1957ff80b 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_modules.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_modules.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: modules.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_modulesources.yaml b/charts/gameplane/crd-manifests/gameplane.local_modulesources.yaml index 8cf33a5ec..6bc4306c7 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_modulesources.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_modulesources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: modulesources.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_networkcaptures.yaml b/charts/gameplane/crd-manifests/gameplane.local_networkcaptures.yaml index f2cd36c44..56faeb1c8 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_networkcaptures.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_networkcaptures.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: networkcaptures.gameplane.local spec: diff --git a/charts/gameplane/crd-manifests/gameplane.local_restores.yaml b/charts/gameplane/crd-manifests/gameplane.local_restores.yaml index d2f47f0ec..c822962f9 100644 --- a/charts/gameplane/crd-manifests/gameplane.local_restores.yaml +++ b/charts/gameplane/crd-manifests/gameplane.local_restores.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96 + gameplane.local/crd-bundle-sha256: da89b28cb8f0dcf2acf5b70116f2da6e3b88880e0c884b9fb5a5f5391a09e98a controller-gen.kubebuilder.io/version: v0.22.0 name: restores.gameplane.local spec: diff --git a/design.pen b/design.pen index 876a2bbb1..4c657283d 100644 --- a/design.pen +++ b/design.pen @@ -1,5 +1,5 @@ { - "version": "2.21", + "version": "2.22", "children": [ { "type": "frame", @@ -9213,7 +9213,7 @@ "fill": "$accent/accent" }, "vj2Ru": { - "content": "—" + "content": "3h ago" }, "h0n7zo": { "enabled": false @@ -9269,7 +9269,7 @@ "fill": "#895AF6" }, "vj2Ru": { - "content": "0" + "content": "1" }, "h0n7zo": { "enabled": false @@ -9284,7 +9284,7 @@ "width": "fill_container", "descendants": { "Nd4Wf": { - "content": "TOTAL SIZE" + "content": "TOTAL LOGICAL SIZE" }, "QdcEm": { "type": "icon", @@ -9297,7 +9297,7 @@ "fill": "$warning/warning" }, "vj2Ru": { - "content": "—" + "content": "1.4 GB" }, "h0n7zo": { "enabled": false @@ -9489,6 +9489,131 @@ } } ] + }, + { + "type": "frame", + "id": "i5P60A", + "name": "Backups", + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "larZ1", + "name": "Completed Restic backup", + "width": "fill_container", + "height": 54, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LWyaJ", + "name": "Backup name", + "width": "fill_container", + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "swtSj", + "name": "Backup name", + "fill": "$foreground/foreground", + "content": "survival-nightly", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "SAxWl", + "name": "Phase", + "width": 120, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BxNZC", + "name": "Phase", + "fill": "$foreground/foreground", + "content": "Succeeded", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b7ipb", + "name": "Logical size and packed bytes added", + "width": 100, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "children": [ + { + "type": "text", + "id": "hjNfI", + "name": "Logical size", + "fill": "$foreground/foreground", + "content": "1434Mi", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yXBnm", + "name": "Packed bytes added", + "fill": "$muted", + "content": "14 KB added", + "fontFamily": "$typography/font-mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "vL5Li", + "name": "Completed", + "width": 140, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t8xv6", + "name": "Completed", + "fill": "$foreground/foreground", + "content": "3h ago", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] } ] } @@ -22067,7 +22192,12 @@ "type": "ref", "ref": "wKe4F", "name": "Filter Row", - "width": "fill_container" + "width": "fill_container", + "descendants": { + "g9iDB": { + "content": "1 of 1 backups" + } + } }, { "id": "KYn2g", @@ -22188,7 +22318,8 @@ "type": "ref", "ref": "tDY4O", "name": "Empty Row", - "width": "fill_container", + "width": "fill_container(1124)", + "enabled": false, "children": [ { "id": "AhRTF", @@ -22209,6 +22340,170 @@ } } ] + }, + { + "type": "frame", + "id": "lhfrX", + "name": "Completed Restic backup", + "width": "fill_container", + "height": 54, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "iccWL", + "name": "Backup name", + "width": "fill_container", + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l4BIu", + "name": "Backup name", + "fill": "$foreground/foreground", + "content": "survival-nightly", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jBIfp", + "name": "Server", + "width": 216, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "u0T4s", + "name": "Server", + "fill": "$foreground/foreground", + "content": "mc-survival", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ciPec", + "name": "Phase", + "width": 196, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f3V71r", + "name": "Phase", + "fill": "$foreground/foreground", + "content": "Succeeded", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Wc15b", + "name": "Logical size and packed bytes added", + "width": 160, + "layout": "vertical", + "gap": 2, + "padding": [ + 0, + 12 + ], + "children": [ + { + "type": "text", + "id": "lpzAb", + "name": "Logical size", + "fill": "$foreground/foreground", + "content": "1434Mi", + "fontFamily": "$typography/font-mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dYsvp", + "name": "Packed bytes added", + "fill": "$muted", + "content": "14 KB added", + "fontFamily": "$typography/font-mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D7aND", + "name": "Completed", + "width": 288, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ha4wo", + "name": "Completed", + "fill": "$foreground/foreground", + "content": "3h ago", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "aiBeF", + "name": "Action", + "width": 88, + "height": 48, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I7Enl", + "name": "Action", + "fill": "$foreground/foreground", + "content": "Restore", + "fontFamily": "$typography/font-sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] } ] } @@ -119503,11 +119798,21 @@ "type": "text", "id": "ws91g", "fill": "$foreground/foreground", - "content": "1.4 GiB", + "content": "1434Mi", "lineHeight": 1.4285714285714286, "fontFamily": "$typography/font-sans", "fontSize": 14, "fontWeight": "normal" + }, + { + "type": "text", + "id": "nzp8v", + "name": "Packed data added by this run", + "fill": "$muted", + "content": "14 KB added", + "fontFamily": "$typography/font-mono", + "fontSize": 12, + "fontWeight": "normal" } ] }, diff --git a/i18n/locales/en/app.json b/i18n/locales/en/app.json index af51c12f9..3a121eeba 100644 --- a/i18n/locales/en/app.json +++ b/i18n/locales/en/app.json @@ -1215,6 +1215,10 @@ "defaultMessage": "Language", "description": "Label beside the language select of each notification sink row on the admin Notifications card" }, + "web.backups.bytes_added": { + "defaultMessage": "{size} added", + "description": "Packed data added to the backup repository by this run. The size includes its unit." + }, "web.cluster_configuration.access": { "defaultMessage": "Access", "description": "Cluster configuration access section" @@ -7992,8 +7996,8 @@ "description": "Backups tab: warning note shown when the server has more than one backup schedule, advising to combine them to avoid duplicate backups" }, "web.routes.tabs.backups.total_size": { - "defaultMessage": "Total size", - "description": "Backups tab: stat card label for the combined size of all backups of this server" + "defaultMessage": "Total logical size", + "description": "Sum of the logical sizes reported by successful backups" }, "web.routes.tabs.backups.unavailable": { "defaultMessage": "Unavailable", diff --git a/operator/api/v1alpha1/backup_types.go b/operator/api/v1alpha1/backup_types.go index bddbb5088..dfded04a4 100644 --- a/operator/api/v1alpha1/backup_types.go +++ b/operator/api/v1alpha1/backup_types.go @@ -128,11 +128,16 @@ type BackupStatus struct { // +optional VolumeSnapshotContentName string `json:"volumeSnapshotContentName,omitempty"` - // Size is the on-disk size of the snapshot, as reported by the - // backup driver. Not always exact for incremental snapshots. + // Size is the logical size reported by the driver (total bytes processed + // for Restic, restore size for a CSI snapshot). // +optional Size *resource.Quantity `json:"size,omitempty"` + // BytesAdded is the packed data Restic added during this backup. Zero + // is a measured result. CSI and older Restic summaries omit this field. + // +optional + BytesAdded *resource.Quantity `json:"bytesAdded,omitempty"` + // Message is a human-readable reason when Phase=Failed. // +optional Message string `json:"message,omitempty"` diff --git a/operator/api/v1alpha1/zz_generated.deepcopy.go b/operator/api/v1alpha1/zz_generated.deepcopy.go index 9b349cc91..317f659a2 100644 --- a/operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -432,6 +432,11 @@ func (in *BackupStatus) DeepCopyInto(out *BackupStatus) { x := (*in).DeepCopy() *out = &x } + if in.BytesAdded != nil { + in, out := &in.BytesAdded, &out.BytesAdded + x := (*in).DeepCopy() + *out = &x + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) diff --git a/operator/config/crd/gameplane.local_backups.yaml b/operator/config/crd/gameplane.local_backups.yaml index 67fb20227..f24cf8b26 100644 --- a/operator/config/crd/gameplane.local_backups.yaml +++ b/operator/config/crd/gameplane.local_backups.yaml @@ -146,6 +146,15 @@ spec: status: description: BackupStatus is the observed state of a Backup. properties: + bytesAdded: + anyOf: + - type: integer + - type: string + description: |- + BytesAdded is the packed data Restic added during this backup. Zero + is a measured result. CSI and older Restic summaries omit this field. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true captureManifest: description: |- CaptureManifest is recorded once, before the first capture worker starts. @@ -319,8 +328,8 @@ spec: - type: integer - type: string description: |- - Size is the on-disk size of the snapshot, as reported by the - backup driver. Not always exact for incremental snapshots. + Size is the logical size reported by the driver (total bytes processed + for Restic, restore size for a CSI snapshot). pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true snapshotID: diff --git a/operator/internal/controller/backup_budgets_test.go b/operator/internal/controller/backup_budgets_test.go index 738274e96..3ab275066 100644 --- a/operator/internal/controller/backup_budgets_test.go +++ b/operator/internal/controller/backup_budgets_test.go @@ -510,7 +510,7 @@ func TestPreparedBackupPlacementAndResources(t *testing.T) { r := &BackupReconciler{HardSameNode: hard, BackupResources: &corev1.ResourceRequirements{}} b := guardBackup("copy") source := &backupWorkerSource{NodeName: "node-a", Tolerations: []corev1.Toleration{{Key: "games", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule}}} - spec := r.buildPreparedBackupPodSpec(b, &gameplanev1alpha1.GameTemplate{}, source, time.Now().Add(time.Minute)) + spec := r.buildPreparedBackupPodSpec(b, &gameplanev1alpha1.GameTemplate{}, source, "server-uid", time.Now().Add(time.Minute)) if spec.AutomountServiceAccountToken == nil || *spec.AutomountServiceAccountToken || len(spec.Tolerations) != 1 || spec.Tolerations[0].Key != "games" { t.Fatal("worker credentials or tolerations contract lost") } @@ -524,12 +524,28 @@ func TestPreparedBackupPlacementAndResources(t *testing.T) { t.Fatal("resource clearing or preparation probe missing") } source.NodeName = "" - if stopped := r.buildPreparedBackupPodSpec(b, &gameplanev1alpha1.GameTemplate{}, source, time.Now()); stopped.Affinity != nil { + if stopped := r.buildPreparedBackupPodSpec(b, &gameplanev1alpha1.GameTemplate{}, source, "server-uid", time.Now()); stopped.Affinity != nil { t.Fatal("stopped source invented a game node") } } } +func TestResticWorkerHostUsesBoundServerIdentity(t *testing.T) { + r := &BackupReconciler{} + for _, tc := range []struct{ backupName, uid, want string }{ + {"manual", "first-server", "gameplane-first-server"}, + {"nightly", "first-server", "gameplane-first-server"}, + {"nightly", "replacement-server", "gameplane-replacement-server"}, + } { + b := guardBackup(tc.backupName) + s := r.buildPreparedBackupPodSpec(b, &gameplanev1alpha1.GameTemplate{}, &backupWorkerSource{}, types.UID(tc.uid), time.Now()) + args := s.Containers[0].Args + if len(args) < 2 || args[len(args)-2] != "--host" || args[len(args)-1] != tc.want { + t.Fatalf("worker arguments %q", args) + } + } +} + // Execute the real gate script in CI with only its mount paths redirected to // temp directories. A fake restic records whether source capture was invoked. func TestBackupWorkerScriptRejectsExpiredAndReplayedTokens(t *testing.T) { diff --git a/operator/internal/controller/backup_controller.go b/operator/internal/controller/backup_controller.go index 9f61106a8..c199a46e4 100644 --- a/operator/internal/controller/backup_controller.go +++ b/operator/internal/controller/backup_controller.go @@ -533,6 +533,9 @@ func (r *BackupReconciler) mirrorJobStatus( switch { case err == nil && summary.SnapshotID != "": b.Status.SnapshotID = summary.SnapshotID + if n := summary.PackedBytesAdded; n != nil && *n >= 0 { + b.Status.BytesAdded = resource.NewQuantity(*n, resource.BinarySI) + } if summary.TotalBytesProcessed > 0 { q := resource.MustParse(strconv.FormatInt(summary.TotalBytesProcessed, 10)) b.Status.Size = &q diff --git a/operator/internal/controller/backup_envtest_test.go b/operator/internal/controller/backup_envtest_test.go index e67328880..2cfe103b0 100644 --- a/operator/internal/controller/backup_envtest_test.go +++ b/operator/internal/controller/backup_envtest_test.go @@ -54,7 +54,7 @@ func TestBackup_CreatesJobWithExpectedSpec(t *testing.T) { if !strings.HasPrefix(c.Image, "restic/restic:") { t.Errorf("image = %q, want restic/restic:*", c.Image) } - if got, want := c.Args, []string{resticWorkerScript, "backup-worker", "backup", "/data", "--json", "--retry-lock", "10m", "--tag", "gameplane"}; !equalStrings(got, want) { + if got, want := c.Args, []string{resticWorkerScript, "backup-worker", "backup", "/data", "--json", "--retry-lock", "10m", "--tag", "gameplane", "--host", "gameplane-" + string(getGameServer(t, ns, "smp").UID)}; !equalStrings(got, want) { t.Errorf("args = %v, want %v", got, want) } @@ -362,7 +362,7 @@ func TestBackup_PassesTagsToRestic(t *testing.T) { if !ok { return false, "job not yet created" } - want := []string{"backup", "/data", "--json", "--retry-lock", "10m", "--tag", "gameplane", "--tag", "nightly", "--tag", "preupgrade"} + want := []string{"backup", "/data", "--json", "--retry-lock", "10m", "--tag", "gameplane", "--tag", "nightly", "--tag", "preupgrade", "--host", "gameplane-" + string(getGameServer(t, ns, "smp").UID)} want = append([]string{resticWorkerScript, "backup-worker"}, want...) if !equalStrings(j.Spec.Template.Spec.Containers[0].Args, want) { return false, "args = " + strings.Join(j.Spec.Template.Spec.Containers[0].Args, " ") diff --git a/operator/internal/controller/backup_snapshot_scrape_test.go b/operator/internal/controller/backup_snapshot_scrape_test.go index bfe302b17..159012426 100644 --- a/operator/internal/controller/backup_snapshot_scrape_test.go +++ b/operator/internal/controller/backup_snapshot_scrape_test.go @@ -148,6 +148,30 @@ func TestMirrorJobStatus_ScrapesSnapshotID(t *testing.T) { } } +func TestMirrorJobStatusPackedBytes(t *testing.T) { + for _, tc := range []struct { + field string + want int64 + }{ + {"", -1}, {`,"data_added":121`, -1}, {`,"data_added_packed":0`, 0}, + {`,"data_added_packed":317`, 317}, {`,"data_added_packed":-1`, -1}, + } { + b := scrapeBackup() + body := `{"message_type":"summary","snapshot_id":"abc","total_bytes_processed":2048` + tc.field + "}" + r := newScrapeReconciler(t, b, scrapeLogReader{body: body}, time.Hour, nil) + if _, err := r.mirrorJobStatus(context.Background(), b, succeededJob(time.Second)); err != nil { + t.Fatal(err) + } + if b.Status.Size == nil || b.Status.Size.Value() != 2048 { + t.Fatalf("logical size = %v", b.Status.Size) + } + got := b.Status.BytesAdded + if tc.want < 0 && got != nil || tc.want >= 0 && (got == nil || got.Value() != tc.want) { + t.Errorf("summary %s: added %v, want %d (negative means unknown)", body, got, tc.want) + } + } +} + // While the snapshot id can't yet be read but the grace period hasn't lapsed, // the Backup stays Succeeded (unquiesce has run) and requeues to retry. func TestMirrorJobStatus_RetriesWhenSnapshotUnavailable(t *testing.T) { diff --git a/operator/internal/controller/backup_worker.go b/operator/internal/controller/backup_worker.go index 14852e727..9ed35bc37 100644 --- a/operator/internal/controller/backup_worker.go +++ b/operator/internal/controller/backup_worker.go @@ -126,7 +126,7 @@ func (r *BackupReconciler) prepareBackupWorkerSource(ctx context.Context, b *gam return &source, r.Update(ctx, b) } -func (r *BackupReconciler) buildPreparedBackupPodSpec(b *gameplanev1alpha1.Backup, tmpl *gameplanev1alpha1.GameTemplate, source *backupWorkerSource, deadline time.Time) corev1.PodSpec { +func (r *BackupReconciler) buildPreparedBackupPodSpec(b *gameplanev1alpha1.Backup, tmpl *gameplanev1alpha1.GameTemplate, source *backupWorkerSource, serverUID types.UID, deadline time.Time) corev1.PodSpec { spec := r.buildBackupPodSpec(b, tmpl) automount := false spec.AutomountServiceAccountToken = &automount @@ -141,6 +141,10 @@ func (r *BackupReconciler) buildPreparedBackupPodSpec(b *gameplanev1alpha1.Backu } spec.Volumes = append(spec.Volumes, corev1.Volume{Name: "start", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: backupStartConfigMapName(b)}}}}) worker := &spec.Containers[0] + // The default Restic parent group includes the source host. A Job's + // hostname changes every run, while the bound GameServer UID survives + // pod replacements and changes when a server is recreated. + worker.Args = append(worker.Args, "--host", "gameplane-"+string(serverUID)) worker.Command = []string{"/bin/sh", "-c"} worker.Args = append([]string{resticWorkerScript, "backup-worker"}, worker.Args...) worker.Env = append(worker.Env, corev1.EnvVar{Name: "BACKUP_UID", Value: string(b.UID)}, @@ -299,7 +303,7 @@ func (r *BackupReconciler) reconcilePreparedRestic(ctx context.Context, b *gamep } backoff, deadline := resolveJobLimits(r.JobBackoffLimit, r.JobActiveDeadlineSeconds) job = batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: b.Name, Namespace: b.Namespace, Annotations: map[string]string{backupWorkerProtocolAnnotation: "v1"}}, Spec: batchv1.JobSpec{BackoffLimit: &backoff, ActiveDeadlineSeconds: &deadline, - Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{backupRestoreJobLabel: backupRestoreJobValue, backupUIDLabel: string(b.UID)}}, Spec: r.buildPreparedBackupPodSpec(b, tmpl, source, preparation)}}} + Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{backupRestoreJobLabel: backupRestoreJobValue, backupUIDLabel: string(b.UID)}}, Spec: r.buildPreparedBackupPodSpec(b, tmpl, source, bound.UID, preparation)}}} job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyNever if err := controllerutil.SetControllerReference(b, &job, r.Scheme); err != nil { return ctrl.Result{}, err diff --git a/operator/internal/controller/restic_summary.go b/operator/internal/controller/restic_summary.go index fd93b94d4..8c127d889 100644 --- a/operator/internal/controller/restic_summary.go +++ b/operator/internal/controller/restic_summary.go @@ -18,6 +18,7 @@ type ResticSummary struct { MessageType string `json:"message_type"` SnapshotID string `json:"snapshot_id"` TotalBytesProcessed int64 `json:"total_bytes_processed"` + PackedBytesAdded *int64 `json:"data_added_packed"` } // ErrNoResticSummary is returned by ParseResticSummary when the input diff --git a/operator/specs.md b/operator/specs.md index 10c9b06d7..787f21b9f 100644 --- a/operator/specs.md +++ b/operator/specs.md @@ -843,3 +843,7 @@ RecoveryIntervention is removed only after cleanup has durably released lifecycl ownership and its compatibility guards; unresolved owners retain the condition. See [the recovery contract](../specs/033-stopped-multivolume-recovery/contracts/recovery.md) and [operator operations](../docs/operator-operations.md) for defaults and failures. + +### Restic parent selection and accounting + +New workers use the bound GameServer UID as their Restic host. The first backup after upgrading may have no matching parent. Historical snapshots remain restorable by ID. `status.size` records the logical bytes processed. `status.bytesAdded` records the packed bytes added by that run, including a measured zero. Retention and pruning operate on shared repository data independently. diff --git a/web/e2e/screenshots/slice-3.spec.ts b/web/e2e/screenshots/slice-3.spec.ts index af8e46555..d6b6535ec 100644 --- a/web/e2e/screenshots/slice-3.spec.ts +++ b/web/e2e/screenshots/slice-3.spec.ts @@ -180,6 +180,7 @@ test.describe("Slice 3: Create Server, Modules, Backups (Desktop — 1440x900) @ await expect(page.getByText("mc-survival-nightly-0713")).toBeVisible({ timeout: 10_000 }); await expect(page.getByText("mc-survival-nightly-0712")).toBeVisible(); await page.waitForTimeout(200); + await expect(page.getByText("14 KB added", { exact: true })).toBeVisible(); await capture(page, "DPrYX"); }); @@ -244,9 +245,10 @@ test.describe("Slice 3: Create Server, Modules, Backups (Desktop — 1440x900) @ const dialog = page.getByRole("dialog", { name: /backup details/i }); await expect(dialog).toBeVisible(); await expect(dialog.getByText("a1b2c3d4e5f6", { exact: true })).toBeVisible(); - await expect(dialog.getByText("1.4 GiB", { exact: true })).toBeVisible(); + await expect(dialog.getByText("14 KB added", { exact: true }).locator("..")).toContainText("1.4 GiB"); await expect(dialog.getByText("Consistency has not been reported.")).toBeVisible(); await expect(dialog.getByText("Capture-time volume metadata is unavailable.", { exact: false })).toBeVisible(); + await expect(dialog.getByText("14 KB added", { exact: true })).toBeVisible(); await captureLocator(page, "zhLZN", dialog); }); diff --git a/web/e2e/screenshots/slice2b.spec.ts b/web/e2e/screenshots/slice2b.spec.ts index 91571d4e9..10b99932e 100644 --- a/web/e2e/screenshots/slice2b.spec.ts +++ b/web/e2e/screenshots/slice2b.spec.ts @@ -119,6 +119,7 @@ test.describe("Slice 2b: Mods/Modpacks/Backups + Settings (Desktop — 1440x900) await clickTab(page, "Backups"); await expect(page.getByRole("rowheader", { name: "mc-survival-nightly-0713" })).toBeVisible({ timeout: 10_000 }); await page.waitForTimeout(200); + await expect(page.getByText("14 KB added", { exact: true })).toBeVisible(); await capture(page, "pssCT"); }); diff --git a/web/src/components/backups/BackupDetailDrawer.tsx b/web/src/components/backups/BackupDetailDrawer.tsx index d81969f88..1b9cbafed 100644 --- a/web/src/components/backups/BackupDetailDrawer.tsx +++ b/web/src/components/backups/BackupDetailDrawer.tsx @@ -5,6 +5,7 @@ import { Drawer, Button } from "@heroui/react"; import { RotateCcw, Trash2 } from "lucide-react"; import { formatRelative } from "@/lib/utils"; +import { BackupSize } from "@/components/backups/BackupSize"; import type { Backup } from "@/types"; import { PhaseChip } from "@/components/ui/PhaseChip"; import { ErrorBanner } from "./ErrorBanner"; @@ -110,7 +111,7 @@ export function BackupDetailDrawer({ name, ns, onClose, onRestore, target: expli {backup.status?.snapshotID ?? intl.formatMessage({ id: "web.common.empty_value", defaultMessage: "—", description: "Shown in place of a value that is not set" })} - {backup.status?.size ?? intl.formatMessage({ id: "web.common.empty_value", defaultMessage: "—", description: "Shown in place of a value that is not set" })} + {formatRelative(backup.status?.startTime, intl)} {backup.status?.startTime && ( diff --git a/web/src/components/backups/BackupRow.test.tsx b/web/src/components/backups/BackupRow.test.tsx index 4268b6059..d61ad0844 100644 --- a/web/src/components/backups/BackupRow.test.tsx +++ b/web/src/components/backups/BackupRow.test.tsx @@ -65,6 +65,14 @@ describe("BackupRow", () => { expect(screen.getByText("alpha")).toBeInTheDocument(); }); + it.each([[undefined, undefined], ["0", "0 B"], ["13827", "14 KB"], ["317Ki", "317 KB"]])("keeps logical size separate from packed bytes %s", (added, formatted) => { + const backup = makeBackup({ status: { size: "4Gi", bytesAdded: added } }); + render(tableWrap( {}} onRestore={() => {}} />)); + expect(screen.getByText("4Gi")).toBeInTheDocument(); + if (added == null) expect(screen.queryByText(/ added$/)).not.toBeInTheDocument(); + else expect(screen.getByText(formatted + " added")).toBeInTheDocument(); + }); + it("Restore button is enabled when phase=Succeeded with snapshotID", async () => { const onRestore = vi.fn(); render( diff --git a/web/src/components/backups/BackupRow.tsx b/web/src/components/backups/BackupRow.tsx index 4ee37039e..7009aca8e 100644 --- a/web/src/components/backups/BackupRow.tsx +++ b/web/src/components/backups/BackupRow.tsx @@ -1,6 +1,7 @@ import { useResourcePermissions, resourceCan, type ResourceTarget } from "@/lib/resourceTarget"; import { Button, Table } from "@heroui/react"; import { formatRelative } from "@/lib/utils"; +import { BackupSize } from "@/components/backups/BackupSize"; import type { Backup } from "@/types"; import { PhaseChip } from "@/components/ui/PhaseChip"; import { useOptionalIntl } from "@/lib/i18n/format"; @@ -31,7 +32,7 @@ export function BackupRow({ backup, showServer, onSelect, onRestore, target, per - {backup.status?.size ?? intl.formatMessage({ id: "web.common.empty_value", defaultMessage: "—", description: "Shown in place of a value that is not set" })} + {formatRelative(backup.status?.completionTime, intl)} diff --git a/web/src/components/backups/BackupSize.tsx b/web/src/components/backups/BackupSize.tsx new file mode 100644 index 000000000..d546b0c2a --- /dev/null +++ b/web/src/components/backups/BackupSize.tsx @@ -0,0 +1,13 @@ +import { formatBytes, parseQuantityToBytes } from "@/lib/utils"; +import type { Backup } from "@/types"; +import { useOptionalIntl } from "@/lib/i18n/format"; + +export function BackupSize({ backup }: { backup: Backup }) { + const intl = useOptionalIntl(); + return <> + {backup.status?.size ?? intl.formatMessage({ id: "web.common.empty_value", defaultMessage: "—", description: "Shown in place of a value that is not set" })} + {backup.status?.bytesAdded != null &&
+ {intl.formatMessage({ id: "web.backups.bytes_added", defaultMessage: "{size} added", description: "Packed data added to the backup repository by this run. The size includes its unit." }, { size: formatBytes(parseQuantityToBytes(backup.status.bytesAdded)) })} +
} + ; +} diff --git a/web/src/routes/tabs/Backups.test.tsx b/web/src/routes/tabs/Backups.test.tsx index 16b6ef319..195c3d2cb 100644 --- a/web/src/routes/tabs/Backups.test.tsx +++ b/web/src/routes/tabs/Backups.test.tsx @@ -169,7 +169,7 @@ describe("BackupsTab", () => { expect(await screen.findByText(/Last backup/i)).toBeInTheDocument(); expect(screen.getByText(/Next backup/i)).toBeInTheDocument(); expect(screen.getByRole("heading", { name: /Backups/i })).toBeInTheDocument(); - expect(screen.getByText(/Total size/i)).toBeInTheDocument(); + expect(screen.getByText(/Total logical size/i)).toBeInTheDocument(); }); it("shows warning when multiple schedules exist", async () => { @@ -350,7 +350,7 @@ describe("BackupsTab", () => { ); renderWithQuery(); // Total size stat should show combined size - expect(await screen.findByText(/Total size/i)).toBeInTheDocument(); + expect(await screen.findByText(/Total logical size/i)).toBeInTheDocument(); }); }); diff --git a/web/src/routes/tabs/Backups.tsx b/web/src/routes/tabs/Backups.tsx index 52767b66b..a4c16aff9 100644 --- a/web/src/routes/tabs/Backups.tsx +++ b/web/src/routes/tabs/Backups.tsx @@ -12,6 +12,7 @@ import { ErrorBanner } from "@/components/backups/ErrorBanner"; import { ScheduleForm } from "@/components/backups/ScheduleForm"; import { RestoreDialog } from "@/components/backups/RestoreDialog"; import { BackupDetailDrawer } from "@/components/backups/BackupDetailDrawer"; +import { BackupSize } from "@/components/backups/BackupSize"; import type { Backup } from "@/types"; import { useOptionalIntl } from "@/lib/i18n/format"; @@ -112,7 +113,7 @@ export function BackupsTab({ name, ns }: { name: string; ns?: string }) { accent="violet" /> } accent="warning" @@ -224,7 +225,7 @@ export function BackupsTab({ name, ns }: { name: string; ns?: string }) { - {b.status?.size ?? intl.formatMessage({ id: "web.common.empty_value", defaultMessage: "—", description: "Shown in place of a value that is not set" })} + {formatRelative(b.status?.completionTime, intl)} diff --git a/web/src/test/handlers.ts b/web/src/test/handlers.ts index 1186a0f23..33af18cc4 100644 --- a/web/src/test/handlers.ts +++ b/web/src/test/handlers.ts @@ -838,6 +838,18 @@ const wsOrigin = typeof window !== "undefined" ? window.location.origin.replace( export function buildScreenshotHandlers() { const data = getScreenshotData(); + const nightlyBackup = makeBackup({ + metadata: { name: "mc-survival-nightly-0713", namespace: "default" }, + spec: { serverRef: { name: "mc-survival" } }, + status: { + phase: "Succeeded", + startTime: "2026-07-13T00:12:04Z", + completionTime: "2026-07-13T00:14:41Z", + size: "1.4 GiB", + bytesAdded: "14Ki", + snapshotID: "a1b2c3d4e5f6", + }, + }); return [ ...fleetHandlers, http.get("/admin/installation", () => HttpResponse.json({ standalone: false, localCluster: true })), @@ -1238,10 +1250,7 @@ export function buildScreenshotHandlers() { http.get("/backups", () => HttpResponse.json({ items: [ - makeBackup({ - metadata: { name: "mc-survival-nightly-0713", namespace: "default" }, - spec: { serverRef: { name: "mc-survival" } }, - }), + nightlyBackup, makeBackup({ metadata: { name: "mc-survival-nightly-0712", namespace: "default" }, spec: { serverRef: { name: "mc-survival" } }, @@ -1259,14 +1268,9 @@ export function buildScreenshotHandlers() { if (name === "mc-survival-nightly-0713") { return HttpResponse.json( makeBackup({ - metadata: { name, namespace: "default" }, - spec: { serverRef: { name: "mc-survival" } }, + ...nightlyBackup, status: { - phase: "Succeeded", - startTime: "2026-07-13T00:12:04Z", - completionTime: "2026-07-13T00:14:41Z", - size: "1.4 GiB", - snapshotID: "a1b2c3d4e5f6", + ...nightlyBackup.status, conditions: cookies.e2e_backup_coverage === "recorded" ? [{ type: "Consistency", status: "False", reason: "UnsupportedRawCopy", message: "The game does not support quiesce." }] : undefined, diff --git a/web/src/types.ts b/web/src/types.ts index 2091c1f42..d671d4e51 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -697,6 +697,7 @@ export interface Backup { completionTime?: string; snapshotID?: string; size?: string; + bytesAdded?: string; captureManifest?: BackupCaptureManifest; conditions?: Array<{ type: string; status: string; reason?: string; message?: string }>; };