diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/place.go b/modules/mesh-delivery-pipeline/internal/pipeline/place.go index a5b08a48..3a89ddd8 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/place.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/place.go @@ -11,6 +11,7 @@ import ( builds "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner" + wants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" nodebuildgo "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" nodebuildimage "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage" nodebuildnode "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode" @@ -25,6 +26,7 @@ type StepAsker interface { // acceptStep is where each build seat is sent its step, from its contract. var acceptStep = map[string]string{ builds.Seat: builds.AcceptSubjectStep, + wants.Seat: wants.AcceptSubjectStep, nodebuildgo.Seat: nodebuildgo.AcceptSubjectCheckStep, nodebuildnode.Seat: nodebuildnode.AcceptSubjectCheckStep, nodebuildimage.Seat: nodebuildimage.AcceptSubjectCheckStep, @@ -106,6 +108,14 @@ func Place(p seat.Pipeline, reg catalog.Registration, entries map[string]catalog if status == seat.StepResultStatusBypass { s.Outcome, s.Why = seat.StepResultOutcomeBypassed, "bypassed by the module's policy" } + // Composition is the assigner's, an accepted kind step of the seat mesh-wants (§22): it is asked with the others, + // running at once, and its end is heard from that seat as the build seats' are. Review and approval wait. + if step == seat.StepResultStepComposition && status != seat.StepResultStatusBypass { + s.Seat, s.Outcome, s.Started, s.Fingerprint = wants.Seat, seat.StepResultOutcomeRunning, &now, p.Fingerprint + s.Ask = AskID(p.Pipeline, "", step) + asks = append(asks, Asked{Seat: wants.Seat, ID: s.Ask, Ask: wants.StepRun{Branch: p.Branch, Commit: p.Commit, Fingerprint: p.Fingerprint, + Module: p.Module, Pipeline: p.Pipeline, Revision: p.Revision, Source: p.Source, Step: wants.StepRunStepComposition}}) + } steps = append(steps, s) } return steps, asks diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go b/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go index 52dbf3ff..e8a98929 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go @@ -4,13 +4,26 @@ import ( "encoding/json" "strings" "testing" + "time" builds "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner" + wants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" nodebuildgo "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" ) +func stepSeatOf(t *testing.T, name string) StepSeat { + t.Helper() + for _, bs := range StepSeats { + if bs.Seat == name { + return bs + } + } + t.Fatalf("no step seat %s", name) + return StepSeat{} +} + type stepAsks struct { at []string ids []string @@ -105,10 +118,7 @@ func TestTheBuildHoldsTheApprovalWhateverThePolicyNames(t *testing.T) { // The seat mesh-builds' end of the build step is taken as the others are, held to its one entry's module, commit and // fingerprint; an end naming two entries is refused (A-97, A-99). func TestTheBuildStepsEndIsTakenOnItsOneEntry(t *testing.T) { - bs := StepSeats[len(StepSeats)-1] - if bs.Seat != builds.Seat { - t.Fatalf("the last step seat is %s, want mesh-builds", bs.Seat) - } + bs := stepSeatOf(t, builds.Seat) h, store, _, _, _, _ := fixture() e := store[pkey] e.p.Steps = append([]seat.StepResult{{Step: seat.StepResultStepBuild, Status: seat.StepResultStatusRequired, Outcome: seat.StepResultOutcomeRunning, @@ -127,3 +137,38 @@ func TestTheBuildStepsEndIsTakenOnItsOneEntry(t *testing.T) { t.Error("an end of two entries is refused") } } + +// Composition is asked of the seat mesh-wants with the other steps, running at once, and its end is heard from that seat +// (design 47 G §22, §24): a pipeline whose policy names it places it running with its own ask, a bypassed one asks no one. +func TestCompositionIsAskedOfTheAssignerAndItsEndIsTaken(t *testing.T) { + reg, entries := placeFixture() + reg.Policy = catalog.Policy{Steps: catalog.StepStatuses{Lint: "bypass", Unit: "bypass", Composition: "required", Approval: "required"}} + p := seat.Pipeline{Pipeline: "main.4.app", Branch: "main", Module: "app", Commit: "c1", Fingerprint: "src2:aa", Revision: 7, Source: 4} + steps, asked := Place(p, reg, entries, time.Unix(1, 0), "the-runner") + var comp seat.StepResult + for _, s := range steps { + if s.Step == seat.StepResultStepComposition { + comp = s + } + } + if comp.Outcome != seat.StepResultOutcomeRunning || comp.Seat != wants.Seat || comp.Ask == "" { + t.Fatalf("composition = %+v, want running, asked of mesh-wants", comp) + } + var got *Asked + for i := range asked { + if asked[i].Seat == wants.Seat { + got = &asked[i] + } + } + r, ok := got.Ask.(wants.StepRun) + if got == nil || !ok || got.ID != comp.Ask || acceptStep[wants.Seat] != wants.AcceptSubjectStep || r.Step != "composition" || + r.Commit != "c1" || r.Fingerprint != "src2:aa" || r.Pipeline != "main.4.app" || r.Revision != 7 || r.Source != 4 || r.Module != "app" { + t.Fatalf("the composition ask = %+v", got) + } + // Its end, from the seat's own subject, is taken like the others, a node's warning becoming the step's why. + bs := stepSeatOf(t, wants.Seat) + ev, err := bs.Decode([]byte(`{"id":"` + comp.Ask + `","pipeline":"main.5.app","step":"composition","module":"app","commit":"c1","fingerprint":"src2:aa","node":"n1","outcome":"passed","warnings":["n2 asleep"]}`)) + if err != nil || ev.Said != "n2 asleep" || ev.Outcome != "passed" || ev.Module != "app" { + t.Fatalf("decoded end = %+v, %v", ev, err) + } +} diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/steps.go b/modules/mesh-delivery-pipeline/internal/pipeline/steps.go index 28b676c5..4781a04d 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/steps.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/steps.go @@ -10,6 +10,7 @@ import ( oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" builds "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshpipelinerunner" + wants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" nodebuildgo "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" nodebuildimage "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage" nodebuildnode "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode" @@ -33,6 +34,23 @@ var StepSeats = []StepSeat{ {Seat: nodebuildimage.Seat, Prefix: strings.TrimSuffix(nodebuildimage.SubjectStepEndedAll, "*")}, // The seat mesh-builds answers the build step of a candidate, one entry named by its fingerprint (A-97, A-99). {Seat: builds.Seat, Prefix: strings.TrimSuffix(builds.SubjectStepEndedAll, "*"), Decode: decodeBuildEnded}, + // The seat mesh-wants answers the composition step (§22, §24). + {Seat: wants.Seat, Prefix: strings.TrimSuffix(wants.SubjectStepEndedAll, "*"), Decode: decodeWantsEnded}, +} + +// decodeWantsEnded reads the composition step's end of the seat mesh-wants as the form the hearer holds each end to; a +// warning of a node asleep (a pass with a warning, ADR 0593 §9) is the step's `why` when the end says nothing else. +func decodeWantsEnded(raw []byte) (StepEnded, error) { + var w wants.StepEnded + if err := json.Unmarshal(raw, &w); err != nil { + return StepEnded{}, err + } + said := w.Said + if said == "" && len(w.Warnings) > 0 { + said = strings.Join(w.Warnings, "; ") + } + return StepEnded{V: w.V, ID: w.ID, Pipeline: w.Pipeline, Step: w.Step, Module: w.Module, Commit: w.Commit, Fingerprint: w.Fingerprint, + Node: w.Node, Outcome: w.Outcome, Said: said, Rerun: w.Rerun, By: w.By, Started: w.Started, Ended: w.Ended}, nil } // decodeBuildEnded reads the build step's end of the seat mesh-builds as the form the hearer holds each end to: its one