daemon: agent-driven workspace images — repo→image registry (GET/PUT/DELETE /api/repos), spawn resolves custom image, self-healing lvmh-ops control container with docker.sock; 132 tests, e2e 87/87
This commit is contained in:
@@ -426,3 +426,137 @@ func TestAPIWebHandlerEmbedded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIRepoImagesCRUD(t *testing.T) {
|
||||
ts, store := newTestServer(t)
|
||||
auth := testToken
|
||||
|
||||
// auth required on the new namespace
|
||||
if code, _ := apiReq(t, http.MethodGet, ts.URL+"/api/repos", "", ""); code != http.StatusUnauthorized {
|
||||
t.Fatal("GET /api/repos must require auth")
|
||||
}
|
||||
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/g/p/image", "", `{"image":"lvmh-worker-x"}`); code != http.StatusUnauthorized {
|
||||
t.Fatal("PUT image must require auth")
|
||||
}
|
||||
|
||||
// empty listing
|
||||
code, body := apiReq(t, http.MethodGet, ts.URL+"/api/repos", auth, "")
|
||||
if code != http.StatusOK || body != "[]\n" {
|
||||
t.Fatalf("empty repos = %d %q, want 200 []", code, body)
|
||||
}
|
||||
|
||||
// valid registration
|
||||
code, body = apiReq(t, http.MethodPut, ts.URL+"/api/repos/group/project/image", auth, `{"image":"lvmh-worker-group--project-ab12cd"}`)
|
||||
if code != http.StatusOK || !strings.Contains(body, `"ok":true`) {
|
||||
t.Fatalf("put image = %d %s", code, body)
|
||||
}
|
||||
// a second repo, then listing is sorted by repo
|
||||
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/alpha/repo/image", auth, `{"image":"lvmh-worker-alpha:1.2.3"}`); code != http.StatusOK {
|
||||
t.Fatalf("put tagged image = %d", code)
|
||||
}
|
||||
code, body = apiReq(t, http.MethodGet, ts.URL+"/api/repos", auth, "")
|
||||
if code != http.StatusOK || body != `[{"repo":"alpha/repo","image":"lvmh-worker-alpha:1.2.3"},{"repo":"group/project","image":"lvmh-worker-group--project-ab12cd"}]`+"\n" {
|
||||
t.Fatalf("repos = %d %s", code, body)
|
||||
}
|
||||
|
||||
// upsert via PUT
|
||||
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/group/project/image", auth, `{"image":"lvmh-worker-other"}`); code != http.StatusOK {
|
||||
t.Fatalf("upsert = %d", code)
|
||||
}
|
||||
if img, ok, _ := store.GetRepoImage("group/project"); !ok || img != "lvmh-worker-other" {
|
||||
t.Fatalf("after upsert = %q %v", img, ok)
|
||||
}
|
||||
|
||||
// delete → gone; idempotent delete
|
||||
if code, _ := apiReq(t, http.MethodDelete, ts.URL+"/api/repos/group/project/image", auth, ""); code != http.StatusOK {
|
||||
t.Fatal("delete image failed")
|
||||
}
|
||||
if code, _ := apiReq(t, http.MethodDelete, ts.URL+"/api/repos/group/project/image", auth, ""); code != http.StatusOK {
|
||||
t.Fatal("delete absent image must stay 200")
|
||||
}
|
||||
code, body = apiReq(t, http.MethodGet, ts.URL+"/api/repos", auth, "")
|
||||
if code != http.StatusOK || strings.Contains(body, "group/project") {
|
||||
t.Fatalf("after delete = %d %s", code, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIRepoImageValidation(t *testing.T) {
|
||||
ts, _ := newTestServer(t)
|
||||
auth := testToken
|
||||
|
||||
valid := []string{
|
||||
"lvmh-worker-x",
|
||||
"lvmh-worker-group--project-ab12cd",
|
||||
"lvmh-worker-x:latest",
|
||||
"lvmh-worker-x:1.2.3",
|
||||
"lvmh-worker-x-y.z:tag-1_2",
|
||||
}
|
||||
for _, img := range valid {
|
||||
if code, body := apiReq(t, http.MethodPut, ts.URL+"/api/repos/g/p/image", auth, `{"image":"`+img+`"}`); code != http.StatusOK {
|
||||
t.Fatalf("image %q = %d %s, want 200", img, code, body)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"", // empty
|
||||
"lvmh-worker-", // no name after the prefix
|
||||
"lvmh-worker-:tag", // tag without a name
|
||||
"lvmh-worker-X", // uppercase name part
|
||||
"evil", // foreign namespace
|
||||
"lvmh-worker-x;rm -rf /", // shell metachars
|
||||
"lvmh-worker-x/y", // path separator
|
||||
"lvmh-worker-x:tag with space", // space in tag
|
||||
}
|
||||
for _, img := range invalid {
|
||||
code, body := apiReq(t, http.MethodPut, ts.URL+"/api/repos/g/p/image", auth, `{"image":"`+img+`"}`)
|
||||
if code != http.StatusBadRequest || !strings.Contains(body, "invalid image name") {
|
||||
t.Fatalf("image %q = %d %s, want 400 invalid image name", img, code, body)
|
||||
}
|
||||
}
|
||||
|
||||
// bad repo shapes (single-slash-safe: ServeMux collapses "//" via
|
||||
// redirect before our handler sees the path)
|
||||
for _, repo := range []string{
|
||||
"noslash",
|
||||
"group/proj!bad",
|
||||
} {
|
||||
code, body := apiReq(t, http.MethodPut, ts.URL+"/api/repos/"+repo+"/image", auth, `{"image":"lvmh-worker-x"}`)
|
||||
if code != http.StatusBadRequest {
|
||||
t.Fatalf("repo %q = %d %s, want 400", repo, code, body)
|
||||
}
|
||||
code, _ = apiReq(t, http.MethodDelete, ts.URL+"/api/repos/"+repo+"/image", auth, "")
|
||||
if code != http.StatusBadRequest {
|
||||
t.Fatalf("delete repo %q = %d, want 400", repo, code)
|
||||
}
|
||||
}
|
||||
|
||||
// not an image path at all
|
||||
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/g/p/notimage", auth, `{"image":"lvmh-worker-x"}`); code != http.StatusBadRequest {
|
||||
t.Fatal("non-image path under /api/repos must 400")
|
||||
}
|
||||
// malformed body
|
||||
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/g/p/image", auth, `not json`); code != http.StatusBadRequest {
|
||||
t.Fatal("bad body must 400")
|
||||
}
|
||||
// oversized body
|
||||
big := `{"image":"` + strings.Repeat("x", 1<<20) + `"}`
|
||||
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/g/p/image", auth, big); code != http.StatusBadRequest {
|
||||
t.Fatal("oversized body must 400")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIRepoImagesStoreFailure(t *testing.T) {
|
||||
ts, store := newTestServer(t)
|
||||
if err := store.Close(); err != nil {
|
||||
t.Fatalf("close store: %v", err)
|
||||
}
|
||||
if code, _ := apiReq(t, http.MethodGet, ts.URL+"/api/repos", testToken, ""); code != http.StatusInternalServerError {
|
||||
t.Fatal("GET /api/repos with broken store must 500")
|
||||
}
|
||||
if code, _ := apiReq(t, http.MethodPut, ts.URL+"/api/repos/g/p/image", testToken, `{"image":"lvmh-worker-x"}`); code != http.StatusInternalServerError {
|
||||
t.Fatal("PUT with broken store must 500")
|
||||
}
|
||||
if code, _ := apiReq(t, http.MethodDelete, ts.URL+"/api/repos/g/p/image", testToken, ""); code != http.StatusInternalServerError {
|
||||
t.Fatal("DELETE with broken store must 500")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user