package api import ( "context" "database/sql" "errors" "net/http" "net/url" "time" "git.misaka.ren/m1saka/dsh-web/internal/docker" ) func (a *API) deleteImage(w http.ResponseWriter, r *http.Request) { u, _, err := a.user(r) if err != nil { fail(w, 401, "authentication required") return } if !u.Admin { fail(w, 403, "administrator required") return } id := r.URL.Query().Get("image_id") if !runtimeImageID.MatchString(id) { fail(w, 400, "invalid image_id") return } // Upgrades also acquire a.mu before buildQueue. Keep both through deletion. a.mu.Lock() defer a.mu.Unlock() buildQueue.Lock() defer buildQueue.Unlock() if buildQueue.active[a.s.DB] { fail(w, 409, "image build worker is active") return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() tx, err := a.s.DB.BeginTx(ctx, nil) if err != nil { fail(w, 500, "database error") return } defer tx.Rollback() // Take SQLite's writer lock before checking intents: reservation does not use a.mu. if _, err = tx.ExecContext(ctx, "UPDATE images SET status=status WHERE image_id=?", id); err != nil { fail(w, 500, "image index unavailable") return } var version string err = tx.QueryRowContext(ctx, "SELECT version FROM images WHERE image_id=?", id).Scan(&version) if errors.Is(err, sql.ErrNoRows) { fail(w, 404, "managed image not found") return } if err != nil { fail(w, 500, "image index unavailable") return } for _, check := range []struct{ table, query string }{ {"image_builds", "SELECT count(*) FROM image_builds WHERE status IN ('queued','running','validating')"}, {"creation_intents", "SELECT count(*) FROM creation_intents WHERE status IN ('waiting','creating')"}, {"upgrade_journal", "SELECT count(*) FROM upgrade_journal WHERE phase NOT IN ('complete','rolled_back')"}, } { var exists, count int if err = tx.QueryRowContext(ctx, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", check.table).Scan(&exists); err == nil && exists > 0 { err = tx.QueryRowContext(ctx, check.query).Scan(&count) } if err != nil { fail(w, 500, "active task lookup failed") return } if count > 0 { fail(w, 409, "active task prevents image deletion") return } } var references int if err = tx.QueryRowContext(ctx, "SELECT count(*) FROM instances WHERE image_id=?", id).Scan(&references); err != nil { fail(w, 500, "instance reference lookup failed") return } if references > 0 { fail(w, 409, "image is referenced by an instance") return } var image struct { ID string `json:"Id"` Config struct{ Labels map[string]string } } path := "/images/" + url.PathEscape(id) if err = a.docker.Do(ctx, http.MethodGet, path+"/json", nil, &image); err != nil { fail(w, 502, "image inspection failed") return } if image.ID != id || image.Config.Labels["dsh-mgr.role"] != "runtime" || version == "" || image.Config.Labels["dsh-mgr.version"] != version { fail(w, 409, "managed runtime image required") return } var containers []struct{ ImageID string } if err = a.docker.Do(ctx, http.MethodGet, "/containers/json?all=1", nil, &containers); err != nil || containers == nil { fail(w, 502, "container reference lookup failed") return } for _, container := range containers { if container.ImageID == "" { fail(w, 502, "container image identity unavailable") return } if container.ImageID == id { fail(w, 409, "image is referenced by a container") return } } if _, err = tx.ExecContext(ctx, "UPDATE images SET status='missing',usage_count=0 WHERE image_id=?", id); err == nil { _, err = tx.ExecContext(ctx, "UPDATE image_builds SET status='missing',error='image deleted by administrator' WHERE image_id=? AND status='success'", id) } if err == nil { _, err = tx.ExecContext(ctx, "INSERT INTO audit(uid,action,target,detail,at) VALUES(?,'image.delete',?,'',?)", u.ID, id, time.Now().Unix()) } if err != nil { fail(w, 500, "image deletion recording failed") return } if err = a.docker.Do(ctx, http.MethodDelete, path+"?force=false", nil, nil); err != nil { status := http.StatusBadGateway var apiErr *docker.APIError if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict { status = http.StatusConflict } fail(w, status, "docker image deletion failed") return } if err = tx.Commit(); err != nil { fail(w, 500, "image deleted but recording failed") return } writeJSON(w, map[string]string{"image_id": id, "status": "missing"}) }