Files
dsh-web/internal/api/images_delete.go
T

141 lines
4.4 KiB
Go

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"})
}