fix: prune missing image indexes and align instance creation payload

This commit is contained in:
2026-10-14 19:37:52 +08:00
parent 15910a086f
commit 3bf197206a
6 changed files with 125 additions and 24 deletions
@@ -93,22 +93,19 @@ func TestImageIndexDoesNotUndoConcurrentDelete(t *testing.T) {
if im.ImageID == id {
matches++
}
if im.ImageID == id && im.Status != "missing" {
t.Errorf("returned deleted image as %s", im.Status)
}
}
if matches != 1 {
t.Errorf("returned %d records for deleted image, want 1", matches)
if matches != 0 {
t.Errorf("returned %d records for deleted image, want 0", matches)
}
case <-ctx.Done():
t.Fatal("index did not finish")
}
var status string
if err := a.s.DB.QueryRow("SELECT status FROM images WHERE image_id=?", id).Scan(&status); err != nil {
var count int
if err := a.s.DB.QueryRow("SELECT count(*) FROM images WHERE image_id=?", id).Scan(&count); err != nil {
t.Fatal(err)
}
if status != "missing" {
t.Errorf("persisted deleted image as %s", status)
if count != 0 {
t.Errorf("persisted %d rows for deleted image", count)
}
})
}
+15 -6
View File
@@ -92,7 +92,7 @@ func (a *API) imageIndexSnapshot(ctx context.Context) ([]imageRecord, error) {
err := a.ensureBuilds()
if err == nil {
_, err = a.s.DB.ExecContext(ctx, `INSERT OR IGNORE INTO images(image_id,version,recipe_rev,platform,tag,status,built_at)
SELECT image_id,version,recipe_rev,platform,'','missing',COALESCE(finished_at,created_at) FROM image_builds WHERE status IN ('success','missing') AND image_id<>'' ORDER BY created_at DESC,id DESC`)
SELECT image_id,version,recipe_rev,platform,'','missing',COALESCE(finished_at,created_at) FROM image_builds WHERE status='success' AND image_id<>'' ORDER BY created_at DESC,id DESC`)
}
var rows *sql.Rows
if err == nil {
@@ -184,18 +184,27 @@ func (a *API) imageIndexSnapshot(ctx context.Context) ([]imageRecord, error) {
return nil, err
}
defer tx.Rollback()
present := make([]imageRecord, 0, len(result))
for _, im := range result {
if _, err = tx.ExecContext(ctx, `INSERT INTO images(image_id,version,recipe_rev,platform,tag,status,built_at,usage_count) VALUES(?,?,?,?,?,?,?,?)
ON CONFLICT(image_id) DO UPDATE SET version=excluded.version,recipe_rev=excluded.recipe_rev,platform=excluded.platform,tag=excluded.tag,status=excluded.status,built_at=excluded.built_at,usage_count=excluded.usage_count`, im.ImageID, im.Version, im.RecipeRev, im.Platform, im.Tag, im.Status, im.Created, im.UsageCount); err != nil {
return nil, err
}
if im.Status == "missing" {
if _, err = tx.ExecContext(ctx, "UPDATE image_builds SET status='missing',error='image no longer available or template identity is unknown' WHERE image_id=? AND status='success'", im.ImageID); err != nil {
return nil, err
}
if _, err = tx.ExecContext(ctx, "DELETE FROM images WHERE image_id=?", im.ImageID); err != nil {
return nil, err
}
continue
}
if _, err = tx.ExecContext(ctx, `INSERT INTO images(image_id,version,recipe_rev,platform,tag,status,built_at,usage_count) VALUES(?,?,?,?,?,?,?,?)
ON CONFLICT(image_id) DO UPDATE SET version=excluded.version,recipe_rev=excluded.recipe_rev,platform=excluded.platform,tag=excluded.tag,status=excluded.status,built_at=excluded.built_at,usage_count=excluded.usage_count`, im.ImageID, im.Version, im.RecipeRev, im.Platform, im.Tag, im.Status, im.Created, im.UsageCount); err != nil {
return nil, err
}
present = append(present, im)
}
return result, tx.Commit()
if err = tx.Commit(); err != nil {
return nil, err
}
return present, nil
}
func (a *API) StartImageBuilds() error {
+100 -5
View File
@@ -173,7 +173,7 @@ func TestImageIndexLocalInventory(t *testing.T) {
}
deleted.Store(true)
w = imageRequest(a.images, sid, "GET", "")
if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil || w.Code != 200 || len(images) != 1 || images[0].Status != "missing" {
if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil || w.Code != 200 || len(images) != 0 {
t.Fatal(w.Body.String(), err)
}
})
@@ -226,17 +226,112 @@ func TestImageIndexUsageMissingAndDockerFailure(t *testing.T) {
t.Fatal(status, err)
}
mode.Store(2)
check("missing", 2)
if _, err := a.s.DB.Exec("DELETE FROM instances WHERE id='two'"); err != nil {
t.Fatal(err)
for refresh := 0; refresh < 3; refresh++ {
w := imageRequest(a.images, sid, "GET", "")
if w.Code != 200 || strings.TrimSpace(w.Body.String()) != "[]" {
t.Fatal(w.Code, w.Body.String())
}
var count int
if err := a.s.DB.QueryRow("SELECT count(*) FROM images").Scan(&count); err != nil || count != 0 {
t.Fatal(count, err)
}
}
var count int
if err := a.s.DB.QueryRow("SELECT count(*) FROM instances WHERE image_id='image'").Scan(&count); err != nil || count != 2 {
t.Fatal(count, err)
}
if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE image_id='image'").Scan(&status); err != nil || status != "missing" {
t.Fatal(status, err)
}
check("missing", 1)
verified, err := a.verifiedImages(context.Background())
if err != nil || len(verified) != 0 {
t.Fatal(verified, err)
}
}
func TestImageIndexFailurePreservesRows(t *testing.T) {
for _, mode := range []string{"list", "inspect", "rollback"} {
t.Run(mode, func(t *testing.T) {
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1.47/images/json" {
if mode == "list" {
w.WriteHeader(500)
} else {
writeJSON(w, []any{})
}
return
}
if mode == "inspect" && strings.Contains(r.URL.Path, "second") {
w.WriteHeader(500)
return
}
w.WriteHeader(404)
})
seedVerifiedImage(t, a, "first", "1.0.0")
seedVerifiedImage(t, a, "second", "2.0.0")
if _, err := a.s.DB.Exec("INSERT INTO images(image_id,version,status,built_at) VALUES('first','1.0.0','available','2'),('second','2.0.0','available','1')"); err != nil {
t.Fatal(err)
}
if mode == "rollback" {
if _, err := a.s.DB.Exec("CREATE TRIGGER deny_second_delete BEFORE DELETE ON images WHEN OLD.image_id='second' BEGIN SELECT RAISE(FAIL,'unavailable'); END"); err != nil {
t.Fatal(err)
}
}
if images, err := a.imageIndex(context.Background()); err == nil || images != nil {
t.Fatalf("images=%v err=%v", images, err)
}
for _, query := range []string{"SELECT count(*) FROM images WHERE status='available'", "SELECT count(*) FROM image_builds WHERE status='success'"} {
var count int
if err := a.s.DB.QueryRow(query).Scan(&count); err != nil || count != 2 {
t.Fatal(query, count, err)
}
}
})
}
}
func TestImageIndexMissingHistoryAndBlockedReimport(t *testing.T) {
var present atomic.Bool
var inspections atomic.Int32
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1.47/images/json" {
if present.Load() {
writeJSON(w, []any{map[string]any{"Id": "image", "Labels": map[string]string{"dsh-mgr.role": "runtime"}}})
} else {
writeJSON(w, []any{})
}
return
}
inspections.Add(1)
if !present.Load() {
w.WriteHeader(404)
return
}
writeImageInspect(w, "image", "0.2.0-rc.1")
})
seedVerifiedImage(t, a, "image", "0.2.0-rc.1")
if _, err := a.s.DB.Exec("UPDATE image_admissions SET state='blocked',evidence='unsafe'"); err != nil {
t.Fatal(err)
}
for refresh := 0; refresh < 3; refresh++ {
if images, err := a.imageIndex(context.Background()); err != nil || len(images) != 0 {
t.Fatal(images, err)
}
}
if inspections.Load() != 1 {
t.Fatalf("missing history caused %d inspections", inspections.Load())
}
present.Store(true)
images, err := a.imageIndex(context.Background())
if err != nil || len(images) != 1 || images[0].Status != "blocked" || images[0].Selectable || images[0].AdmissionReason != "unsafe" {
t.Fatal(images, err)
}
var status string
if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE image_id='image'").Scan(&status); err != nil || status != "missing" {
t.Fatal(status, err)
}
}
func TestBuildDiskPolicy(t *testing.T) {
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unexpected build") })
if err := a.checkBuildDisk(context.Background()); err != nil {