Add exact-image compatibility policy, experimental admission, paginated build history and shared reauthentication checks. Preserve uncertain Docker creation records, bound frontend resource waits and renew runtime sessions proactively. Lazy-load and split Monaco, add regression and opt-in live Docker fault tests, and record full race and real-browser acceptance results.
502 lines
17 KiB
Go
502 lines
17 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.misaka.ren/m1saka/dsh-web/internal/docker"
|
|
"git.misaka.ren/m1saka/dsh-web/internal/store"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func imageTestAPI(t *testing.T, h http.HandlerFunc) (*API, string) {
|
|
t.Helper()
|
|
oldDiskAvailable := buildDiskAvailable
|
|
buildDiskAvailable = func(string) (uint64, error) { return minimumBuildDiskBytes, nil }
|
|
t.Cleanup(func() { buildDiskAvailable = oldDiskAvailable })
|
|
oldClient := npmBuildClient
|
|
npmBuildClient = &http.Client{Transport: imageRoundTrip(func(r *http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"version":"0.2.0-rc.1"}`))}, nil
|
|
})}
|
|
t.Cleanup(func() { npmBuildClient = oldClient })
|
|
root := t.TempDir()
|
|
t.Setenv("DSH_DOCKER_ROOT_VISIBLE", root)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v1.47/info" {
|
|
writeJSON(w, map[string]string{"DockerRootDir": root})
|
|
return
|
|
}
|
|
h(w, r)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
s, err := store.Open(filepath.Join(t.TempDir(), "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.DB.SetMaxOpenConns(1)
|
|
a := New(s, t.TempDir())
|
|
a.docker = docker.New(srv.URL)
|
|
t.Cleanup(func() {
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
buildQueue.Lock()
|
|
active := buildQueue.active[s.DB]
|
|
buildQueue.Unlock()
|
|
if !active {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Error("worker did not finish")
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
s.Close()
|
|
})
|
|
if err = s.CreateUser(store.User{ID: "u", Username: "user"}, "unused"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sid, _, err := s.Session("u")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registerTestAccess(t, a, sid)
|
|
return a, sid
|
|
}
|
|
func imageRequest(h http.HandlerFunc, sid, method, body string) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest(method, "/", strings.NewReader(body))
|
|
if sid != "" {
|
|
r.AddCookie(&http.Cookie{Name: "dsh_session", Value: sid})
|
|
addTestAccess(r, sid)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h(w, r)
|
|
return w
|
|
}
|
|
func TestImageListContract(t *testing.T) {
|
|
a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v1.47/images/unverified/json" {
|
|
writeImageInspect(w, "unverified", "0.2.0-rc.1")
|
|
return
|
|
}
|
|
if r.URL.Path == "/v1.47/images/sha256:abc/json" {
|
|
writeImageInspect(w, "sha256:abc", "0.2.0-rc.1")
|
|
return
|
|
}
|
|
if !strings.Contains(r.URL.Query().Get("filters"), "dsh-mgr.role=runtime") {
|
|
t.Error("missing label filter")
|
|
}
|
|
io.WriteString(w, `[{"Id":"sha256:abc","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"0.2.0-rc.1","dsh-mgr.recipe":"`+imageRecipeRev+`"},"RepoTags":["runtime:0.2.0-rc.1"]},{"Id":"other","Labels":{}},{"Id":"unverified","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"0.2.0-rc.1","dsh-mgr.recipe":"`+imageRecipeRev+`"}},{"Id":"sha256:abc","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"9.9.9"}}]`)
|
|
})
|
|
seedVerifiedImage(t, a, "sha256:abc", "0.2.0-rc.1")
|
|
if w := imageRequest(a.images, "", "GET", ""); w.Code != 401 {
|
|
t.Fatal(w.Code)
|
|
}
|
|
w := imageRequest(a.images, sid, "GET", "")
|
|
var images []imageRecord
|
|
if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(images) != 2 || images[0].ID != "sha256:abc" || images[0].ImageID != images[0].ID || images[0].Status != "available" || images[1].Status != "unverified" {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
if w = imageRequest(a.images, sid, "POST", ""); w.Code != 405 {
|
|
t.Fatal(w.Code)
|
|
}
|
|
}
|
|
|
|
func seedVerifiedImage(t *testing.T, a *API, id, version string) {
|
|
t.Helper()
|
|
buildQueue.Lock()
|
|
defer buildQueue.Unlock()
|
|
if err := a.ensureBuilds(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := a.s.DB.Exec("INSERT INTO image_builds(id,version,status,image_id,created_at,recipe_rev,platform) VALUES(?,?,'success',?,'now',?,?)", store.ID(), version, id, imageRecipeRev, imagePlatform); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := a.s.DB.Exec("INSERT OR REPLACE INTO image_admissions(image_id,version,recipe_rev,platform,state,evidence,admitted_by,admitted_at) VALUES(?,?,?,?,'compatible','test fixture explicit acceptance','test','now')", id, version, imageRecipeRev, imagePlatform); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func TestImageContextAndSanitizing(t *testing.T) {
|
|
r, err := imageBuildContext()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr := tar.NewReader(r)
|
|
n := 0
|
|
for {
|
|
h, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := io.ReadAll(tr)
|
|
original, err := os.ReadFile(filepath.Join("..", "..", "image", h.Name))
|
|
if err != nil || string(data) != string(original) {
|
|
t.Fatalf("recipe drift %s: %v", h.Name, err)
|
|
}
|
|
if h.Name == "Dockerfile" && (!strings.Contains(string(data), "ARG RECIPE_REV="+imageRecipeRev+"\n") || !strings.Contains(string(data), "dsh-mgr.recipe=$RECIPE_REV")) {
|
|
t.Fatal("recipe revision or label differs from image policy")
|
|
}
|
|
n++
|
|
}
|
|
if n != 7 {
|
|
t.Fatal(n)
|
|
}
|
|
s := sanitizeBuildLog("token=topsecret Authorization: Bearer hidden password=private https://user:[email protected]/")
|
|
for _, secret := range []string{"topsecret", "hidden", "private", "user:pass"} {
|
|
if strings.Contains(s, secret) {
|
|
t.Fatal(s)
|
|
}
|
|
}
|
|
if len(sanitizeBuildLog(strings.Repeat("x", 2*buildLogLimit))) > buildLogLimit {
|
|
t.Fatal("unbounded")
|
|
}
|
|
for _, v := range []string{"latest", "*", "^0.2.0-rc.1", "1.2", "0.2.0-rc.1;id", "https://example.com", ""} {
|
|
if validBuildVersion(v) {
|
|
t.Fatal(v)
|
|
}
|
|
}
|
|
if !validBuildVersion("0.2.0-rc.1-beta.1") {
|
|
t.Fatal("fixed prerelease rejected")
|
|
}
|
|
}
|
|
|
|
type imageRoundTrip func(*http.Request) (*http.Response, error)
|
|
|
|
func (f imageRoundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
|
func TestLatestOfficialRegistry(t *testing.T) {
|
|
client := &http.Client{Transport: imageRoundTrip(func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Host != "registry.npmjs.org" || r.URL.Scheme != "https" {
|
|
t.Fatal(r.URL)
|
|
}
|
|
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(`{"version":"0.2.0-rc.1"}`)), Header: make(http.Header)}, nil
|
|
})}
|
|
v, err := fetchLatest(context.Background(), client)
|
|
if err != nil || v != "0.2.0-rc.1" {
|
|
t.Fatalf("%s %v", v, err)
|
|
}
|
|
}
|
|
func TestBuildDedupAndVerification(t *testing.T) {
|
|
var calls, deleted atomic.Int32
|
|
a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v1.47/images/sha256:abc/json", "/v1.47/images/sha256:reuse/json":
|
|
writeImageInspect(w, strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v1.47/images/"), "/json"), "0.2.0-rc.1")
|
|
case "/v1.47/images/json":
|
|
if calls.Load() == 0 {
|
|
io.WriteString(w, "[]")
|
|
} else {
|
|
io.WriteString(w, `[{"Id":"sha256:abc","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"0.2.0-rc.1","dsh-mgr.recipe":"`+imageRecipeRev+`"}}]`)
|
|
}
|
|
case "/v1.47/build":
|
|
calls.Add(1)
|
|
if r.URL.Query().Get("t") != "dsh-web:0.2.0-rc.1-"+imageRecipeRev {
|
|
t.Error("unexpected tag", r.URL.Query())
|
|
}
|
|
var args map[string]string
|
|
json.Unmarshal([]byte(r.URL.Query().Get("buildargs")), &args)
|
|
if args["DSH_VERSION"] != "0.2.0-rc.1" {
|
|
t.Error(args)
|
|
}
|
|
io.Copy(io.Discard, r.Body)
|
|
io.WriteString(w, `{"stream":"token=secretvalue"}`+string(rune(10))+`{"aux":{"ID":"sha256:abc"}}`)
|
|
case "/v1.47/containers/create":
|
|
var cfg struct {
|
|
Image string
|
|
HostConfig struct {
|
|
NetworkMode string
|
|
ReadonlyRootfs bool
|
|
Tmpfs map[string]string
|
|
}
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&cfg)
|
|
if cfg.Image != "sha256:abc" || cfg.HostConfig.NetworkMode != "none" || !cfg.HostConfig.ReadonlyRootfs || !strings.Contains(cfg.HostConfig.Tmpfs["/tmp"], "exec") {
|
|
t.Errorf("unsafe config %+v", cfg)
|
|
}
|
|
io.WriteString(w, `{"Id":"check"}`)
|
|
case "/v1.47/containers/check/start", "/v1.47/exec/probe/start":
|
|
w.WriteHeader(204)
|
|
case "/v1.47/containers/check/exec":
|
|
var cfg struct{ Cmd []string }
|
|
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil || len(cfg.Cmd) != 3 || !strings.Contains(cfg.Cmd[2], "execFileSync('dsh',['--version']") || !strings.Contains(cfg.Cmd[2], `actual!=="0.2.0-rc.1"`) {
|
|
t.Errorf("missing version verification: %+v %v", cfg, err)
|
|
}
|
|
io.WriteString(w, `{"Id":"probe"}`)
|
|
case "/v1.47/exec/probe/json":
|
|
io.WriteString(w, `{"Running":false,"ExitCode":0}`)
|
|
case "/v1.47/containers/check/json":
|
|
io.WriteString(w, `{"State":{"Running":true}}`)
|
|
case "/v1.47/containers/check":
|
|
deleted.Add(1)
|
|
w.WriteHeader(204)
|
|
default:
|
|
t.Errorf("unexpected %s", r.URL)
|
|
w.WriteHeader(500)
|
|
}
|
|
})
|
|
responses := make(chan *httptest.ResponseRecorder, 8)
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 8; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
responses <- imageRequest(a.builds, sid, "POST", `{"version":"0.2.0-rc.1"}`)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(responses)
|
|
first := ""
|
|
for w := range responses {
|
|
if w.Code != 202 {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
var b buildRecord
|
|
json.Unmarshal(w.Body.Bytes(), &b)
|
|
if first == "" {
|
|
first = b.ID
|
|
}
|
|
if first != b.ID {
|
|
t.Fatal("duplicate job")
|
|
}
|
|
}
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
w := imageRequest(a.builds, sid, "GET", "")
|
|
var jobs []buildRecord
|
|
json.Unmarshal(w.Body.Bytes(), &jobs)
|
|
if len(jobs) == 1 && jobs[0].Status == "success" {
|
|
if strings.Contains(jobs[0].Logs, "secretvalue") {
|
|
t.Fatal(jobs)
|
|
}
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if calls.Load() != 1 || deleted.Load() != 1 {
|
|
t.Fatalf("builds %d cleanup %d", calls.Load(), deleted.Load())
|
|
}
|
|
for _, body := range []string{`{"version":"latest"}`, `{"version":"0.2.0-rc.1","url":"bad"}`, `{"version":"0.2.0-rc.1"} {}`} {
|
|
if w := imageRequest(a.builds, sid, "POST", body); w.Code != 400 {
|
|
t.Fatal(w.Code)
|
|
}
|
|
}
|
|
}
|
|
func TestVerificationFailureCleansUp(t *testing.T) {
|
|
var deleted atomic.Bool
|
|
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/create"):
|
|
io.WriteString(w, `{"Id":"bad"}`)
|
|
case r.Method == "DELETE":
|
|
deleted.Store(true)
|
|
w.WriteHeader(204)
|
|
default:
|
|
w.WriteHeader(500)
|
|
io.WriteString(w, `{"message":"cannot start"}`)
|
|
}
|
|
})
|
|
if err := a.verifyBuiltImage(context.Background(), "bad", "0.2.0-rc.1"); err == nil {
|
|
t.Fatal("accepted failed start")
|
|
}
|
|
if !deleted.Load() {
|
|
t.Fatal("container leaked")
|
|
}
|
|
}
|
|
|
|
func TestBuildReuseAndMissingImage(t *testing.T) {
|
|
var exists atomic.Bool
|
|
exists.Store(true)
|
|
var builds, checks atomic.Int32
|
|
a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v1.47/images/sha256:abc/json", "/v1.47/images/sha256:reuse/json":
|
|
writeImageInspect(w, strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v1.47/images/"), "/json"), "0.2.0-rc.1")
|
|
case "/v1.47/images/json":
|
|
if exists.Load() {
|
|
io.WriteString(w, `[{"Id":"sha256:reuse","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"0.2.0-rc.1","dsh-mgr.recipe":"`+imageRecipeRev+`"}}]`)
|
|
} else {
|
|
io.WriteString(w, "[]")
|
|
}
|
|
case "/v1.47/build":
|
|
builds.Add(1)
|
|
exists.Store(true)
|
|
io.Copy(io.Discard, r.Body)
|
|
io.WriteString(w, `{"aux":{"ID":"sha256:reuse"}}`)
|
|
case "/v1.47/containers/create":
|
|
checks.Add(1)
|
|
io.WriteString(w, `{"Id":"c"}`)
|
|
case "/v1.47/containers/c/start", "/v1.47/exec/e/start", "/v1.47/containers/c":
|
|
w.WriteHeader(204)
|
|
case "/v1.47/containers/c/exec":
|
|
io.WriteString(w, `{"Id":"e"}`)
|
|
case "/v1.47/exec/e/json":
|
|
io.WriteString(w, `{"Running":false,"ExitCode":0}`)
|
|
case "/v1.47/containers/c/json":
|
|
io.WriteString(w, `{"State":{"Running":true}}`)
|
|
default:
|
|
t.Errorf("unexpected %s", r.URL)
|
|
w.WriteHeader(500)
|
|
}
|
|
})
|
|
wait := func(id string) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
var status, image string
|
|
err := a.s.DB.QueryRow("SELECT status,image_id FROM image_builds WHERE id=?", id).Scan(&status, &image)
|
|
if err == nil && status == "success" {
|
|
if image != "sha256:reuse" {
|
|
t.Fatal(image)
|
|
}
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("status %s: %v", status, err)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
post := func() buildRecord {
|
|
t.Helper()
|
|
w := imageRequest(a.builds, sid, "POST", `{"version":"0.2.0-rc.1"}`)
|
|
if w.Code != 202 {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
var b buildRecord
|
|
if err := json.Unmarshal(w.Body.Bytes(), &b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
first := post()
|
|
wait(first.ID)
|
|
if builds.Load() != 0 || checks.Load() != 1 {
|
|
t.Fatal("local image not reused and verified")
|
|
}
|
|
exists.Store(false)
|
|
second := post()
|
|
if second.ID == first.ID {
|
|
t.Fatal("missing image deduplicated")
|
|
}
|
|
wait(second.ID)
|
|
if builds.Load() != 1 || checks.Load() != 2 {
|
|
t.Fatal("missing image not rebuilt")
|
|
}
|
|
}
|
|
func TestBuildRestartRecovery(t *testing.T) {
|
|
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) })
|
|
buildQueue.Lock()
|
|
defer buildQueue.Unlock()
|
|
if err := a.ensureBuilds(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := a.s.DB.Exec("INSERT INTO image_builds(id,version,status,created_at) VALUES('old','0.2.0-rc.1','running','now')"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
delete(buildQueue.initialized, a.s.DB)
|
|
if err := a.ensureBuilds(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var status, detail string
|
|
if err := a.s.DB.QueryRow("SELECT status,error FROM image_builds WHERE id='old'").Scan(&status, &detail); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != "failed" || !strings.Contains(detail, "restart") {
|
|
t.Fatalf("%s %s", status, detail)
|
|
}
|
|
}
|
|
|
|
func TestFailedVerificationNeverAvailable(t *testing.T) {
|
|
var deleted atomic.Bool
|
|
imageID := "sha256:" + strings.Repeat("c", 64)
|
|
a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v1.47/images/sha256:abc/json", "/v1.47/images/sha256:reuse/json":
|
|
writeImageInspect(w, strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v1.47/images/"), "/json"), "0.2.0-rc.1")
|
|
case "/v1.47/images/json":
|
|
writeJSON(w, []any{map[string]any{"Id": imageID, "Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "0.2.0-rc.1", "dsh-mgr.recipe": imageRecipeRev}}})
|
|
case "/v1.47/containers/create":
|
|
io.WriteString(w, `{"Id":"check"}`)
|
|
case "/v1.47/containers/check/start", "/v1.47/exec/probe/start":
|
|
w.WriteHeader(204)
|
|
case "/v1.47/containers/check/exec":
|
|
io.WriteString(w, `{"Id":"probe"}`)
|
|
case "/v1.47/exec/probe/json":
|
|
io.WriteString(w, `{"Running":false,"ExitCode":1}`)
|
|
case "/v1.47/containers/check":
|
|
deleted.Store(true)
|
|
w.WriteHeader(204)
|
|
case "/v1.47/images/" + imageID + "/json":
|
|
writeImageInspect(w, imageID, "0.2.0-rc.1")
|
|
default:
|
|
t.Errorf("unexpected Docker call %s", r.URL)
|
|
w.WriteHeader(500)
|
|
}
|
|
})
|
|
w := imageRequest(a.builds, sid, "POST", `{"version":"0.2.0-rc.1"}`)
|
|
if w.Code != 202 {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
var status, id, detail string
|
|
err := a.s.DB.QueryRow("SELECT status,image_id,error FROM image_builds").Scan(&status, &id, &detail)
|
|
if err == nil && status == "failed" {
|
|
if id != imageID || !strings.Contains(detail, "verification failed") || !deleted.Load() {
|
|
t.Fatalf("%s %s cleanup=%v", id, detail, deleted.Load())
|
|
}
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("status=%s error=%v", status, err)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
w = imageRequest(a.images, sid, "GET", "")
|
|
var images []imageRecord
|
|
if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil || w.Code != 200 || len(images) != 1 || images[0].Status != "unverified" {
|
|
t.Fatal("failed image advertised", w.Body.String())
|
|
}
|
|
w = imageRequest(a.instances, sid, "POST", fmt.Sprintf(`{"name":"rejected","image_id":%q}`, imageID))
|
|
if w.Code != 400 {
|
|
t.Fatal("unverified image accepted", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLocalBuildImagePrefersPersistedID(t *testing.T) {
|
|
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v1.47/images/verified/json" {
|
|
writeImageInspect(w, "verified", "0.2.0-rc.1")
|
|
return
|
|
}
|
|
io.WriteString(w, `[{"Id":"newer","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"0.2.0-rc.1","dsh-mgr.recipe":"`+imageRecipeRev+`"}},{"Id":"verified","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"0.2.0-rc.1","dsh-mgr.recipe":"`+imageRecipeRev+`"}}]`)
|
|
})
|
|
id, err := a.localBuildImage(context.Background(), "0.2.0-rc.1", "verified")
|
|
if err != nil || id != "verified" {
|
|
t.Fatalf("id=%s err=%v", id, err)
|
|
}
|
|
}
|
|
|
|
func writeImageInspect(w http.ResponseWriter, id, version string) {
|
|
parts := strings.Split(imagePlatform, "/")
|
|
writeJSON(w, map[string]any{"Id": id, "Os": parts[0], "Architecture": parts[1], "Config": map[string]any{"Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": version, "dsh-mgr.recipe": imageRecipeRev}}})
|
|
}
|