package main import ( "fmt" "net/http" "net/http/httptest" "slices" "strings" "sync" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/artifacts" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // The bases the mesh copied in, and recording a copy no record names (novox/hq ADR 0257). func TestTheMirrorsVerbComposesItsCommandAndRefusesWhatItDoesNotTake(t *testing.T) { r := ref("route-proxy", "on-go_base", 1) for _, c := range []struct { args map[string]any want string }{ {map[string]any{}, "mirrors --json"}, {map[string]any{"record": r}, "mirrors --json --record " + r}, {map[string]any{"record": r, "confirm": "true", "why": "copied before copies were recorded"}, "mirrors --json --record " + r + " --confirm --why copied before copies were recorded"}, } { argv, err := argvFor("mirrors", c.args) if err != nil || strings.Join(argv, " ") != c.want { t.Errorf("mirrors %v: %q %v, want %q", c.args, argv, err, c.want) } } for _, args := range []map[string]any{ {"record": r, "confirm": "true"}, // recorded without a why {"record": r, "why": "x"}, // a why for a dry run {"confirm": "true", "why": "x"}, // nothing to record {"record": r, "confirm": "yes", "why": "x"}, // a switch is true or false {"delete": "true"}, } { if argv, err := argvFor("mirrors", args); err == nil { t.Errorf("mirrors %v was composed as %q", args, argv) } } if repairingCommand([]string{"mirrors", "--json", "--record", r, "--confirm", "--why", "x"}) != "mirrors" { t.Error("recording a copy through the generic verb would go unrecorded") } if repairingCommand([]string{"mirrors", "--json", "--record", r}) != "" { t.Error("a dry run was taken for an act by hand") } if !personsDecision(link.HandAct{Verb: "mirrors"}) { t.Error("recording a copy would count toward a healer the mesh lacks") } } func TestOnlyACopyInARepositoryTheMirrorWritesIsTaken(t *testing.T) { for given, ok := range map[string]bool{ ref("route-proxy", "on-go_base", 1): true, catalogue.ArtifactStoreScheme + "upstream/docker.io/library/golang@sha256:" + strings.Repeat("a", 64): true, ref("route-proxy", "server", 1): false, // a module's own image catalogue.ArtifactStoreScheme + "novox/invoicing-api@sha256:" + strings.Repeat("a", 64): false, archiveRef("web", "on-tools", 1): false, // a blob catalogue.ArtifactStoreScheme + "web/on-x@sha256:abc": false, // not a whole digest "docker.io/library/golang@sha256:" + strings.Repeat("a", 64): false, } { if _, err := mirrorReference(given); (err == nil) != ok { t.Errorf("%s: taken %v, want %v (%v)", given, err == nil, ok, err) } } } // heldStore answers a manifest HEAD with 200 for the digests it holds, and records every request. func heldStore(t *testing.T, holds ...string) (artifacts.Store, *[]string) { t.Helper() var asked []string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { asked = append(asked, r.Method+" "+r.URL.Path) if r.Method == http.MethodHead && slices.ContainsFunc(holds, func(d string) bool { return strings.HasSuffix(r.URL.Path, d) }) { w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusNotFound) })) t.Cleanup(server.Close) return artifacts.Store{Address: strings.TrimPrefix(server.URL, "http://")}, &asked } // indexRegistry holds indexes and platforms of one image's repository, answers GETs with their // documents, refuses GETs for the digests in fail, and records every delete. type indexRegistry struct { mu sync.Mutex indexes map[string][]string held map[string]bool fail map[string]bool deleted []string } func (f *indexRegistry) serve(t *testing.T) artifacts.Store { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { f.mu.Lock() defer f.mu.Unlock() digest := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] switch r.Method { case http.MethodGet: if f.fail[digest] { w.WriteHeader(http.StatusInternalServerError) return } if children, ok := f.indexes[digest]; ok && f.held[digest] { var named []string for _, c := range children { named = append(named, `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"`+c+`"}`) } _, _ = w.Write([]byte(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[` + strings.Join(named, ",") + `]}`)) return } if f.held[digest] { _, _ = w.Write([]byte(`{"schemaVersion":2,"layers":[]}`)) return } w.WriteHeader(http.StatusNotFound) case http.MethodHead: if f.held[digest] { w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusNotFound) case http.MethodDelete: if !f.held[digest] { w.WriteHeader(http.StatusNotFound) return } delete(f.held, digest) f.deleted = append(f.deleted, digest) w.WriteHeader(http.StatusAccepted) default: w.WriteHeader(http.StatusBadRequest) } })) t.Cleanup(server.Close) return artifacts.Store{Address: strings.TrimPrefix(server.URL, "http://")} } func copyDigest(n int) string { return fmt.Sprintf("sha256:%064x", n) } // twoCopies records, in one image's repository, a kept copy (stood on by a held module's build) naming // platforms 11 and 12, and an eligible one (a failed build's) naming 12 and 13. func twoCopies(t *testing.T, inv *inventory.Inventory) (kept, eligible string, reg *indexRegistry) { t.Helper() const repository = "upstream/docker.io/library/golang" kept = catalogue.ArtifactStoreScheme + repository + "@" + copyDigest(1) eligible = catalogue.ArtifactStoreScheme + repository + "@" + copyDigest(2) if err := inv.RegisterModule(t.Context(), catalogue.Manifest{Module: "proxy", Version: "1"}, inventory.Source{Repository: "https://forge.invalid/proxy.git"}); err != nil { t.Fatal(err) } failed := inventory.Build{ID: "f1", Repository: "https://forge.invalid/proxy.git", On: "a-build-machine", Failed: "a recipe refused", Mirrored: []string{eligible}} if err := inv.RecordBuild(t.Context(), failed); err != nil { t.Fatal(err) } ok := inventory.Build{ID: "b1", Repository: "https://forge.invalid/proxy.git", Module: "proxy", On: "a-build-machine", Commit: "c0ffee", Made: []inventory.Artifact{{Name: "app", Kind: "image", Reference: ref("proxy", "app", 7)}}, Against: []string{kept}, Mirrored: []string{kept}} if err := inv.RecordBuild(t.Context(), ok); err != nil { t.Fatal(err) } reg = &indexRegistry{ indexes: map[string][]string{copyDigest(1): {copyDigest(11), copyDigest(12)}, copyDigest(2): {copyDigest(12), copyDigest(13)}}, held: map[string]bool{copyDigest(1): true, copyDigest(2): true, copyDigest(11): true, copyDigest(12): true, copyDigest(13): true}, fail: map[string]bool{}, } return kept, eligible, reg } // Through the records: a confirmed collect lets the eligible index go with the platform only it names, // and leaves the platform the kept index names. // The sweep after a build leaves an eligible index in place and eligible: let go of alone, its // platforms would stay for ever under a record that says collected. A later collect a person confirms // takes it with the platform only it names. An image the same sweep reaches is let go of as before. // A kept index the store will not answer for stops a confirmed collect before its first delete.