R-1252 (P2): a rejected or absent backup block never deletes second-drive copies — reconcile only on a valid classification; a rejected block is said on the app's status and once a day to the operator (backup_run_failures); kept copies stay kept (H2); Remove drops the kept records it deleted (H9). Commit only.
gates / gates (push) Successful in 1m18s
gates / gates (push) Successful in 1m18s
This commit is contained in:
@@ -215,3 +215,26 @@ func TestCatalogBlocks_Fidelity(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// R-1252: the second-drive copy must tell a REJECTED block from an absent one (a rejected one keeps every copy and
|
||||
// alarms; an absent one is quiet). Through the REAL LoadMetadata path. RED-PROOF: drop the BackupRejected assignment
|
||||
// in LoadMetadata → the rejected fixture reads "" and this fails.
|
||||
func TestR1252_BackupBlockRejectedNamesTheReason(t *testing.T) {
|
||||
compose := `services:
|
||||
app:
|
||||
volumes:
|
||||
- ${USERDATA_PATH}/media/audiobooks:/audiobooks
|
||||
`
|
||||
bad := newClassifyManager(t, "display_name: App\nbackup:\n userdata:\n - path: media/nope\n class: mandatory\n", compose)
|
||||
if why := bad.BackupBlockRejected("app"); why == "" {
|
||||
t.Fatal("a block whose path matches no compose bind must be reported as rejected, with its reason")
|
||||
}
|
||||
good := newClassifyManager(t, "display_name: App\nbackup:\n userdata:\n - path: media/audiobooks\n class: mandatory\n", compose)
|
||||
if why := good.BackupBlockRejected("app"); why != "" {
|
||||
t.Fatalf("a valid block is not rejected, got %q", why)
|
||||
}
|
||||
none := newClassifyManager(t, "display_name: App\n", compose)
|
||||
if why := none.BackupBlockRejected("app"); why != "" {
|
||||
t.Fatalf("an absent block is not rejected, got %q", why)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ type Metadata struct {
|
||||
// block (sets this back to nil + logs one ERROR) on any validation defect, so a bad catalog push
|
||||
// degrades to legacy loudly rather than partially classifying. Consumed by Task 3/4 — INERT today.
|
||||
Backup *appbackup.BackupSpec `yaml:"backup,omitempty" json:"backup,omitempty"`
|
||||
// BackupRejected (R-1252) is why LoadMetadata rejected the backup block ("" = not rejected). Never read from the
|
||||
// file: the second-drive copy must tell a REJECTED block (keep every copy, alarm) from an absent one.
|
||||
BackupRejected string `yaml:"-" json:"-"`
|
||||
// DataPaths is the customer-facing folder ANNOTATION (R-75): role + Hungarian label over paths
|
||||
// that must ALREADY exist as compose binds. It never declares a path. Validated in LoadMetadata
|
||||
// with the Fork-3 asymmetry — a malformed PATH rejects the whole block, an unknown ROLE drops
|
||||
@@ -551,9 +554,11 @@ func loadMetadata(stackDir string, validateBackup bool) Metadata {
|
||||
if composeErr != nil {
|
||||
log.Printf("[ERROR] [stacks] .felhom.yml backup block rejected in %s: docker-compose.yml unreadable: %v", stackDir, composeErr)
|
||||
meta.Backup = nil
|
||||
meta.BackupRejected = "docker-compose.yml unreadable: " + composeErr.Error()
|
||||
} else if err := appbackup.ValidateBackupSpec(meta.Backup, binds); err != nil {
|
||||
log.Printf("[ERROR] [stacks] .felhom.yml backup block rejected in %s: %v", stackDir, err)
|
||||
meta.Backup = nil
|
||||
meta.BackupRejected = err.Error()
|
||||
}
|
||||
}
|
||||
// data_paths (R-75) rides the same choke point, with its own asymmetric rules: a malformed
|
||||
@@ -597,6 +602,15 @@ func (m *Manager) ClassifiedBinds(name string) ([]appbackup.ClassifiedBind, bool
|
||||
return appbackup.ClassifyBinds(meta.Backup, binds)
|
||||
}
|
||||
|
||||
// BackupBlockRejected (R-1252) is why this app's backup block was rejected, or "" when it is valid or absent.
|
||||
func (m *Manager) BackupBlockRejected(name string) string {
|
||||
stack, ok := m.GetStack(name)
|
||||
if !ok || name == SambaStackName {
|
||||
return ""
|
||||
}
|
||||
return LoadMetadata(filepath.Dir(stack.ComposePath)).BackupRejected
|
||||
}
|
||||
|
||||
// ExcludedDockerVolumes returns the Docker names of the stack's named volumes its template excludes from every app-data backup
|
||||
// (R-49, D21 = B; appbackup.ExcludedDockerVolumes), and a sentence per template entry that was ignored.
|
||||
func (m *Manager) ExcludedDockerVolumes(name string) ([]string, []string) {
|
||||
|
||||
Reference in New Issue
Block a user