package builder import ( "context" "encoding/json" "net/http/httptest" "runtime" "strings" "testing" ) // A built image's bases are linked into its repository before it is pushed (novox/hq issue 694): a // runtime keeping containerd's image store may not hold a base layer it never had to fetch, and its push // asks this machine only for what the repository lacks. // aBaseForThisMachine puts an index into the registry under `repository`: one manifest for this // machine's platform, one for another, and answers the index's digest and each manifest's blobs. func aBaseForThisMachine(t *testing.T, dst *aRegistryOfRepositories, repository string) (string, []string, []string) { t.Helper() put := func(arch string) (string, []string) { config := []byte(`{"architecture":"` + arch + `"}`) layer := []byte(arch + " layer bytes") for _, b := range [][]byte{config, layer} { dst.blobs[digestOf(b)] = b dst.link(repository, digestOf(b)) } m, _ := json.Marshal(map[string]any{ "schemaVersion": 2, "mediaType": mediaManifestOCI, "config": map[string]any{"mediaType": "application/vnd.oci.image.config.v1+json", "digest": digestOf(config), "size": len(config)}, "layers": []map[string]any{{"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": digestOf(layer), "size": len(layer)}}, }) dst.manifests[repository+"@"+digestOf(m)] = m return digestOf(m), []string{digestOf(config), digestOf(layer)} } other := "s390x" if runtime.GOARCH == other { other = "amd64" } mine, mineBlobs := put(runtime.GOARCH) theirs, theirBlobs := put(other) index, _ := json.Marshal(map[string]any{ "schemaVersion": 2, "mediaType": mediaIndexOCI, "manifests": []map[string]any{ {"mediaType": mediaManifestOCI, "digest": mine, "size": 1, "platform": map[string]string{"os": runtime.GOOS, "architecture": runtime.GOARCH}}, {"mediaType": mediaManifestOCI, "digest": theirs, "size": 1, "platform": map[string]string{"os": "linux", "architecture": other}}, }, }) dst.manifests[repository+"@"+digestOf(index)] = index return digestOf(index), mineBlobs, theirBlobs } func TestABuildsBaseIsLinkedIntoItsRepositoryBeforeThePush(t *testing.T) { dst := newRegistryOfRepositories() server := httptest.NewServer(dst.handler()) defer server.Close() address := strings.TrimPrefix(server.URL, "http://") r := Registry{Address: address} const from = "upstream/docker.io/library/golang" index, mine, theirs := aBaseForThisMachine(t, dst, from) bases := []string{ address + "/" + from + "@" + index, // Not bases this registry holds: a package version, a tag, an image held nowhere here. "@novox/mesh-sdk@1.2.3", "golang:1-alpine", "elsewhere.invalid/thing@sha256:" + strings.Repeat("e", 64), } n, err := r.LendBases(context.Background(), "mesh-tools-go/build", bases) if err != nil { t.Fatal(err) } // Every blob of this machine's platform is in the image's repository, so a push asks this machine's // store for none of them; another platform's are not, since nothing built here stands on them. for _, blob := range mine { if !dst.links["mesh-tools-go/build"][blob] { t.Fatalf("the base's blob %s is not in the image's repository", blob) } } for _, blob := range theirs { if dst.links["mesh-tools-go/build"][blob] { t.Fatalf("another platform's blob %s was linked", blob) } } if n != len(mine) || dst.mounts != len(mine) || dst.uploads != 0 { t.Fatalf("%d linked, %d mounts, %d uploads; want %d mounts and no bytes moved", n, dst.mounts, dst.uploads, len(mine)) } // Asked again, nothing is mounted twice. if n, err := r.LendBases(context.Background(), "mesh-tools-go/build", bases); err != nil || n != 0 { t.Fatalf("a second lending mounted %d: %v", n, err) } } // A registry that will not mount is said, not hidden: the push is still tried, from the store. func TestABaseTheRegistryWillNotMountIsAnError(t *testing.T) { dst := newRegistryOfRepositories() dst.noMount = true server := httptest.NewServer(dst.handler()) defer server.Close() address := strings.TrimPrefix(server.URL, "http://") index, _, _ := aBaseForThisMachine(t, dst, "upstream/thing") _, err := Registry{Address: address}.LendBases(context.Background(), "hello/server", []string{address + "/upstream/thing@" + index}) if err == nil || !strings.Contains(err.Error(), "did not mount") { t.Fatalf("a refused mount passed silently: %v", err) } } // lending records what a build lent, and when, against the push. type lending struct { *mirroring order []string } func (l *lending) LendBases(_ context.Context, repository string, bases []string) (int, error) { l.order = append(l.order, "lend "+repository+" "+strings.Join(bases, " ")) return len(bases), nil } func (l *lending) PublishImage(ctx context.Context, localTag, repository string) (string, error) { l.order = append(l.order, "push "+repository) return l.mirroring.PublishImage(ctx, localTag, repository) } func TestAnImageIsPushedOnlyAfterItsBasesAreLent(t *testing.T) { r, workspace := aRepository(t, anImage, map[string]string{"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}"}) r.contents["modules/bus/module.json"] = anImage l := &lending{mirroring: &mirroring{recorded: r, at: "127.0.0.1:5000"}} if _, err := Build(context.Background(), r.run, l, "https://forge.invalid/catalogue.git", "modules/bus", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil { t.Fatal(err) } want := []string{ "lend bus/server 127.0.0.1:5000/upstream/docker.io/vendor/server@sha256:" + strings.Repeat("1", 64), "push bus/server", } if strings.Join(l.order, "\n") != strings.Join(want, "\n") { t.Fatalf("the build did:\n%s\nwant:\n%s", strings.Join(l.order, "\n"), strings.Join(want, "\n")) } }