288 lines
10 KiB
Go
288 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.misaka.ren/m1saka/dsh-web/internal/store"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHistoricalExperimentalBuildRequiresAcceptance(t *testing.T) {
|
|
id := "sha256:" + strings.Repeat("d", 64)
|
|
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v1.47/images/json":
|
|
writeJSON(w, []any{map[string]any{"Id": id, "Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "9.8.7", "dsh-mgr.recipe": imageRecipeRev}}})
|
|
case "/v1.47/images/" + id + "/json":
|
|
writeImageInspect(w, id, "9.8.7")
|
|
case "/v1.47/containers/create":
|
|
writeJSON(w, map[string]string{"Id": "check"})
|
|
case "/v1.47/containers/check/exec":
|
|
writeJSON(w, map[string]string{"Id": "probe"})
|
|
case "/v1.47/exec/probe/json":
|
|
writeJSON(w, map[string]any{"Running": false, "ExitCode": 0})
|
|
case "/v1.47/containers/check/json":
|
|
writeJSON(w, map[string]any{"State": map[string]bool{"Running": true}})
|
|
default:
|
|
w.WriteHeader(204)
|
|
}
|
|
})
|
|
if _, err := a.s.DB.Exec("UPDATE users SET admin=1 WHERE id='u'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seedVerifiedImage(t, a, id, "9.8.7")
|
|
if _, err := a.s.DB.Exec("DELETE FROM image_admissions; UPDATE image_builds SET experimental=1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
im, err := a.evaluateImage(context.Background(), id)
|
|
if err != nil || im.Selectable || im.Status != "experimental" {
|
|
t.Fatal(im, err)
|
|
}
|
|
}
|
|
|
|
func TestLegacyQueuedBuildFailsBeforeDocker(t *testing.T) {
|
|
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unsupported build contacted Docker") })
|
|
buildQueue.Lock()
|
|
err := a.ensureBuilds()
|
|
buildQueue.Unlock()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
previous := imagePlatform
|
|
imagePlatform = "linux/arm64"
|
|
defer func() { imagePlatform = previous }()
|
|
b := buildRecord{ID: "unsupported", Version: "0.2.0-rc.2", Status: "queued", RecipeRev: imageRecipeRev, Platform: imagePlatform}
|
|
if err := a.insertBuildRequest(b, "u"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a.runImageBuild(b)
|
|
var state, detail string
|
|
if err := a.s.DB.QueryRow("SELECT status,error FROM image_builds WHERE id=?", b.ID).Scan(&state, &detail); err != nil || state != "failed" || !strings.Contains(detail, "Git release identity required") {
|
|
t.Fatal(state, detail, err)
|
|
}
|
|
}
|
|
|
|
func TestAdmissionDowngradeWaitsForCreate(t *testing.T) {
|
|
id := "sha256:" + strings.Repeat("e", 64)
|
|
entered, release := make(chan struct{}), make(chan struct{})
|
|
a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/images/"):
|
|
writeImageInspect(w, id, "0.2.0-rc.1")
|
|
case strings.HasSuffix(r.URL.Path, "/networks/create"):
|
|
close(entered)
|
|
<-release
|
|
w.WriteHeader(204)
|
|
case strings.HasSuffix(r.URL.Path, "/containers/create"):
|
|
writeJSON(w, map[string]string{"Id": "created"})
|
|
default:
|
|
w.WriteHeader(204)
|
|
}
|
|
})
|
|
seedVerifiedImage(t, a, id, "0.2.0-rc.1")
|
|
if _, err := a.s.DB.Exec("UPDATE users SET admin=1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
created := make(chan int, 1)
|
|
go func() {
|
|
w := httptest.NewRecorder()
|
|
a.instances(w, runtimeRequest("POST", "/api/instances", fmt.Sprintf(`{"name":"race","image_id":%q}`, id), sid))
|
|
created <- w.Code
|
|
}()
|
|
select {
|
|
case <-entered:
|
|
case <-time.After(3 * time.Second):
|
|
close(release)
|
|
t.Fatal("create did not reach network")
|
|
}
|
|
downgraded := make(chan int, 1)
|
|
go func() {
|
|
w := httptest.NewRecorder()
|
|
a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":"blocked","evidence":"downgrade test"}`, id), sid))
|
|
downgraded <- w.Code
|
|
}()
|
|
select {
|
|
case status := <-downgraded:
|
|
close(release)
|
|
t.Fatalf("admission raced active create: %d", status)
|
|
case <-time.After(30 * time.Millisecond):
|
|
}
|
|
close(release)
|
|
if status := <-created; status != 200 {
|
|
t.Fatal(status)
|
|
}
|
|
if status := <-downgraded; status != 200 {
|
|
t.Fatal(status)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
a.instances(w, runtimeRequest("POST", "/api/instances", fmt.Sprintf(`{"name":"denied","image_id":%q}`, id), sid))
|
|
if w.Code != 400 {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdmissionRejectsLegacyLayouts(t *testing.T) {
|
|
for _, recipe := range []string{"", "1", "2", "3", "4", "5", "6", "8"} {
|
|
t.Run(recipe, func(t *testing.T) {
|
|
id := "sha256:" + strings.Repeat("a", 64)
|
|
a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.URL.Path, "/images/") {
|
|
t.Error("unexpected Docker operation", r.Method, r.URL.Path)
|
|
}
|
|
writeJSON(w, map[string]any{"Id": id, "Os": "linux", "Architecture": "amd64", "Config": map[string]any{"Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "1.2.3", "dsh-mgr.recipe": recipe}}})
|
|
})
|
|
seedVerifiedImage(t, a, id, "1.2.3")
|
|
for _, query := range []string{"UPDATE image_builds SET recipe_rev=?", "UPDATE image_admissions SET recipe_rev=?"} {
|
|
if _, err := a.s.DB.Exec(query, recipe); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := a.s.DB.Exec("UPDATE users SET admin=1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
im, err := a.evaluateImage(context.Background(), id)
|
|
if err != nil || im.Selectable || im.Admission != "existing_only" {
|
|
t.Fatal(im, err)
|
|
}
|
|
for _, state := range []string{"compatible", "recommended"} {
|
|
w := httptest.NewRecorder()
|
|
a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":%q,"evidence":"historical acceptance"}`, id, state), sid))
|
|
if w.Code != 400 {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdmissionCurrentCreateAndBlock(t *testing.T) {
|
|
id := "sha256:" + strings.Repeat("a", 64)
|
|
var body map[string]any
|
|
a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/images/"):
|
|
writeJSON(w, map[string]any{"Id": id, "Os": "linux", "Architecture": "amd64", "Config": map[string]any{"Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "1.2.3", "dsh-mgr.recipe": imageRecipeRev}}})
|
|
case strings.HasSuffix(r.URL.Path, "/containers/create"):
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Error(err)
|
|
}
|
|
writeJSON(w, map[string]string{"Id": "created"})
|
|
default:
|
|
w.WriteHeader(204)
|
|
}
|
|
})
|
|
seedVerifiedImage(t, a, id, "1.2.3")
|
|
if _, err := a.s.DB.Exec("DELETE FROM image_admissions; UPDATE users SET admin=1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
im, err := a.evaluateImage(context.Background(), id)
|
|
if err != nil || im.Status != "existing_only" || im.Selectable {
|
|
t.Fatal(im, err)
|
|
}
|
|
records := []buildRecord{{ID: "unused", ImageID: id, Version: "1.2.3", RecipeRev: imageRecipeRev, Platform: imagePlatform, Status: "success"}}
|
|
if err := a.reconcileBuilds(context.Background(), records); err != nil || records[0].Status != "success" {
|
|
t.Fatal(records, err)
|
|
}
|
|
admit := func(state string) *httptest.ResponseRecorder {
|
|
w := httptest.NewRecorder()
|
|
a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":%q,"evidence":"current layout explicitly accepted"}`, id, state), sid))
|
|
return w
|
|
}
|
|
if w := admit("compatible"); w.Code != 200 {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
w := httptest.NewRecorder()
|
|
a.instances(w, runtimeRequest("POST", "/api/instances", fmt.Sprintf(`{"name":"current","image_id":%q}`, id), sid))
|
|
if w.Code != 200 {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
host := body["HostConfig"].(map[string]any)
|
|
mounts := host["Mounts"].([]any)
|
|
if len(mounts) != 1 || mounts[0].(map[string]any)["Target"] != "/workspace" || !strings.Contains(fmt.Sprint(body["Env"]), "DSH_HOME=/workspace") {
|
|
t.Fatal(body)
|
|
}
|
|
var instance store.Instance
|
|
if err := json.Unmarshal(w.Body.Bytes(), &instance); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w := admit("blocked"); w.Code != 200 {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
for _, action := range []string{"restore", "backups", "profile"} {
|
|
w := httptest.NewRecorder()
|
|
a.config(w, runtimeRequest("POST", "/api/instances/"+instance.ID+"/"+action, `{}`, sid), instance, action)
|
|
if w.Code != 409 {
|
|
t.Fatal(action, w.Code, w.Body.String())
|
|
}
|
|
}
|
|
for _, action := range []string{"start", "restart"} {
|
|
w := httptest.NewRecorder()
|
|
a.instance(w, runtimeRequest("POST", "/api/instances/"+instance.ID+"/"+action, "", sid))
|
|
if w.Code != 409 {
|
|
t.Fatal(action, w.Code, w.Body.String())
|
|
}
|
|
}
|
|
if _, err := a.s.DB.Exec("CREATE TRIGGER audit_fail BEFORE INSERT ON audit BEGIN SELECT RAISE(FAIL,'audit unavailable'); END"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w := admit("compatible"); w.Code != 500 {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
im, err = a.evaluateImage(context.Background(), id)
|
|
if err != nil || im.Admission != "blocked" {
|
|
t.Fatal(im, err)
|
|
}
|
|
}
|
|
|
|
func TestAdmissionExperimentalAndPreflight(t *testing.T) {
|
|
id := "sha256:" + strings.Repeat("b", 64)
|
|
a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) { writeImageInspect(w, id, "9.8.7") })
|
|
seedVerifiedImage(t, a, id, "9.8.7")
|
|
if _, err := a.s.DB.Exec("DELETE FROM image_admissions; UPDATE image_builds SET experimental=1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
im, err := a.evaluateImage(context.Background(), id)
|
|
if err != nil || im.Status != "experimental" || im.Selectable {
|
|
t.Fatal(im, err)
|
|
}
|
|
for _, tc := range []struct {
|
|
handler http.HandlerFunc
|
|
body string
|
|
want int
|
|
}{{a.builds, `{"version":"9.8.7"}`, 405}, {a.builds, `{"version":"9.8.7","experimental":true}`, 405}, {a.instances, fmt.Sprintf(`{"name":"unknown","image_id":%q}`, id), 400}} {
|
|
w := httptest.NewRecorder()
|
|
tc.handler(w, runtimeRequest("POST", "/api/test", tc.body, sid))
|
|
if w.Code != tc.want {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
}
|
|
var n int
|
|
if err := a.s.DB.QueryRow("SELECT count(*) FROM image_build_requests").Scan(&n); err != nil || n != 0 {
|
|
t.Fatal(n, err)
|
|
}
|
|
if _, err := a.s.DB.Exec("UPDATE users SET admin=1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":"compatible","evidence":"accepted after startup validation"}`, id), sid))
|
|
if w.Code != 200 {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
im, err = a.evaluateImage(context.Background(), id)
|
|
if err != nil || !im.Selectable {
|
|
t.Fatal(im, err)
|
|
}
|
|
if _, err := a.s.DB.Exec("UPDATE image_builds SET status='failed'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
im, err = a.evaluateImage(context.Background(), id)
|
|
if err != nil || im.Selectable || im.Status != "unverified" {
|
|
t.Fatal(im, err)
|
|
}
|
|
}
|