package main import ( "archive/tar" "bytes" "compress/gzip" "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-controller/internal/builderkeys" "github.com/novox/mesh-controller/internal/inventory" ) // keyBuckets is the builder's and the indexer's buckets as a take-in reads them, for a test: keys by module, manifests by // digest. type keyBuckets struct { keys map[string]builderkeys.Key manifests map[string]string } func (k keyBuckets) Build(_ context.Context, module, fp string) (builderkeys.Key, error) { if key, ok := k.keys[module]; ok && key.Fingerprint == fp { return key, nil } return builderkeys.Key{Module: module, Fingerprint: fp}, nil } func (k keyBuckets) Manifest(_ context.Context, digest string) (sdkdelivery.Manifest, error) { raw, ok := k.manifests[digest] if !ok { return sdkdelivery.Manifest{}, errors.New("the indexer holds no manifest " + digest) } return sdkdelivery.Manifest{Module: "x", JSON: raw}, nil } const keyFP = "src2:" + "1111111111111111111111111111111111111111111111111111111111111111" // aKeyOf is a done key of module x at keyFP for the manifest raw, with these artifacts. func aKeyOf(raw string, revision uint64, made ...sdkdelivery.BuiltArtifact) (keyBuckets, sdkdelivery.Module) { digest := sdkdelivery.ManifestDigest([]byte(raw)) k := builderkeys.Key{Module: "x", Fingerprint: keyFP, Found: true, Revision: revision, Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildDone, By: "ace", Manifest: digest, Artifacts: made}} trunk := sdkdelivery.Module{FP2: keyFP, Manifest: digest, Repository: "novox/mesh-catalog", Commit: "c0ffee0123456789", Path: "modules/x"} return keyBuckets{keys: map[string]builderkeys.Key{"x": k}, manifests: map[string]string{digest: raw}}, trunk } const tsBundle = `{"module":"x","version":"1","tools":["x_read"], "build":{"artifacts":[{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["dist/index.js"]}, {"name":"img","kind":"image","from":"Dockerfile","context":{"repository":"novox/other","seat":"git"}}]}, "resources":[{"id":"run","type":"container","artifact":"img"}]}` func shaOf(b string) string { sum := sha256.Sum256([]byte(b)) return "sha256:" + hex.EncodeToString(sum[:]) } // A3 (ADR 0498 §8, ADR 0501 §1): a done key whose every artifact names a reference is one recorded build, its artifacts // the key's, its launchers by the build modules' rule, its reads from its manifest, the bases its last build stood on // carried; recorded twice it is one record. func TestADoneKeyIsOneRecordedBuildOfItsArtifacts(t *testing.T) { keys, trunk := aKeyOf(tsBundle, 3, sdkdelivery.BuiltArtifact{Name: "tools", Kind: "bundle", Digest: shaOf("t"), Reference: "artifact-store://x/tools/blobs/" + shaOf("t")}, sdkdelivery.BuiltArtifact{Name: "img", Kind: "image", Digest: shaOf("i"), Reference: "artifact-store://x/img@" + shaOf("i")}) previous := []inventory.Build{{ID: "build-1", Module: "x", Against: []string{"artifact-store://base@sha256:aa"}}} take := keyTake{Module: "x", Trunk: trunk} kept, resolved, err := buildOfKey(t.Context(), keys, take, keyContext{previous: previous}) if err != nil { t.Fatal(err) } if kept.ID != "key-1111111111111111111111111111111111111111111111111111111111111111-3" { t.Errorf("the record's id is %s", kept.ID) } again, _, _ := buildOfKey(t.Context(), keys, take, keyContext{previous: previous}) if again.ID != kept.ID { t.Errorf("one key recorded twice is two records: %s and %s", kept.ID, again.ID) } if kept.SourceFingerprint != keyFP || kept.Commit != trunk.Commit || kept.Path != "modules/x" || kept.On != "ace" || kept.Module != "x" || kept.Repository != "novox/mesh-catalog" { t.Errorf("the record: %+v", kept) } if len(kept.Made) != 2 || kept.Made[0].Reference != "artifact-store://x/tools/blobs/"+shaOf("t") || kept.Made[1].Reference != "artifact-store://x/img@"+shaOf("i") || kept.Made[1].Kind != "image" { t.Errorf("Made is not the key's artifacts: %+v", kept.Made) } if len(kept.Read) != 1 || kept.Read[0].Repository != "novox/other" { t.Errorf("Read is not the manifest's contexts: %+v", kept.Read) } if kept.Sources != nil || kept.Mirrored != nil { t.Errorf("a key's record says build sources or mirrored bases it cannot know: %+v %+v", kept.Sources, kept.Mirrored) } if len(kept.Against) != 1 || kept.Against[0] != "artifact-store://base@sha256:aa" { t.Errorf("the bases the last build stood on are not carried: %v", kept.Against) } if len(resolved.Bundles) != 1 || resolved.Bundles[0].Launchers["dist/index.js"] != "dist/index.serve.mjs" { t.Errorf("the bundle's launchers: %+v", resolved.Bundles) } var m map[string]any if err := json.Unmarshal(kept.Manifest, &m); err != nil || !strings.Contains(string(kept.Manifest), shaOf("i")) { t.Errorf("the recorded manifest names no image digest: %s", kept.Manifest) } } // A3 (ADR 0498 §8): a key with an artifact naming no reference, a key not done and a missing key are waited for, said; // a failed key fails in the builder's words; a module with no fingerprint of the new form waits, naming why. func TestAKeyNotReadyWaitsAndAFailedKeyFails(t *testing.T) { noRef, trunk := aKeyOf(tsBundle, 1, sdkdelivery.BuiltArtifact{Name: "tools", Kind: "bundle", Digest: shaOf("t"), Reference: "artifact-store://x/tools/blobs/" + shaOf("t")}, sdkdelivery.BuiltArtifact{Name: "img", Kind: "image", Digest: shaOf("i")}) take := keyTake{Module: "x", Trunk: trunk} _, _, err := buildOfKey(t.Context(), noRef, take, keyContext{}) if !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), "waits for the reference of img") { t.Errorf("a key without a reference: %v", err) } building := noRef k := building.keys["x"] k.Build = sdkdelivery.BuilderBuild{State: sdkdelivery.BuildBuilding, By: "g14"} building.keys = map[string]builderkeys.Key{"x": k} if _, _, err := buildOfKey(t.Context(), building, take, keyContext{}); !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), "building on g14") { t.Errorf("a building key: %v", err) } if _, _, err := buildOfKey(t.Context(), keyBuckets{}, take, keyContext{}); !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), "no build of x") { t.Errorf("no key: %v", err) } k.Build = sdkdelivery.BuilderBuild{State: sdkdelivery.BuildFailed, Error: "digests differ: ace sha256:1, g14 sha256:2"} failed := keyBuckets{keys: map[string]builderkeys.Key{"x": k}} if _, _, err := buildOfKey(t.Context(), failed, take, keyContext{}); !errors.Is(err, errKeyFailed) || !strings.Contains(err.Error(), "digests differ: ace sha256:1, g14 sha256:2") { t.Errorf("a failed key: %v", err) } none := take none.Trunk.FP2, none.Trunk.None2 = "", "no lock" if _, _, err := buildOfKey(t.Context(), noRef, none, keyContext{}); !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), "no lock") { t.Errorf("no fingerprint: %v", err) } } // ADR 0501 §7: a manifest word this controller does not read waits for the controller that reads it; never fails. func TestAManifestWordTheControllerDoesNotReadWaits(t *testing.T) { raw := `{"module":"x","version":"1","not-a-word-yet":true,"build":{"artifacts":[{"name":"a","kind":"archive","from":"files"}]}, "resources":[{"id":"a","type":"archive","path":"/opt/x","artifact":"a"}]}` keys, trunk := aKeyOf(raw, 1, sdkdelivery.BuiltArtifact{Name: "a", Kind: "archive", Digest: shaOf("a"), Reference: "artifact-store://x/a/blobs/" + shaOf("a")}) _, _, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{}) if !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), `"not-a-word-yet"`) { t.Fatalf("an unread word: %v", err) } } func packed(t *testing.T, files map[string]string) []byte { t.Helper() var buf bytes.Buffer z := gzip.NewWriter(&buf) w := tar.NewWriter(z) for name, text := range files { if err := w.WriteHeader(&tar.Header{Name: name, Mode: 0o644, Size: int64(len(text)), Typeflag: tar.TypeReg}); err != nil { t.Fatal(err) } if _, err := w.Write([]byte(text)); err != nil { t.Fatal(err) } } if err := w.Close(); err != nil { t.Fatal(err) } if err := z.Close(); err != nil { t.Fatal(err) } return buf.Bytes() } // A templated module's texts are read back from the archive the store holds under the key's reference; an archive the // store does not answer is waited for (the lead's Q1b). func TestATemplatedModulesTextsAreReadFromItsArchive(t *testing.T) { raw := `{"module":"x","version":"1","templates":"templates", "build":{"artifacts":[{"name":"templates","kind":"archive","from":"templates"}]}, "resources":[{"id":"main","type":"file","path":"/etc/x.conf","mode":"0644","template":"templates/x.conf"}]}` body := packed(t, map[string]string{"x.conf": "listen 1\n"}) sum := sha256.Sum256(body) digest := "sha256:" + hex.EncodeToString(sum[:]) keys, trunk := aKeyOf(raw, 1, sdkdelivery.BuiltArtifact{Name: "templates", Kind: "archive", Digest: digest, Reference: "artifact-store://x/templates/blobs/" + digest}) asked := "" archive := func(_ context.Context, reference string) ([]byte, error) { asked = reference; return body, nil } _, resolved, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{archive: archive}) if err != nil { t.Fatal(err) } if asked != "artifact-store://x/templates/blobs/"+digest { t.Errorf("the archive asked: %q", asked) } if text, _ := json.Marshal(resolved.Resources); !strings.Contains(string(text), "listen 1") { t.Errorf("the template's text is not in the file: %s", text) } gone := func(context.Context, string) ([]byte, error) { return nil, errors.New("the store does not hold it") } if _, _, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{archive: gone}); !errors.Is(err, errKeyWaits) || !strings.Contains(err.Error(), "cannot be read from the artifact store") { t.Errorf("an archive the store does not answer: %v", err) } } // The launchers a key's bundle is resolved with are mesh-sdk's one rule (buildstep.Launchers), the names // mesh-build-node writes: a TypeScript bundle's entrypoint x.js runs as x.serve.mjs, a Go bundle has none. func TestALaunchersRuleIsTheBuildModules(t *testing.T) { m, err := catalogue.ParseManifest([]byte(tsBundle)) if err != nil { t.Fatal(err) } k := builderkeys.Key{Build: sdkdelivery.BuilderBuild{Artifacts: []sdkdelivery.BuiltArtifact{{Name: "tools", Digest: "sha256:1", Reference: "artifact-store://x/tools/blobs/sha256:1"}}}} built, err := builtOfKey(t.Context(), nil, m, k) if err != nil { t.Fatal(err) } if built[0].Launchers["dist/index.js"] != "dist/index.serve.mjs" || built[0].Kind != "bundle" { t.Fatalf("built: %+v", built[0]) } }