The mesh knows where a module came from, and whether it is behind
Delivery is a comparison, not a pipeline: the control plane holds what source
exists and what has been built from it, and the difference is the work. Both
halves are written down now, so "is this current" is a question about two
columns rather than something you find out by building.
`status` answers "did my change go out?", which ADR 0010 names as the real risk
of replacing a pipeline with a comparison -- it is answerable today by opening
a pipeline, and something had to replace that.
zsh holds 4f2a9c1e, source has 9e3b7d2a
running on laptop
The machines are the point. A module being out of date is a fact about the
catalogue; which machines are running last week's version is the thing with
consequences.
Three things this had to get right.
A module with no source is never behind -- it was handed over directly, which
is how a one-off arrives, and saying "out of date" about it would be inventing
a comparison against nothing.
A source nobody has checked is not behind either. Reporting it as behind would
put every module on the list the moment provenance was recorded, which makes
the list say nothing. Fault injection found this: my first test passed with the
guard removed, because both halves were empty strings and compared equal. The
case that actually needed it -- a known commit and an unknown head -- was
untested.
And handing over a manifest by hand does not erase where the module normally
comes from. Fixing something in a hurry is legitimate; silently forgetting its
origin is not, because that record is the only thing that would say afterwards
that a machine is running something nobody can rebuild.
Also fixed the flag parsing, which stopped at the first positional argument and
silently ignored every flag after it -- so `module add thing.json --source x`
recorded no source at all and said it had succeeded. The host's own parser
documents this exact footgun and I wrote it again anyway.
This commit is contained in:
@@ -20,25 +20,135 @@ var ErrNoSuchModule = errors.New("no module of that name")
|
||||
// removing the record would leave the mesh unable to describe what is on it.
|
||||
var ErrStillAssigned = errors.New("that module is still assigned to nodes")
|
||||
|
||||
// Source is where a module comes from and what has been built from it.
|
||||
type Source struct {
|
||||
Repository string
|
||||
Ref string
|
||||
// BuiltFrom is the commit the manifest the mesh holds was read at.
|
||||
BuiltFrom string
|
||||
// Head is the newest commit the source is known to have.
|
||||
Head string
|
||||
}
|
||||
|
||||
// Current reports whether what the mesh holds is what the source last had.
|
||||
//
|
||||
// A module with no source is always current: it was handed over directly, and there is nothing
|
||||
// it could be behind. Saying "out of date" about it would be inventing a comparison.
|
||||
func (s Source) Current() bool {
|
||||
if s.Repository == "" || s.Head == "" {
|
||||
return true
|
||||
}
|
||||
return s.BuiltFrom == s.Head
|
||||
}
|
||||
|
||||
// RegisterModule records a module, replacing what was there.
|
||||
//
|
||||
// Replacing rather than refusing, because a manifest changing is the ordinary case -- a module
|
||||
// gains a requirement, a claim, a resource. What matters is that the change is visible the next
|
||||
// time a node is resolved, which it is.
|
||||
func (i *Inventory) RegisterModule(ctx context.Context, m catalogue.Manifest) error {
|
||||
func (i *Inventory) RegisterModule(ctx context.Context, m catalogue.Manifest, from Source) error {
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// A module registered without provenance keeps whatever it had. Handing over a manifest by
|
||||
// hand is a legitimate way to fix something in a hurry, and it should not silently erase the
|
||||
// record of where the module normally comes from — which is the only thing that would say,
|
||||
// afterwards, that the machine is running something nobody can rebuild.
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`insert into module (name, manifest, version) values ($1, $2, nullif($3,''))
|
||||
on conflict (name) do update set manifest = excluded.manifest,
|
||||
version = excluded.version,
|
||||
registered = now()`,
|
||||
m.Module, raw, m.Version)
|
||||
`insert into module (name, manifest, version, source, ref, built_from, source_head)
|
||||
values ($1, $2, nullif($3,''), nullif($4,''), nullif($5,''), nullif($6,''), nullif($6,''))
|
||||
on conflict (name) do update set
|
||||
manifest = excluded.manifest,
|
||||
version = excluded.version,
|
||||
registered = now(),
|
||||
source = coalesce(excluded.source, module.source),
|
||||
ref = coalesce(excluded.ref, module.ref),
|
||||
built_from = coalesce(excluded.built_from, module.built_from),
|
||||
source_head = coalesce(excluded.built_from, module.source_head)`,
|
||||
m.Module, raw, m.Version, from.Repository, from.Ref, from.BuiltFrom)
|
||||
return err
|
||||
}
|
||||
|
||||
// SourceMoved records that a module's source has a newer commit than the mesh has built.
|
||||
//
|
||||
// This is the whole of noticing. Nothing here builds anything — it writes down that the two
|
||||
// halves differ, which is what makes *is this current?* answerable without building, and what
|
||||
// makes a module that nobody rebuilt visible rather than silent.
|
||||
func (i *Inventory) SourceMoved(ctx context.Context, module, head string) error {
|
||||
tag, err := i.store.Pool().Exec(ctx,
|
||||
`update module set source_head = $2, source_seen = now() where name = $1`, module, head)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return fmt.Errorf("%w: %s", ErrNoSuchModule, module)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SourceOf is where a module came from and whether the mesh is behind it.
|
||||
func (i *Inventory) SourceOf(ctx context.Context, module string) (Source, error) {
|
||||
var s Source
|
||||
var repo, ref, built, head *string
|
||||
err := i.store.Pool().QueryRow(ctx,
|
||||
`select source, ref, built_from, source_head from module where name = $1`,
|
||||
module).Scan(&repo, &ref, &built, &head)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return Source{}, fmt.Errorf("%w: %s", ErrNoSuchModule, module)
|
||||
}
|
||||
if err != nil {
|
||||
return Source{}, err
|
||||
}
|
||||
for _, pair := range []struct {
|
||||
from *string
|
||||
to *string
|
||||
}{{repo, &s.Repository}, {ref, &s.Ref}, {built, &s.BuiltFrom}, {head, &s.Head}} {
|
||||
if pair.from != nil {
|
||||
*pair.to = *pair.from
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Behind is every module the mesh has not built from what its source now has, with the nodes
|
||||
// running the old one.
|
||||
//
|
||||
// The nodes are the point. "Is this module out of date" is a fact about the catalogue; "which
|
||||
// machines are running last week's version" is the question somebody actually has, and it is the
|
||||
// one novox/hq ADR 0010 names as the thing that must not be lost.
|
||||
func (i *Inventory) Behind(ctx context.Context) (map[string][]string, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select m.name, coalesce(n.name, '')
|
||||
from module m
|
||||
left join assignment a on a.module = m.name
|
||||
left join node n on n.id = a.node
|
||||
where m.source is not null
|
||||
and m.source_head is not null
|
||||
and coalesce(m.built_from, '') is distinct from m.source_head
|
||||
order by m.name, n.name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := map[string][]string{}
|
||||
for rows.Next() {
|
||||
var module, node string
|
||||
if err := rows.Scan(&module, &node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, seen := out[module]; !seen {
|
||||
out[module] = nil
|
||||
}
|
||||
if node != "" {
|
||||
out[module] = append(out[module], node)
|
||||
}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Catalogue is every module the mesh knows about, which is what resolution needs: the question
|
||||
// "how many modules provide this" cannot be asked of a subset.
|
||||
func (i *Inventory) Catalogue(ctx context.Context) (map[string]catalogue.Manifest, error) {
|
||||
|
||||
Reference in New Issue
Block a user