Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b4c7e0e0d | ||
|
|
cec792ce9d | ||
|
|
338d033632 | ||
|
|
1be926cec4 | ||
|
|
2134768dfe | ||
|
|
ef825688ee | ||
|
|
77a14360df | ||
|
|
9be2fb4750 |
@@ -186,7 +186,7 @@ func (r Registry) MirrorImage(ctx context.Context, from, repository string) (str
|
||||
// on the first merge that rebuilt a whole catalogue (2026-09-28), and every module whose base
|
||||
// lives there failed on a copy it did not need.
|
||||
if strings.HasPrefix(where.reference, "sha256:") {
|
||||
held, err := r.has(ctx, "http://"+r.Address+"/v2/"+repository+"/manifests/"+where.reference)
|
||||
held, err := r.has(ctx, "http://"+r.Address+"/v2/"+repository+"/manifests/"+where.reference, manifestAccept)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("asking %s whether it holds %s: %w", r.Address, from, err)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,14 @@ func (m *theMeshsRegistry) handler() http.Handler {
|
||||
defer m.mu.Unlock()
|
||||
switch {
|
||||
case r.Method == http.MethodHead && strings.Contains(r.URL.Path, "/manifests/"):
|
||||
// **As strictly as a real registry.** A manifest is answered only in a media type the
|
||||
// caller named; a request with no Accept is answered as if nothing were there. The fake
|
||||
// used to answer regardless, which is why it could not catch a check that asked without
|
||||
// one — and the mesh copied every base again (2026-09-28).
|
||||
if !strings.Contains(r.Header.Get("Accept"), "manifest") && !strings.Contains(r.Header.Get("Accept"), "index") {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if _, ok := m.manifests[r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]]; ok {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
|
||||
@@ -122,11 +122,23 @@ func (r Registry) PublishArchive(ctx context.Context, repository string, body []
|
||||
return final, nil
|
||||
}
|
||||
|
||||
func (r Registry) has(ctx context.Context, url string) (bool, error) {
|
||||
// has is whether this registry already holds what is at that URL.
|
||||
//
|
||||
// **A manifest HEAD must say what it accepts.** A registry answers a manifest request only in a media
|
||||
// type the caller named, and a bare HEAD — no Accept at all — is answered 404 for a manifest it holds
|
||||
// perfectly well. Measured against the mesh's own registry (2026-09-28): the same digest answered 200
|
||||
// with the manifest media types and 404 without them, so a check written without them concluded the
|
||||
// registry held nothing, copied every base again, and exhausted the public hub's pull limit. A blob
|
||||
// needs no Accept, which is why this went unnoticed: the same helper was right for blobs and wrong
|
||||
// for manifests.
|
||||
func (r Registry) has(ctx context.Context, url string, accept ...string) (bool, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, media := range accept {
|
||||
request.Header.Set("Accept", media)
|
||||
}
|
||||
response, err := r.client().Do(request)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot reach the registry at %s: %w", r.Address, err)
|
||||
|
||||
@@ -1685,7 +1685,10 @@ func prepared(from map[string]any) map[string]any {
|
||||
for k, v := range from {
|
||||
step[k] = v
|
||||
}
|
||||
step["id"] = fmt.Sprint(from["id"]) + ".prepare"
|
||||
// **A hyphen, not a dot.** A resource's id is `<module>.<its own id>`, and a module's name may
|
||||
// itself contain a dot (`novox.be`), so the module is everything before the *last* dot — which
|
||||
// only works if what the mesh derives adds no dot of its own.
|
||||
step["id"] = fmt.Sprint(from["id"]) + "-prepare"
|
||||
step["name"] = fmt.Sprint(from["name"]) + "-prepare"
|
||||
step["run-once"] = true
|
||||
step["args"] = []any{PreparationArgument}
|
||||
|
||||
@@ -3,6 +3,7 @@ package catalogue
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -57,13 +58,18 @@ func TestThePreparationRunsTheModulesOwnCodeAndComesRightBeforeIt(t *testing.T)
|
||||
ids := idsOf(out)
|
||||
at := -1
|
||||
for i, id := range ids {
|
||||
if id == "gitea.runtime.prepare" {
|
||||
if id == "gitea.runtime-prepare" {
|
||||
at = i
|
||||
}
|
||||
}
|
||||
if at < 0 {
|
||||
t.Fatalf("nothing prepares this module's state: %v", ids)
|
||||
}
|
||||
// A module's name may contain a dot, so a resource's module is everything before the last one —
|
||||
// which the derived id must not add to, or a machine reads the wrong owner from it.
|
||||
if strings.Count("gitea.runtime-prepare", ".") != 1 {
|
||||
t.Fatal("the derived id adds a dot, so what owns it cannot be read from it")
|
||||
}
|
||||
if ids[at+1] != "gitea.runtime" {
|
||||
t.Fatalf("the preparation is not immediately before the module's own code: %v", ids)
|
||||
}
|
||||
@@ -79,7 +85,7 @@ func TestThePreparationRunsTheModulesOwnCodeAndComesRightBeforeIt(t *testing.T)
|
||||
func TestThePreparationIsGivenWhatTheModuleIsGiven(t *testing.T) {
|
||||
out := declaredFor(t, aPreparingModule())
|
||||
declared := byID(out)
|
||||
step, workload := declared["gitea.runtime.prepare"], declared["gitea.runtime"]
|
||||
step, workload := declared["gitea.runtime-prepare"], declared["gitea.runtime"]
|
||||
if step == nil || workload == nil {
|
||||
t.Fatalf("expected both, got %v", idsOf(out))
|
||||
}
|
||||
@@ -108,7 +114,7 @@ func TestAModuleThatPreparesNothingGetsNoStep(t *testing.T) {
|
||||
m := aPreparingModule()
|
||||
m.Prepares = false
|
||||
for _, id := range idsOf(declaredFor(t, m)) {
|
||||
if id == "gitea.runtime.prepare" {
|
||||
if id == "gitea.runtime-prepare" {
|
||||
t.Fatal("a module that prepares nothing was given a preparation")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user