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() {