artifact-store-tools: say an image a definition pins is named by a definition, under any name the store has had (issue 323)
mesh/merge-gate pass: builds artifact-store-tools → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery stopped: the pull request closed unmerged

No record names an image the mesh did not make, so one a definition pins under an
older name of the store read as unrecorded, the same word as an image nothing
uses. The tool now reads every definition through the catalogue and says
named-by-a-definition with the module. The store's older names are the mesh's
setting older-store-names, since a catalogue names no installation. The
catalogue's check warns of a pin under an older name with the current one.
This commit is contained in:
jochen
2026-10-12 14:32:31 +02:00
parent 2772120758
commit c0a24882f4
14 changed files with 1005 additions and 33 deletions
+3
View File
@@ -0,0 +1,3 @@
module git.novox.be/novox/mesh-catalog/checks/storenames
go 1.22
+132
View File
@@ -0,0 +1,132 @@
// Command storenames warns, in every module's manifest, of an image pinned by digest under a name the
// artifact store had before (novox/hq issue 323). Such a pin still pulls, and artifact_store_references says
// it is named by a definition; the warning says the store's current name, so a definition moves to it when
// it next changes. It warns and never refuses: the image runs, and nothing is wrong with it but its name.
//
// The older names are listed in older-names beside this check, one per line: the same names the mesh's
// setting older-store-names of artifact-store-tools gives the store's tools, which read definitions under
// them. A manifest names no installation, so the tools have them from the mesh's setting; this check runs
// before the mesh is asked anything, so it has them from its list.
//
// go run . <catalogue root>
//
// Exits 1 only when a manifest cannot be read.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// OlderNamesFile is the list of the store's older names, beside the check.
const OlderNamesFile = "checks/storenames/older-names"
// Current is the store's name a definition writes now: the scheme the controller routes to wherever the
// store is.
const Current = "artifact-store://"
// Finding is one pin under an older name.
type Finding struct {
Module, Pinned, Instead string
}
// OlderNames reads the older names from the list: one per line, a # beginning a remark.
func OlderNames(root string) ([]string, error) {
raw, err := os.ReadFile(filepath.Join(root, OlderNamesFile))
if err != nil {
return nil, err
}
var out []string
for _, line := range strings.Split(string(raw), "\n") {
if at := strings.Index(line, "#"); at >= 0 {
line = line[:at]
}
if n := strings.Trim(strings.TrimSpace(line), "/"); n != "" {
out = append(out, n)
}
}
return out, nil
}
// pinUnder is `<name>/<repository>@sha256:<hex>`, the name standing on its own.
func pinUnder(name string) *regexp.Regexp {
return regexp.MustCompile(`(?:^|[^A-Za-z0-9.-])` + regexp.QuoteMeta(name) + `/([a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64})`)
}
// Check reads every manifest under root/modules and answers each pin under an older name.
func Check(root string) ([]Finding, error) {
older, err := OlderNames(root)
if err != nil {
return nil, err
}
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Finding
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var doc any
if err := json.Unmarshal(raw, &doc); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
module := filepath.Base(filepath.Dir(path))
seen := map[string]bool{}
var walk func(any)
walk = func(x any) {
switch v := x.(type) {
case map[string]any:
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
walk(v[k])
}
case []any:
for _, e := range v {
walk(e)
}
case string:
for _, name := range older {
for _, m := range pinUnder(name).FindAllStringSubmatch(v, -1) {
pinned := name + "/" + m[1]
if !seen[pinned] {
seen[pinned] = true
out = append(out, Finding{module, pinned, Current + m[1]})
}
}
}
}
}
walk(doc)
}
return out, nil
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
for _, f := range found {
fmt.Printf("WARNING %s pins %s under an older name of the artifact store: its current name is %s "+
"(novox/hq issue 323; a warning, not a refusal)\n", f.Module, f.Pinned, f.Instead)
}
fmt.Printf("store-names: %d pins under an older name of the artifact store\n", len(found))
}
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func manifest(t *testing.T, root, module, body string) {
t.Helper()
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func older(t *testing.T, root, body string) {
t.Helper()
path := filepath.Join(root, OlderNamesFile)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func digest(c string) string { return "sha256:" + strings.Repeat(c, 64) }
func TestAPinUnderAnOlderNameOfTheStoreIsWarnedOfWithItsCurrentName(t *testing.T) {
root := t.TempDir()
older(t, root, "# the store's older names\nold.example\nolder.example/ # with a slash\n")
manifest(t, root, "website", `{"module":"website","resources":[{"type":"container","image":"old.example/novox/www@`+digest("a")+`"}]}`)
manifest(t, root, "tool", `{"module":"tool","resources":[{"type":"file","content":"IMAGE=older.example/novox/t@`+digest("b")+`\n"}]}`)
manifest(t, root, "fine", `{"module":"fine","resources":[`+
`{"type":"container","image":"artifact-store://novox/www@`+digest("a")+`"},`+
`{"type":"container","image":"notold.example/novox/www@`+digest("c")+`"},`+
`{"type":"container","image":"old.example/novox/www:latest"},`+
`{"type":"container","image":"docker.io/library/nginx@`+digest("d")+`"}]}`)
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
if len(found) != 2 {
t.Fatalf("warned %+v", found)
}
if f := found[1]; f.Module != "website" || f.Pinned != "old.example/novox/www@"+digest("a") || f.Instead != "artifact-store://novox/www@"+digest("a") {
t.Errorf("website: %+v", f)
}
if f := found[0]; f.Module != "tool" || f.Instead != "artifact-store://novox/t@"+digest("b") {
t.Errorf("tool: %+v", f)
}
}
func TestWithoutOlderNamesNothingIsWarned(t *testing.T) {
root := t.TempDir()
older(t, root, "# none\n")
manifest(t, root, "website", `{"module":"website","resources":[{"type":"container","image":"old.example/novox/www@`+digest("a")+`"}]}`)
if found, err := Check(root); err != nil || len(found) != 0 {
t.Fatalf("warned %+v, %v", found, err)
}
}
func TestTheListOfOlderNamesMustBeThere(t *testing.T) {
if _, err := Check(t.TempDir()); err == nil {
t.Fatal("passed without its list of older names")
}
}
func TestAManifestThatCannotBeReadFails(t *testing.T) {
root := t.TempDir()
older(t, root, "")
manifest(t, root, "broken", `{`)
if _, err := Check(root); err == nil {
t.Fatal("a manifest that is not JSON was passed")
}
}
+4
View File
@@ -0,0 +1,4 @@
# The names the artifact store had before, one per line (novox/hq issue 323). A definition that pins an image
# by digest under one of these is warned of, with the store's current name. The same names are the mesh's
# setting older-store-names of artifact-store-tools, under which its tools read the definitions; change both.
registry-api.novox.be