From 45b0779b6801ecbc6997e4ebb50f1e3bb4196988 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 17:41:39 +0100 Subject: [PATCH] mesh-vault: a store file that is not a record is listed by name with its error (issue 1409, V10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store.ListReadable lists every record and, beside them, each file it cannot read by name with its error; RecordsHeld lists those as rows with an error and goes on. recordsError stays for a store that cannot be listed at all. Store.List and ListPrefix keep their behaviour for their other callers. Builds: design 39 bus-and-switch §20; design 39 parts §12. Lead answers (issue 1409): V9 (keep recordsCount and recordsError), V10. --- modules/mesh-vault/cmd/mesh-vault/licences.go | 7 +++- .../cmd/mesh-vault/licences_test.go | 15 ++++++-- modules/mesh-vault/cmd/mesh-vault/store.go | 36 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/modules/mesh-vault/cmd/mesh-vault/licences.go b/modules/mesh-vault/cmd/mesh-vault/licences.go index 0079f069f..77662e793 100644 --- a/modules/mesh-vault/cmd/mesh-vault/licences.go +++ b/modules/mesh-vault/cmd/mesh-vault/licences.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "regexp" + "sort" "strings" "time" ) @@ -102,7 +103,7 @@ type RecordHeld struct { // RecordsHeld lists every record the store keeps, by name, ordered as the store lists them. The fingerprint is the // vault's own of the whole value, as the ledger gives it. func (v *Vault) RecordsHeld() ([]RecordHeld, error) { - records, err := v.Store.List() + records, bad, err := v.Store.ListReadable() if err != nil { return nil, err } @@ -122,5 +123,9 @@ func (v *Vault) RecordsHeld() ([]RecordHeld, error) { } out = append(out, row) } + for _, b := range bad { + out = append(out, RecordHeld{Name: b.Name, Error: "the vault could not read the record's file: " + b.Err.Error()}) + } + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out, nil } diff --git a/modules/mesh-vault/cmd/mesh-vault/licences_test.go b/modules/mesh-vault/cmd/mesh-vault/licences_test.go index c0085b49c..6368f4572 100644 --- a/modules/mesh-vault/cmd/mesh-vault/licences_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/licences_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "os" "strings" "testing" @@ -138,6 +139,10 @@ func TestSecretHoldersListsEveryStoreRecordByNameAndFingerprintNeverTheValue(t * t.Fatal(err) } + if err := os.WriteFile(v.Store.pathOf("pair/notjson/one"), []byte("{not json"), 0o600); err != nil { + t.Fatal(err) + } + got, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_holders").Run(map[string]any{}) if err != nil { t.Fatal(err) @@ -151,8 +156,8 @@ func TestSecretHoldersListsEveryStoreRecordByNameAndFingerprintNeverTheValue(t * for _, r := range m["records"].([]RecordHeld) { rows[r.Name] = r } - if m["recordsCount"] != 4 || len(rows) != 4 { - t.Fatalf("records %v, want 4 (two licence secrets, a pair, a broken one)", rows) + if m["recordsCount"] != 5 || len(rows) != 5 { + t.Fatalf("records %v, want 5 (two licence secrets, a pair, a sealed-broken one, a file that is not JSON)", rows) } for part, value := range map[string]string{LicenceGrant: grant, LicenceAccess: access} { r := rows["licence.jane_at_example.org."+part] @@ -167,4 +172,10 @@ func TestSecretHoldersListsEveryStoreRecordByNameAndFingerprintNeverTheValue(t * if r := rows["pair/broken/one"]; r.Error == "" || r.Fingerprint != "" { t.Errorf("a record the vault cannot open must stay listed with an error: %+v", r) } + if r := rows["pair/notjson/one"]; r.Error == "" || r.Fingerprint != "" { + t.Errorf("a file that is not JSON must be listed by name with its error, the listing going on (V10): %+v", r) + } + if _, bad := m["recordsError"]; bad { + t.Errorf("recordsError is for a store that cannot be listed at all (V10): %v", m["recordsError"]) + } } diff --git a/modules/mesh-vault/cmd/mesh-vault/store.go b/modules/mesh-vault/cmd/mesh-vault/store.go index b7ec8b287..fd4b97f96 100644 --- a/modules/mesh-vault/cmd/mesh-vault/store.go +++ b/modules/mesh-vault/cmd/mesh-vault/store.go @@ -327,6 +327,42 @@ func (s *Store) ListPrefix(prefix string) ([]Record, error) { return out, nil } +// Unreadable is a record file the store could not read, by the name its file gives. +type Unreadable struct { + Name string + Err error +} + +// ListReadable is every record by name, and beside them each file that is not a record (not valid JSON, say) by name +// with its error: the listing goes on past it (issue 1409, V10). Only an unreadable directory is an error. +func (s *Store) ListReadable() ([]Record, []Unreadable, error) { + entries, err := os.ReadDir(filepath.Join(s.dir, "secrets")) + if err != nil { + return nil, nil, err + } + var out []Record + var bad []Unreadable + for _, e := range entries { + if !strings.HasSuffix(e.Name(), ".json") { + continue + } + name, err := url.PathUnescape(strings.TrimSuffix(e.Name(), ".json")) + if err != nil { + bad = append(bad, Unreadable{Name: strings.TrimSuffix(e.Name(), ".json"), Err: err}) + continue + } + r, ok, err := s.Read(name) + if err != nil { + bad = append(bad, Unreadable{Name: name, Err: err}) + } else if ok { + out = append(out, r) + } + } + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + sort.Slice(bad, func(i, j int) bool { return bad[i].Name < bad[j].Name }) + return out, bad, nil +} + type operatorFile struct { Key string `json:"key"` Since time.Time `json:"since"`