169 lines
5.3 KiB
Go
169 lines
5.3 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
func TestImageDelete(t *testing.T) {
|
|
id := "sha256:" + strings.Repeat("a", 64)
|
|
for _, mode := range []string{"success", "anonymous", "non-admin", "invalid", "unmanaged", "role", "instance", "container-running", "container-exited", "containers-failure", "containers-null", "containers-unknown", "inspect-failure", "delete-failure", "delete-conflict", "queued", "running", "validating", "worker", "journal", "complete", "rolled_back", "db-failure", "task-query-failure", "audit-failure"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
var deletes atomic.Int32
|
|
a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodDelete:
|
|
deletes.Add(1)
|
|
if r.URL.Path != "/v1.47/images/"+id || r.URL.Query().Get("force") != "false" {
|
|
t.Errorf("unsafe delete: %s", r.URL)
|
|
}
|
|
if mode == "delete-failure" {
|
|
w.WriteHeader(500)
|
|
return
|
|
}
|
|
if mode == "delete-conflict" {
|
|
w.WriteHeader(409)
|
|
return
|
|
}
|
|
writeJSON(w, []any{})
|
|
case r.URL.Path == "/v1.47/images/"+id+"/json":
|
|
if mode == "inspect-failure" {
|
|
w.WriteHeader(500)
|
|
return
|
|
}
|
|
if mode == "role" {
|
|
writeJSON(w, map[string]any{"Id": id})
|
|
return
|
|
}
|
|
writeImageInspect(w, id, "1.2.3")
|
|
case r.URL.Path == "/v1.47/containers/json":
|
|
if r.URL.RawQuery != "all=1" {
|
|
t.Errorf("must inspect all containers: %s", r.URL)
|
|
}
|
|
if mode == "containers-failure" {
|
|
w.WriteHeader(500)
|
|
return
|
|
}
|
|
if mode == "containers-null" {
|
|
writeJSON(w, nil)
|
|
return
|
|
}
|
|
if mode == "containers-unknown" {
|
|
writeJSON(w, []any{map[string]string{"Image": "tag"}})
|
|
return
|
|
}
|
|
if strings.HasPrefix(mode, "container-") {
|
|
writeJSON(w, []any{map[string]string{"ImageID": id, "State": strings.TrimPrefix(mode, "container-")}})
|
|
return
|
|
}
|
|
writeJSON(w, []any{})
|
|
default:
|
|
t.Errorf("unexpected Docker request: %s %s", r.Method, r.URL)
|
|
w.WriteHeader(500)
|
|
}
|
|
})
|
|
exec := func(q string, args ...any) {
|
|
t.Helper()
|
|
if _, err := a.s.DB.Exec(q, args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
seedVerifiedImage(t, a, id, "1.2.3")
|
|
exec("INSERT INTO images(image_id,version,status,built_at) VALUES(?,'1.2.3','available','now')", id)
|
|
if mode != "non-admin" {
|
|
exec("UPDATE users SET admin=1 WHERE id='u'")
|
|
}
|
|
want := 409
|
|
switch mode {
|
|
case "success", "complete", "rolled_back":
|
|
want = 200
|
|
case "anonymous":
|
|
sid = ""
|
|
want = 401
|
|
case "non-admin":
|
|
want = 403
|
|
case "invalid":
|
|
want = 400
|
|
case "unmanaged":
|
|
exec("DELETE FROM images")
|
|
want = 404
|
|
case "inspect-failure", "containers-failure", "containers-null", "containers-unknown", "delete-failure":
|
|
want = 502
|
|
case "instance":
|
|
exec("INSERT INTO instances(id,uid,name,image_id,created) VALUES('i','other','i',?,0)", id)
|
|
case "queued", "running", "validating":
|
|
exec("INSERT INTO image_builds(id,version,status,created_at) VALUES('active','2.0.0',?,'now')", mode)
|
|
case "worker":
|
|
buildQueue.Lock()
|
|
buildQueue.active[a.s.DB] = true
|
|
buildQueue.Unlock()
|
|
defer func() { buildQueue.Lock(); delete(buildQueue.active, a.s.DB); buildQueue.Unlock() }()
|
|
case "db-failure":
|
|
exec("ALTER TABLE instances RENAME COLUMN image_id TO broken")
|
|
want = 500
|
|
case "task-query-failure":
|
|
exec("CREATE TABLE upgrade_journal(broken TEXT)")
|
|
want = 500
|
|
case "audit-failure":
|
|
exec("CREATE TRIGGER deny_audit BEFORE INSERT ON audit BEGIN SELECT RAISE(FAIL,'unavailable'); END")
|
|
want = 500
|
|
}
|
|
if mode == "journal" || mode == "complete" || mode == "rolled_back" {
|
|
exec("CREATE TABLE upgrade_journal(phase TEXT)")
|
|
exec("INSERT INTO upgrade_journal VALUES(?)", mode)
|
|
}
|
|
queryID := id
|
|
if mode == "invalid" {
|
|
queryID = "runtime:latest"
|
|
}
|
|
r := httptest.NewRequest(http.MethodDelete, "/api/images?image_id="+queryID, nil)
|
|
if sid != "" {
|
|
r.AddCookie(&http.Cookie{Name: "dsh_session", Value: sid})
|
|
addTestAccess(r, sid)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
a.images(w, r)
|
|
if w.Code != want {
|
|
t.Fatalf("status=%d want=%d body=%s", w.Code, want, w.Body.String())
|
|
}
|
|
wantDeletes := int32(0)
|
|
if want == 200 || mode == "delete-failure" || mode == "delete-conflict" {
|
|
wantDeletes = 1
|
|
}
|
|
if deletes.Load() != wantDeletes {
|
|
t.Fatalf("deletes=%d want=%d", deletes.Load(), wantDeletes)
|
|
}
|
|
if mode != "unmanaged" {
|
|
var status string
|
|
if err := a.s.DB.QueryRow("SELECT status FROM images WHERE image_id=?", id).Scan(&status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantStatus := "available"
|
|
if want == 200 {
|
|
wantStatus = "missing"
|
|
}
|
|
if status != wantStatus {
|
|
t.Fatalf("image status=%s want=%s", status, wantStatus)
|
|
}
|
|
}
|
|
var audits int
|
|
if err := a.s.DB.QueryRow("SELECT count(*) FROM audit WHERE action='image.delete' AND uid='u' AND target=?", id).Scan(&audits); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if (audits == 1) != (want == 200) {
|
|
t.Fatalf("audits=%d", audits)
|
|
}
|
|
var status string
|
|
if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE image_id=?", id).Scan(&status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if (status == "missing") != (want == 200) {
|
|
t.Fatalf("build status=%s", status)
|
|
}
|
|
})
|
|
}
|
|
}
|