From be62f49eab18c571bdc9a30943b6e6af72be3d3c Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 1 Sep 2026 21:18:14 +0200 Subject: [PATCH] What provides the artifact store cannot be delivered through it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refused where it is written: a module that provides `artifact-store` and also builds artifacts asks the mesh to put an artifact into the thing that artifact is needed to create. Building publishes to the store, and the builder will not start without one — "a built artifact nobody can fetch is not built". This is the question the substrate record asks of every candidate: can it grant itself the thing it provides? The store cannot create its own database, the broker cannot create its own virtual host, and a registry cannot grant itself a repository. The first two are why they are in the bundle. This is the same sentence, unenforced. So such a module names its image, exactly as the bundle names the three a first node starts from. One that wants an interface or a tool server beside it is a second module, mirrored the ordinary way once the first is running — a real limit, and better said here than discovered on a mesh new enough that nobody is watching it. Refused at the manifest because the alternative is a build that never returns. The provision name is now a constant. A string compared in one place is a convention; a string a rule turns on is a fact. --- internal/catalogue/machineside_test.go | 49 +++++++++++++++++++++++++- internal/catalogue/manifest.go | 36 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/internal/catalogue/machineside_test.go b/internal/catalogue/machineside_test.go index 553e1a5..82d163d 100644 --- a/internal/catalogue/machineside_test.go +++ b/internal/catalogue/machineside_test.go @@ -1,6 +1,9 @@ package catalogue -import "testing" +import ( + "strings" + "testing" +) // A module with nothing that publishes binds what it binds, and the mesh may not move it. // @@ -58,3 +61,47 @@ func TestAPortNotInTheMappingIsNotFound(t *testing.T) { at, mayAssign) } } + +// The module that provides the artifact store may not be delivered through it. +// +// Building publishes to the store and the builder will not start without one, so a module that +// provides the store and also builds something asks the mesh to put an artifact into the thing +// that artifact is needed to create. On a mesh new enough to have no registry, that is a build +// that never returns (novox/hq 04-ISSUES/029). +func TestTheArtifactStoreCannotBeDeliveredThroughItself(t *testing.T) { + _, err := ParseManifest([]byte(`{"module":"registry","version":"1",` + + `"provides":[{"name":"artifact-store","scope":"mesh"}],` + + `"build":{"artifacts":[{"name":"registry","kind":"upstream","from":"registry:2"}]},` + + `"resources":[{"id":"store","type":"container","name":"mesh-registry",` + + `"artifact":"registry","ports":["5000:5000"]}]}`)) + if err == nil { + t.Fatal("a registry module that builds its own image was accepted; the build has " + + "nowhere to publish until the module it belongs to is already running") + } + if !strings.Contains(err.Error(), "artifact-store") { + t.Fatalf("refused without naming the provision the cycle turns on: %v", err) + } +} + +// Naming the image directly is the way out, and must stay accepted. +func TestAnArtifactStoreThatNamesItsImageIsAccepted(t *testing.T) { + _, err := ParseManifest([]byte(`{"module":"registry","version":"1",` + + `"provides":[{"name":"artifact-store","scope":"mesh"}],` + + `"resources":[{"id":"store","type":"container","name":"mesh-registry",` + + `"image":"registry@sha256:` + + `266f282fabd7cd3df053ee7c658c77b42380d1a2f0d8e5a1c0d7a6d5b5c4a3b2",` + + `"ports":["5000:5000"]}]}`)) + if err != nil { + t.Fatalf("the one way an artifact store can be delivered was refused: %v", err) + } +} + +// And an ordinary module still builds whatever it likes. +func TestAModuleThatDoesNotProvideTheStoreStillBuilds(t *testing.T) { + _, err := ParseManifest([]byte(`{"module":"forge","version":"1",` + + `"build":{"artifacts":[{"name":"forge","kind":"upstream","from":"gitea/gitea:1.22"}]},` + + `"resources":[{"id":"run","type":"container","name":"forge","artifact":"forge"}]}`)) + if err != nil { + t.Fatalf("an ordinary module was caught by a rule about the artifact store: %v", err) + } +} diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index 910a09f..5c5bef7 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -310,6 +310,14 @@ const ( ArtifactUpstream = "upstream" ) +// ArtifactStoreProvision is the name a module offers when it is the mesh's store for what modules +// ship — images, and archives, which are directories from a repository packed as blobs. +// +// Named here because a rule depends on it: what provides this cannot be delivered through it +// (novox/hq 04-ISSUES/029). A string compared in one place is a convention; a string a rule turns +// on is a fact, and it should be written once. +const ArtifactStoreProvision = "artifact-store" + // Listening is one port a module accepts connections on. type Listening struct { Port int `json:"port"` @@ -516,6 +524,34 @@ func ParseManifest(raw []byte) (Manifest, error) { } } problems = append(problems, m.Build.problems(m.Module)...) + // **What provides the artifact store cannot be delivered through it** (novox/hq 04-ISSUES/029). + // + // Building publishes to the store, and the builder will not start without one. So a module + // that provides the store and also builds something asks the mesh to put an artifact into the + // thing that artifact is needed to create. + // + // It is the question the substrate record asks of every candidate — can it grant itself the + // thing it provides? The store cannot create its own database, the broker cannot create its + // own virtual host, and a registry cannot grant itself a repository. Such a module names its + // image, exactly as the bundle names the three a first node starts from. + // + // Refused here because the alternative is a build that never returns, on a mesh new enough + // that nobody is watching it yet. + if m.Build != nil && len(m.Build.Artifacts) > 0 { + for _, o := range m.Offers() { + if o != ArtifactStoreProvision { + continue + } + problems = append(problems, fmt.Sprintf( + "%s provides %q and also builds %d artifact(s), which cannot both be true: "+ + "building publishes to the artifact store, so this asks the mesh to put an "+ + "artifact into the thing that artifact is needed to create. A module that "+ + "provides the store names its image instead. One that wants more beside it "+ + "— an interface, a tool server — is a second module, mirrored in the "+ + "ordinary way once this one is running", + m.Module, ArtifactStoreProvision, len(m.Build.Artifacts))) + } + } for to := range m.Serves { var offered bool for _, o := range m.Offers() {