diff --git a/internal/inventory/catalogue.go b/internal/inventory/catalogue.go index e29f994..8a3401b 100644 --- a/internal/inventory/catalogue.go +++ b/internal/inventory/catalogue.go @@ -431,6 +431,9 @@ func (i *Inventory) Assign(ctx context.Context, nodeName, module string) error { if err != nil { return err } + if err := i.runsSomewhere(ctx, module); err != nil { + return err + } _, err = i.store.Pool().Exec(ctx, `insert into assignment (node, module) values ($1, $2) on conflict do nothing`, node.ID, module) @@ -871,3 +874,43 @@ func (i *Inventory) Running(ctx context.Context, module string) ([]string, error } return out, rows.Err() } + +// runsSomewhere refuses to put a module on a machine when it would put nothing there. +// +// **Not every thing the mesh builds is a thing a machine runs.** The image every module in the +// scripted toolchain is compiled on top of is built, versioned and depended upon like a module, and +// is registered as one so the mesh can track all three. It is not one: it declares no resources, +// because nothing about it belongs on a machine — its whole purpose is to be the starting point for +// other modules' builds. +// +// Without this, assigning it succeeds, the machine is sent a declaration containing nothing of it, +// and everything reports success. The operator has said "run this here" and the mesh has agreed to +// something it cannot do. Said at the assignment, which is where somebody is standing. +// +// A module whose resources are worked out per node declares none here and is still assignable — +// that is the point of it — so it is asked about separately rather than caught by the same test. +func (i *Inventory) runsSomewhere(ctx context.Context, module string) error { + var raw []byte + err := i.store.Pool().QueryRow(ctx, + `select manifest from module where name = $1`, module).Scan(&raw) + if errors.Is(err, pgx.ErrNoRows) { + // Left to the insert, which already says this and says it the same way everywhere. + return nil + } + if err != nil { + return err + } + var m catalogue.Manifest + if err := json.Unmarshal(raw, &m); err != nil { + // Unreadable is not the same as empty. A manifest the mesh cannot parse is a separate + // fault and refusing the assignment here would report it as the wrong one. + return nil + } + if m.Computed != "" || len(m.Resources) > 0 { + return nil + } + return fmt.Errorf( + "%s puts nothing on a machine, so there is nothing to assign. It is something this mesh "+ + "builds and other modules are built on top of, not something a machine runs — "+ + "`module list` shows what it produces", module) +}