mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
216 lines
8.2 KiB
Go
216 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The machine's images, at 2026-10-04 12:00 (client's Now):
|
|
//
|
|
// img1 store/web line, used by the container mesh-web
|
|
// img2 postgres:16, used by the stopped container dev-db
|
|
// web-old store/web@sha256:old, older than img1: the previous, kept
|
|
// web-older store/web, oldest: removed
|
|
// app-new store/app@sha256:cur: declared, not running
|
|
// app-mid local build tag app-build:abc AND store/app@sha256:mid: one image, two names, one line: previous
|
|
// app-old app-build:old: same line through the local name: removed
|
|
// fresh other:1, two days old: younger than the floor
|
|
// gone retired:1, no line in use: removed
|
|
const imagesInspect = `[
|
|
{"Id":"sha256:img1","RepoTags":["store:5000/web/site:latest"],"RepoDigests":["store:5000/web/site@sha256:w1"],"Created":"2026-09-20T00:00:00Z","Size":100},
|
|
{"Id":"sha256:img2","RepoTags":["postgres:16"],"RepoDigests":["postgres@sha256:pg"],"Created":"2026-01-01T00:00:00Z","Size":200},
|
|
{"Id":"sha256:webold","RepoTags":[],"RepoDigests":["store:5000/web/site@sha256:w0"],"Created":"2026-09-10T00:00:00Z","Size":100},
|
|
{"Id":"sha256:webolder","RepoTags":[],"RepoDigests":["store:5000/web/site@sha256:wm"],"Created":"2026-09-01T00:00:00Z","Size":100},
|
|
{"Id":"sha256:appnew","RepoTags":[],"RepoDigests":["store:5000/app/server@sha256:cur"],"Created":"2026-09-25T00:00:00Z","Size":50},
|
|
{"Id":"sha256:appmid","RepoTags":["app-build:abc"],"RepoDigests":["store:5000/app/server@sha256:mid"],"Created":"2026-09-20T00:00:00Z","Size":50},
|
|
{"Id":"sha256:appold","RepoTags":["app-build:old"],"RepoDigests":[],"Created":"2026-09-01T00:00:00Z","Size":50},
|
|
{"Id":"sha256:fresh","RepoTags":["other:1"],"RepoDigests":[],"Created":"2026-10-02T00:00:00Z","Size":10},
|
|
{"Id":"sha256:gone","RepoTags":["retired:1"],"RepoDigests":[],"Created":"2026-08-01T00:00:00Z","Size":70}
|
|
]`
|
|
|
|
const ids = "sha256:img1\nsha256:img2\nsha256:webold\nsha256:webolder\nsha256:appnew\nsha256:appmid\nsha256:appold\nsha256:fresh\nsha256:gone\n"
|
|
|
|
func imagesMachine() *fake {
|
|
f := machine().
|
|
on("docker image ls --quiet --no-trunc", Ran{Stdout: ids}).
|
|
on("docker image inspect", Ran{Stdout: imagesInspect}).
|
|
on("docker image rm", Ran{Stdout: "Deleted"})
|
|
return f
|
|
}
|
|
|
|
func declaring(t *testing.T, answer string) Asker {
|
|
return func(key string, body any) (json.RawMessage, error) {
|
|
if key != "seat:mesh-artifact-sweep.images" {
|
|
t.Errorf("asked %s", key)
|
|
}
|
|
if b, _ := body.(map[string]any); b["node"] != "laptop" {
|
|
t.Errorf("asked for %v", body)
|
|
}
|
|
wrapped, _ := json.Marshal(map[string]any{"ok": true, "output": "", "answer": json.RawMessage(answer)})
|
|
return wrapped, nil
|
|
}
|
|
}
|
|
|
|
const declaredApp = `{"node":"laptop","sent_known":true,"images":[
|
|
{"image":"store:5000/app/server@sha256:cur","resources":["app.server"],"in":["declaration"]},
|
|
{"image":"docker.io/library/postgres@sha256:pg","resources":["db.server"],"in":["sent"]}]}`
|
|
|
|
func pruned(t *testing.T, out map[string]any) map[string]*PrunedImage {
|
|
t.Helper()
|
|
got := map[string]*PrunedImage{}
|
|
for _, img := range out["images"].([]*PrunedImage) {
|
|
got[img.ID] = img
|
|
}
|
|
return got
|
|
}
|
|
|
|
func has(why []string, w string) bool {
|
|
for _, x := range why {
|
|
if x == w {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestImagesAreKeptForEachReasonAndTheRestAreCandidates(t *testing.T) {
|
|
f := imagesMachine()
|
|
c := client(f, 1000)
|
|
c.Node, c.Ask = "laptop", declaring(t, declaredApp)
|
|
out, err := c.PruneImages(context.Background(), PruneImagesAsk{DryRun: true, OlderThanDays: 7, Limit: 100})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := pruned(t, out)
|
|
want := map[string]string{
|
|
"img1": keptUsed, "img2": keptUsed, "webold": keptPrevious, "appnew": keptDeclared,
|
|
"appmid": keptPrevious, "fresh": keptYoung,
|
|
}
|
|
for id, w := range want {
|
|
if !got[id].Kept || !has(got[id].Why, w) {
|
|
t.Errorf("%s: kept %v why %v, want %s", id, got[id].Kept, got[id].Why, w)
|
|
}
|
|
}
|
|
if !has(got["img2"].Why, keptDeclared) {
|
|
t.Errorf("postgres named as docker.io/library/postgres@… was not matched: %v", got["img2"].Why)
|
|
}
|
|
for _, id := range []string{"webolder", "appold", "gone"} {
|
|
if got[id].Kept {
|
|
t.Errorf("%s kept: %v", id, got[id].Why)
|
|
}
|
|
}
|
|
if got["appmid"].Line != got["appold"].Line || got["appmid"].Line != got["appnew"].Line {
|
|
t.Errorf("an image with two names did not join its lines: %q %q %q", got["appmid"].Line, got["appold"].Line, got["appnew"].Line)
|
|
}
|
|
if out["bytes_candidate"].(int64) != 220 {
|
|
t.Errorf("bytes %v", out["bytes_candidate"])
|
|
}
|
|
if f.ran("docker image rm") {
|
|
t.Fatal("a dry run removed an image")
|
|
}
|
|
}
|
|
|
|
func TestARealRunRemovesByNameNeverForced(t *testing.T) {
|
|
f := imagesMachine()
|
|
c := client(f, 1000)
|
|
c.Node, c.Ask = "laptop", declaring(t, declaredApp)
|
|
out, err := c.PruneImages(context.Background(), PruneImagesAsk{DryRun: false, Why: "disk", OlderThanDays: 7, Limit: 100})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(out["removed"].([]string)); n != 3 {
|
|
t.Errorf("removed %v", out["removed"])
|
|
}
|
|
for _, call := range f.calls {
|
|
line := strings.Join(call.args, " ")
|
|
if strings.HasPrefix(line, "image rm") {
|
|
if strings.Contains(line, "-f") || strings.Contains(line, "--force") {
|
|
t.Errorf("forced: %s", line)
|
|
}
|
|
if strings.Contains(line, "sha256:img1") || strings.Contains(line, "web/site@sha256:w0") {
|
|
t.Errorf("removed a kept image: %s", line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestARefusalIsReportedAndTheRestGoOn(t *testing.T) {
|
|
f := machine().
|
|
on("docker image ls --quiet --no-trunc", Ran{Stdout: ids}).
|
|
on("docker image inspect", Ran{Stdout: imagesInspect}).
|
|
on("docker image rm retired:1", Ran{Status: 1, Stderr: "conflict: unable to remove repository reference"}).
|
|
on("docker image rm", Ran{Stdout: "Deleted"})
|
|
c := client(f, 1000)
|
|
c.Node, c.Ask = "laptop", declaring(t, declaredApp)
|
|
out, err := c.PruneImages(context.Background(), PruneImagesAsk{DryRun: false, Why: "disk", OlderThanDays: 7})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out["refused"].([]map[string]string)) != 1 || len(out["removed"].([]string)) != 2 {
|
|
t.Errorf("removed %v refused %v", out["removed"], out["refused"])
|
|
}
|
|
}
|
|
|
|
func TestNoAnswerFromTheControllerRemovesNothing(t *testing.T) {
|
|
for name, c := range map[string]*Client{
|
|
"error": {Node: "laptop", Ask: func(string, any) (json.RawMessage, error) { return nil, errors.New("no responders") }},
|
|
"refused": {Node: "laptop", Ask: func(string, any) (json.RawMessage, error) {
|
|
return json.RawMessage(`{"ok":false,"output":"no such machine"}`), nil
|
|
}},
|
|
"no node": {Ask: declaring(t, declaredApp)},
|
|
} {
|
|
f := imagesMachine()
|
|
cl := client(f, 1000)
|
|
cl.Node, cl.Ask = c.Node, c.Ask
|
|
out, err := cl.PruneImages(context.Background(), PruneImagesAsk{DryRun: false, Why: "disk", OlderThanDays: 7})
|
|
if err != nil {
|
|
t.Fatal(name, err)
|
|
}
|
|
if f.ran("docker image rm") {
|
|
t.Errorf("%s: removed without knowing the declaration", name)
|
|
}
|
|
if out["declaration_known"] != false || out["counts"].(map[string]int)["candidates"] != 0 {
|
|
t.Errorf("%s: %v", name, out["counts"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestARealRunWithoutWhyIsRefused(t *testing.T) {
|
|
f := imagesMachine()
|
|
c := client(f, 1000)
|
|
c.Node, c.Ask = "laptop", declaring(t, declaredApp)
|
|
if _, err := c.PruneImages(context.Background(), PruneImagesAsk{DryRun: false, OlderThanDays: 7}); err == nil {
|
|
t.Fatal("a real run without why was accepted")
|
|
}
|
|
if len(f.calls) != 0 {
|
|
t.Fatal("something was asked of the runtime before refusing")
|
|
}
|
|
for _, tool := range tools(c) {
|
|
if tool.Name == "docker_prune_images" {
|
|
if _, err := tool.Run(map[string]any{"dry_run": false}); err == nil {
|
|
t.Fatal("the tool accepted a real run without why")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReferencesAreNormalisedAsTheRuntimeReportsThem(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"docker.io/library/redis@sha256:x": "redis@sha256:x",
|
|
"redis": "redis:latest",
|
|
"store:5000/a/b": "store:5000/a/b:latest",
|
|
"store:5000/a/b:1": "store:5000/a/b:1",
|
|
"ghcr.io/x/y@sha256:z": "ghcr.io/x/y@sha256:z",
|
|
} {
|
|
if got := normalRef(in); got != want {
|
|
t.Errorf("%s: %s, want %s", in, got, want)
|
|
}
|
|
}
|
|
if repositoryOf("store:5000/a/b:1") != "store:5000/a/b" || repositoryOf("store:5000/a/b@sha256:z") != "store:5000/a/b" {
|
|
t.Error("repository")
|
|
}
|
|
}
|