232 lines
11 KiB
Go
232 lines
11 KiB
Go
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])
|
|
}
|
|
}
|