Bring F3 up to its release branch, with both sides' store tests (part 5 of 14)
mesh/merge-gate head 14c1e3bb9e7d merged into trunk fcdecb436117 — fail: head 14c1e3bb9e7d does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/repo-check head 14c1e3bb9e7d merged into trunk fcdecb436117 — fail: head 14c1e3bb9e7d does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 14c1e3bb9e7d merged into trunk fcdecb436117 — fail: head 14c1e3bb9e7d does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/repo-check head 14c1e3bb9e7d merged into trunk fcdecb436117 — fail: head 14c1e3bb9e7d does not merge cleanly into trunk fcdecb436117: in conflict cmd/…
mesh/delivery superseded: a newer head of the same pull request
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -268,7 +268,14 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
||||
}
|
||||
// What is judged only where a definition is written (novox/hq ADR 0326 §3, issue 574): a build is
|
||||
// where a changed definition is first read, so it is refused here before anything is made of it.
|
||||
if problems := catalogue.WrittenSettingsProblems(manifest); len(problems) > 0 {
|
||||
// Judged with each template's text in its file, as written in the tree (novox/hq ADR 0349 §2): a template the
|
||||
// module's directory lacks is refused here, before anything is built.
|
||||
judged, err := catalogue.WithTemplatesFrom(manifest, within)
|
||||
if err != nil {
|
||||
say("manifest", "INVALID: %v", err)
|
||||
return Result{}, err
|
||||
}
|
||||
if problems := catalogue.WrittenSettingsProblems(judged); len(problems) > 0 {
|
||||
err := fmt.Errorf("%s", strings.Join(problems, "; "))
|
||||
say("manifest", "INVALID: %v", err)
|
||||
return Result{}, err
|
||||
@@ -410,6 +417,11 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
||||
}
|
||||
}
|
||||
|
||||
// Each template's text, read back from the archive the build made or took (novox/hq ADR 0349 §2).
|
||||
if built, err = withTemplateTexts(manifest, within, built); err != nil {
|
||||
say("templates", "FAILED: %v", err)
|
||||
return Result{}, err
|
||||
}
|
||||
resolved, err := manifest.Resolve(built)
|
||||
if err != nil {
|
||||
say("resolve", "FAILED: %v", err)
|
||||
@@ -1076,6 +1088,16 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
||||
return catalogue.Built{}, fmt.Errorf("%s: building %s failed: %w", module, a.Name, err)
|
||||
}
|
||||
say("image", "built, publishing")
|
||||
// Its bases linked into its repository first, so the push needs none of their blobs from this
|
||||
// machine's store, which may not hold them (novox/hq issue 694). Said and not fatal: a push
|
||||
// from a store that holds them works without, and one that fails says which blob it lacked.
|
||||
if lender, can := publish.(BaseLender); can {
|
||||
if n, err := lender.LendBases(ctx, module+"/"+a.Name, buildArgValues(args)); err != nil {
|
||||
say("image", "its bases could not all be linked into %s/%s before the push: %v", module, a.Name, err)
|
||||
} else if n > 0 {
|
||||
say("image", "%d blob(s) of its bases linked into %s/%s before the push", n, module, a.Name)
|
||||
}
|
||||
}
|
||||
reference, err := publish.PublishImage(ctx, local, module+"/"+a.Name)
|
||||
if err != nil {
|
||||
return catalogue.Built{}, err
|
||||
@@ -1427,6 +1449,19 @@ func standingOn(ctx context.Context, manifest catalogue.Manifest, held map[strin
|
||||
return args, resolved, nil
|
||||
}
|
||||
|
||||
// buildArgValues is what each --build-arg of a build was handed: the bases, as this mesh holds them.
|
||||
func buildArgValues(args []string) []string {
|
||||
var values []string
|
||||
for i := 0; i+1 < len(args); i += 2 {
|
||||
if args[i] == "--build-arg" {
|
||||
if _, value, has := strings.Cut(args[i+1], "="); has {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// compile runs a module's own code through its toolchain, and says where the result is.
|
||||
//
|
||||
// **In the module's own directory, under the path the toolchain expects.** A module is compiled
|
||||
|
||||
@@ -103,6 +103,23 @@ type CheckSpec struct {
|
||||
// for the walk to take. None for a group's composed check, whose members' own checks build.
|
||||
Builds []CheckBuildSpec
|
||||
BuildWith *CheckBuilding
|
||||
// Tests and StoreOnly narrow what the repository's own script runs, for a branch tried before its pull
|
||||
// request (novox/hq issue 686): handed to the script as EnvTests and EnvStoreOnly, which it may read.
|
||||
Tests string
|
||||
StoreOnly bool
|
||||
}
|
||||
|
||||
// selection is what a check hands the repository's script to narrow what it runs (novox/hq issue 686): nothing
|
||||
// for a pull request's check, which always runs whole.
|
||||
func selection(spec CheckSpec) []string {
|
||||
var env []string
|
||||
if spec.Tests != "" {
|
||||
env = append(env, EnvTests+"="+spec.Tests)
|
||||
}
|
||||
if spec.StoreOnly {
|
||||
env = append(env, EnvStoreOnly+"=1")
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// GroupHead is one other head of a delivery group's composed check.
|
||||
@@ -193,6 +210,11 @@ const (
|
||||
// EnvGroup is a delivery group's other heads, as JSON, for the gate (novox/hq ADR 0239); empty for a
|
||||
// pull request checked alone.
|
||||
EnvGroup = "MESH_CHECK_GROUP"
|
||||
// EnvTests is a `go test -run` pattern and EnvStoreOnly is 1 when a branch tried before its pull request
|
||||
// asks for only those tests, or only the store tests (novox/hq issue 686). Never set for a pull request's
|
||||
// check. A script that reads neither runs whole.
|
||||
EnvTests = "MESH_CHECK_TESTS"
|
||||
EnvStoreOnly = "MESH_CHECK_STORE_ONLY"
|
||||
)
|
||||
|
||||
// CheckTimeout bounds one check; a check that runs past it is an error, not a pass.
|
||||
@@ -515,6 +537,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
EnvModules + "=" + strings.Join(append(append([]string{}, spec.Modules...), spec.New...), ","),
|
||||
EnvGroup + "=" + groupFile,
|
||||
"GOCACHE=" + filepath.Join(workspace, "go-cache"), "GOMODCACHE=" + filepath.Join(workspace, "go-modules")}
|
||||
env = append(env, selection(spec)...)
|
||||
|
||||
v := CheckVerdict{}
|
||||
modules := append(append([]string{}, spec.Modules...), prefixed("new:", spec.New)...)
|
||||
|
||||
@@ -590,3 +590,15 @@ func TestARepositoryInTwoLanguagesIsCheckedInBoth(t *testing.T) {
|
||||
t.Errorf("a part in a toolchain the mesh does not hold answered %+v — never a pass", layer)
|
||||
}
|
||||
}
|
||||
|
||||
// A branch tried before its pull request may narrow what the repository's script runs (novox/hq issue 686); a
|
||||
// pull request's check is handed nothing, and runs whole.
|
||||
func TestOnlyATrysSelectionReachesTheScript(t *testing.T) {
|
||||
if env := selection(CheckSpec{Number: 7}); len(env) != 0 {
|
||||
t.Fatalf("a pull request's check was handed a selection: %v", env)
|
||||
}
|
||||
env := selection(CheckSpec{Tests: "^TestX$", StoreOnly: true})
|
||||
if len(env) != 2 || env[0] != "MESH_CHECK_TESTS=^TestX$" || env[1] != "MESH_CHECK_STORE_ONLY=1" {
|
||||
t.Fatalf("a try's selection: %v", env)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// **A built image's bases are linked into its repository before it is pushed** (novox/hq issue 694).
|
||||
//
|
||||
// A push reads every blob of the image out of this machine's store, unless the repository it goes to
|
||||
// already holds that blob: the runtime asks the registry first, blob by blob, and reads locally only
|
||||
// what the registry lacks. A runtime keeping its images in containerd's store — the default for a
|
||||
// fresh install from docker 29 on — does not always hold the compressed layers of the base an image
|
||||
// was built FROM: a layer whose unpacked form it already had is never fetched. The push of such an
|
||||
// image then fails on a layer this machine never had ("not all of them are available locally"), while
|
||||
// the very bytes sit in this registry, under the repository the base was mirrored into.
|
||||
//
|
||||
// So the base's blobs are mounted into the image's repository first. A mount moves no bytes, and once
|
||||
// the repository holds a blob the push asks this machine for nothing of it — whichever store the
|
||||
// runtime keeps. A runtime that holds every layer (the classic store) is not changed by this: its push
|
||||
// finds the same blobs already there.
|
||||
|
||||
// BaseLender links what a build's bases hold into the repository its image is about to be pushed to.
|
||||
type BaseLender interface {
|
||||
// LendBases mounts every blob of each base this registry holds — for the platform this machine
|
||||
// builds — into `repository`, and answers how many it mounted.
|
||||
LendBases(ctx context.Context, repository string, bases []string) (int, error)
|
||||
}
|
||||
|
||||
// platformed is an index's entry: a manifest and the platform it is for.
|
||||
type platformed struct {
|
||||
descriptor
|
||||
Platform *struct {
|
||||
OS string `json:"os"`
|
||||
Architecture string `json:"architecture"`
|
||||
} `json:"platform"`
|
||||
}
|
||||
|
||||
// LendBases mounts each base's blobs into the repository. A base this registry does not hold under its
|
||||
// repository — named by tag, or held somewhere else — lends nothing and is no error: the push reads it
|
||||
// from this machine's store as before.
|
||||
func (r Registry) LendBases(ctx context.Context, repository string, bases []string) (int, error) {
|
||||
mounted := 0
|
||||
for _, base := range bases {
|
||||
name, digest, pinned := strings.Cut(base, "@")
|
||||
if !pinned || !strings.HasPrefix(digest, "sha256:") {
|
||||
continue
|
||||
}
|
||||
where, err := parseReference(name + "@" + digest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
from := where.repository
|
||||
if from == repository {
|
||||
continue
|
||||
}
|
||||
blobs, held, err := r.blobsOf(ctx, from, digest)
|
||||
if err != nil {
|
||||
return mounted, fmt.Errorf("reading %s: %w", base, err)
|
||||
}
|
||||
if !held {
|
||||
continue
|
||||
}
|
||||
for _, blob := range blobs {
|
||||
did, err := r.mount(ctx, repository, from, blob)
|
||||
if err != nil {
|
||||
return mounted, fmt.Errorf("linking %s of %s into %s: %w", blob, base, repository, err)
|
||||
}
|
||||
if did {
|
||||
mounted++
|
||||
}
|
||||
}
|
||||
}
|
||||
return mounted, nil
|
||||
}
|
||||
|
||||
// blobsOf is the config and layers of the manifest under repository@digest — for an index, of each
|
||||
// manifest it names for this machine's platform. False when the registry holds no such manifest there.
|
||||
func (r Registry) blobsOf(ctx context.Context, repository, digest string) ([]string, bool, error) {
|
||||
body, held, err := r.manifest(ctx, repository, digest)
|
||||
if err != nil || !held {
|
||||
return nil, held, err
|
||||
}
|
||||
var document struct {
|
||||
Manifests []platformed `json:"manifests"`
|
||||
Config *descriptor `json:"config"`
|
||||
Layers []descriptor `json:"layers"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &document); err != nil {
|
||||
return nil, false, fmt.Errorf("%s@%s is not a manifest: %w", repository, digest, err)
|
||||
}
|
||||
var blobs []string
|
||||
if document.Config != nil {
|
||||
blobs = append(blobs, document.Config.Digest)
|
||||
}
|
||||
for _, l := range document.Layers {
|
||||
blobs = append(blobs, l.Digest)
|
||||
}
|
||||
for _, m := range document.Manifests {
|
||||
if m.Platform == nil || m.Platform.OS != runtime.GOOS || m.Platform.Architecture != runtime.GOARCH {
|
||||
continue
|
||||
}
|
||||
more, held, err := r.blobsOf(ctx, repository, m.Digest)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if held {
|
||||
blobs = append(blobs, more...)
|
||||
}
|
||||
}
|
||||
return blobs, true, nil
|
||||
}
|
||||
|
||||
// manifest reads one manifest document from this registry; false when it is not there.
|
||||
func (r Registry) manifest(ctx context.Context, repository, digest string) ([]byte, bool, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||
"http://"+r.Address+"/v2/"+repository+"/manifests/"+digest, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
request.Header.Set("Accept", manifestAccept)
|
||||
response, err := r.client().Do(request)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("cannot reach the registry at %s: %w", r.Address, err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil, false, nil
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, false, fmt.Errorf("%s/%s@%s: %s", r.Address, repository, digest, response.Status)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, 4<<20))
|
||||
return body, err == nil, err
|
||||
}
|
||||
|
||||
// mount links one blob from one repository into another; false when the repository already held it.
|
||||
func (r Registry) mount(ctx context.Context, repository, from, digest string) (bool, error) {
|
||||
base := "http://" + r.Address + "/v2/" + repository
|
||||
if there, err := r.has(ctx, base+"/blobs/"+digest); err != nil {
|
||||
return false, err
|
||||
} else if there {
|
||||
return false, nil
|
||||
}
|
||||
start, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
base+"/blobs/uploads/?mount="+digest+"&from="+from, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
begun, err := r.client().Do(start)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot reach the registry at %s: %w", r.Address, err)
|
||||
}
|
||||
begun.Body.Close()
|
||||
if begun.StatusCode == http.StatusCreated {
|
||||
return true, nil
|
||||
}
|
||||
if begun.StatusCode == http.StatusAccepted {
|
||||
// Not mounted, and an upload begun in its place: let go of it, so nothing is left half-open.
|
||||
if location := begun.Header.Get("Location"); location != "" {
|
||||
if strings.HasPrefix(location, "/") {
|
||||
location = "http://" + r.Address + location
|
||||
}
|
||||
if cancel, err := http.NewRequestWithContext(ctx, http.MethodDelete, location, nil); err == nil {
|
||||
if done, err := r.client().Do(cancel); err == nil {
|
||||
done.Body.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("%s did not mount it from %s", r.Address, from)
|
||||
}
|
||||
return false, fmt.Errorf("%s answered %s when asked to mount it from %s", r.Address, begun.Status, from)
|
||||
}
|
||||
Reference in New Issue
Block a user