diff --git a/modules/mesh-delivery-assigner/asks/asks.go b/modules/mesh-delivery-assigner/asks/asks.go index 4542253a..bc29ad14 100644 --- a/modules/mesh-delivery-assigner/asks/asks.go +++ b/modules/mesh-delivery-assigner/asks/asks.go @@ -41,10 +41,13 @@ const ( // AskKindOutside is the kind of the assigner's ask about a moved module whose failing health check has waited past its bound // on an outside cause (design 47 G §706): go on, or count it as failed. `outside...`. AskKindOutside = "outside" + // AskKindRevert is the kind of the assigner's ask about a held revert, one with no restore point (design 47 G §710): keep it + // held, or start the reverted build on the live data. `revert...`, the reverted build's. + AskKindRevert = "revert" ) // AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module -// and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest Composed of the +// and fingerprint FP, a restore's, a backup's, an outside's and a revert's Node, Module and FP, a record's Node, Module and the digest Composed of the // composition it would allow, the assignments' Proposal. type AskKey struct { Proposal string @@ -63,11 +66,11 @@ func AskNeedsProof(kind string) (bool, error) { switch kind { case meshdeliveryclient.AskKindMerge: return true, nil - case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside: + case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert: return false, nil } - return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, - AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside) + return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, + AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert) } // HoldAskKey is the key of the ask about a module's held fingerprint: `hold..`. @@ -119,6 +122,16 @@ func BackupAskKey(node, module, fingerprint string) (string, error) { return AskKindBackup + "." + key, nil } +// RevertAskKey is the key of the ask about a held revert of a module on a node to the reverted build's fingerprint: +// `revert...` (design 47 G §710). +func RevertAskKey(node, module, fingerprint string) (string, error) { + key, err := BackupAskKey(node, module, fingerprint) + if err != nil { + return "", err + } + return AskKindRevert + strings.TrimPrefix(key, AskKindBackup), nil +} + // AssignmentsAskKey is the key of the authorising ask about a proposal of assignments: `assignments.` (ADR 0567 §5). func AssignmentsAskKey(proposal string) (string, error) { if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil { @@ -140,7 +153,7 @@ func RecordAskKey(node, module, composed string) (string, error) { return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil } -// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record or assignments ask); it refuses an unknown +// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record, assignments, outside or revert ask); it refuses an unknown // kind and a key whose parts do not fit its kind, naming the key. func ParseAnyAskKey(key string) (AskKey, error) { kind, rest, _ := strings.Cut(key, ".") @@ -180,10 +193,10 @@ func ParseAnyAskKey(key string) (AskKey, error) { } k.Kind = kind return k, nil - case AskKindBackup: + case AskKindBackup, AskKindRevert: node, module, part, isNode, err := nodeengineclient.ParseComposedKey(rest) if err != nil || isNode { - return refused(fmt.Errorf("a backup ask is backup...")) + return refused(fmt.Errorf("a %s ask is %s...", kind, kind)) } fp, err := catalogkey.ParseKeyPart(part) if err != nil { diff --git a/modules/mesh-delivery-assigner/asks/asks_test.go b/modules/mesh-delivery-assigner/asks/asks_test.go index c522584c..d97babc9 100644 --- a/modules/mesh-delivery-assigner/asks/asks_test.go +++ b/modules/mesh-delivery-assigner/asks/asks_test.go @@ -19,6 +19,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { backup, _ := BackupAskKey("n1", "novox.be", fp) assignments, _ := AssignmentsAskKey("p-20261028t183000-1a2b3c") record, _ := RecordAskKey("n1", "gitea", digest) + revert, _ := RevertAskKey("n1", "nats", fp) for key, want := range map[string]struct { got string back AskKey @@ -28,6 +29,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { "backup": {backup, AskKey{Kind: "backup", Node: "n1", Module: "novox.be", FP: fp}}, "assignments": {assignments, AskKey{Kind: "assignments", Proposal: "p-20261028t183000-1a2b3c"}}, "record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}}, + "revert": {revert, AskKey{Kind: "revert", Node: "n1", Module: "nats", FP: fp}}, } { back, err := ParseAnyAskKey(want.got) if err != nil || back != want.back { @@ -35,7 +37,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { } } for got, want := range map[string]string{hold: "hold.gitea." + hex, restore: "restore.n1.gitea." + hex, backup: "backup.n1.novox=2ebe." + hex, - assignments: "assignments.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32)} { + assignments: "assignments.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32), revert: "revert.n1.nats." + hex} { if got != want { t.Errorf("key %q, want %q", got, want) } @@ -45,7 +47,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { t.Errorf("a merge ask key: %+v, %v", merge, err) } for _, bad := range []string{"", "other.x", "hold.gitea", "hold.gitea.src2:" + hex, "restore.n1", "backup.n1.node", "backup.n1.gitea." + hex[:60], - "assignment.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g"} { + "assignment.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g", "revert.n1.node", "revert.n1.nats." + hex[:60]} { if _, err := ParseAnyAskKey(bad); err == nil { t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad) } @@ -53,8 +55,10 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { if proof, err := AskNeedsProof("merge"); err != nil || !proof { t.Error("a merge ask needs proof") } - if proof, err := AskNeedsProof("hold"); err != nil || proof { - t.Error("a hold ask needs no proof") + for _, kind := range []string{"hold", "revert"} { + if proof, err := AskNeedsProof(kind); err != nil || proof { + t.Errorf("a %s ask needs no proof", kind) + } } if _, err := AskNeedsProof("view"); err == nil { t.Error("an unknown kind of ask is taken") diff --git a/modules/mesh-delivery-assigner/internal/assign/asks_test.go b/modules/mesh-delivery-assigner/internal/assign/asks_test.go index 3e23b1db..dd3ecd79 100644 --- a/modules/mesh-delivery-assigner/internal/assign/asks_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/asks_test.go @@ -42,6 +42,7 @@ func anAskEndedAtTheChannelIsWrittenAgain(t *testing.T) { holdKey, _ := asks.HoldAskKey("nats", fNew) restoreKey, _ := asks.RestoreAskKey("novox", "nats", fNew) backupKey, _ := asks.BackupAskKey("novox", "nats", fNew) + revertKey, _ := asks.RevertAskKey("novox", "nats", fNew) recordKey, _ := asks.RecordAskKey("novox", "nats", digest) assignmentsKey, _ := asks.AssignmentsAskKey("p-20261028t183000-1a2b3c") need := RecordNeed{Node: "novox", Digest: digest, Judgement: JudgedMovesData} @@ -64,6 +65,9 @@ func anAskEndedAtTheChannelIsWrittenAgain(t *testing.T) { {name: "hold", key: holdKey, a: ptr(HoldAsk(holdKey, "nats", fNew, "novox", "w", AskingSeconds(asked), asked)), outcome: occ.OutcomeExpired}, {name: "restore", key: restoreKey, a: ptr(RestoreAsk(restoreKey, "novox", "nats", fNew, "w", AskingSeconds(asked), asked)), outcome: occ.OutcomeCancelled}, {name: "backup", key: backupKey, a: ptr(BackupAsk(backupKey, "novox", "nats", fNew, digest, "w", 0, asked)), outcome: occ.OutcomeCancelled}, + {name: "revert", key: revertKey, a: ptr(assignerAsk(AskingSeconds(asked), oc.Ask{ID: meshdeliveryclient.AskingID(revertKey, AskingSeconds(asked)), + Headline: "h", Explanation: "e", Who: occ.Operator, Expires: occ.FormatTime(asked.Add(AskLasts)), OnExpiry: "o", + Options: []oc.Option{{ID: "hold", Label: "Hold", Does: "d", Level: occ.Acknowledge}}})), outcome: occ.OutcomeExpired}, {name: "record", key: recordKey, a: &record, outcome: occ.OutcomeCancelled}, {name: "record of another put", key: recordKey, a: &record, outcome: occ.OutcomeExpired, put: &other}, {name: "record a put asked", key: recordKey, a: &byPut, outcome: occ.OutcomeExpired, put: &put, why: "put"}, diff --git a/modules/mesh-delivery-assigner/internal/assign/ended.go b/modules/mesh-delivery-assigner/internal/assign/ended.go index 98985d51..cc05a714 100644 --- a/modules/mesh-delivery-assigner/internal/assign/ended.go +++ b/modules/mesh-delivery-assigner/internal/assign/ended.go @@ -18,7 +18,7 @@ import ( ) // AskLasts is how long an ask of the assigner's rules stands at the operator channel before it lapses unanswered: the hold, -// restore, record and backup asks. +// restore, record, backup, outside and revert asks. const AskLasts = 7 * 24 * time.Hour // writtenAgainOnEnd is, per kind of the assigner's asks, whether an asking that ended at the channel without an answer while @@ -31,6 +31,7 @@ var writtenAgainOnEnd = map[string]string{ asks.AskKindBackup: "", asks.AskKindOutside: "", // raised again at once while the wait stands (design 47 G §706); the wait's end takes the key away asks.AskKindRecord: "", + asks.AskKindRevert: "", // raised again at once while the hold stands (design 47 G §710); one answered hold is not asked again asks.AskKindAssignments: "a proposal's authorising ask that lapsed ends the proposal (ADR 0567 §5); the verb again asks it again (§11)", }