Harden image admission, resource recovery and session renewal
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.
This commit is contained in:
+256
-93
@@ -29,10 +29,13 @@ func (a *API) ensureBuilds() error {
|
||||
return err
|
||||
}
|
||||
for _, q := range []string{
|
||||
"CREATE INDEX IF NOT EXISTS image_builds_history ON image_builds(created_at DESC,id DESC)",
|
||||
"ALTER TABLE image_builds ADD COLUMN requested_by TEXT NOT NULL DEFAULT ''",
|
||||
"ALTER TABLE image_builds ADD COLUMN recipe_rev TEXT",
|
||||
"ALTER TABLE image_builds ADD COLUMN platform TEXT",
|
||||
"ALTER TABLE image_builds ADD COLUMN finished_at TEXT",
|
||||
"ALTER TABLE image_builds ADD COLUMN experimental INTEGER NOT NULL DEFAULT 0",
|
||||
"CREATE TABLE IF NOT EXISTS image_admissions(image_id TEXT NOT NULL,version TEXT NOT NULL,recipe_rev TEXT NOT NULL,platform TEXT NOT NULL,state TEXT NOT NULL,evidence TEXT NOT NULL,admitted_by TEXT NOT NULL,admitted_at TEXT NOT NULL,PRIMARY KEY(image_id,version,recipe_rev,platform))",
|
||||
"ALTER TABLE images ADD COLUMN recipe_rev TEXT",
|
||||
"ALTER TABLE images ADD COLUMN platform TEXT",
|
||||
"ALTER TABLE images ADD COLUMN usage_count INTEGER NOT NULL DEFAULT 0",
|
||||
@@ -45,6 +48,9 @@ func (a *API) ensureBuilds() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = a.s.DB.Exec("UPDATE image_builds SET status='failed',error='version requires administrator experimental build' WHERE status='queued' AND experimental=0 AND version NOT IN ('0.2.0-rc.1','0.2.0-rc.2')"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !buildQueue.initialized[a.s.DB] {
|
||||
if _, err = a.s.DB.Exec("UPDATE image_builds SET status='failed',error='image template identity is unknown' WHERE status='queued' AND (recipe_rev IS NULL OR recipe_rev<>? OR platform IS NULL OR platform<>?)", imageRecipeRev, imagePlatform); err != nil {
|
||||
return err
|
||||
@@ -66,7 +72,10 @@ func (a *API) builds(w http.ResponseWriter, r *http.Request) {
|
||||
fail(w, 405, "method not allowed")
|
||||
return
|
||||
}
|
||||
var in struct{ Version string }
|
||||
var in struct {
|
||||
Version string `json:"version"`
|
||||
Experimental bool `json:"experimental"`
|
||||
}
|
||||
if r.Method == http.MethodPost {
|
||||
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096))
|
||||
dec.DisallowUnknownFields()
|
||||
@@ -76,48 +85,19 @@ func (a *API) builds(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if r.Method == http.MethodGet {
|
||||
buildQueue.Lock()
|
||||
err := a.ensureBuilds()
|
||||
var rows *sql.Rows
|
||||
if err == nil {
|
||||
rows, err = a.s.DB.Query("SELECT id,version,status,logs,error,image_id,created_at,requested_by,recipe_rev,platform,finished_at FROM image_builds ORDER BY created_at DESC,id DESC")
|
||||
}
|
||||
result := []buildRecord{}
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var b buildRecord
|
||||
var finished, recipe, platform sql.NullString
|
||||
if err = rows.Scan(&b.ID, &b.Version, &b.Status, &b.Logs, &b.Error, &b.ImageID, &b.Created, &b.RequestedBy, &recipe, &platform, &finished); err != nil {
|
||||
break
|
||||
}
|
||||
b.RecipeRev, b.Platform, b.Finished = recipe.String, platform.String, finished.String
|
||||
result = append(result, b)
|
||||
}
|
||||
scanErr := rows.Err()
|
||||
rows.Close()
|
||||
if err == nil {
|
||||
err = scanErr
|
||||
}
|
||||
}
|
||||
buildQueue.Unlock()
|
||||
if err != nil {
|
||||
fail(w, 500, "database error")
|
||||
return
|
||||
}
|
||||
// Docker image reconciliation is I/O-bound and updates rows with
|
||||
// compare-and-set, so it must not hold the queue lock that instance
|
||||
// creation and upgrades also need.
|
||||
if err := a.reconcileBuilds(r.Context(), result); err != nil {
|
||||
fail(w, 502, "docker image reconciliation failed")
|
||||
return
|
||||
}
|
||||
a.startBuildWorker()
|
||||
writeJSON(w, result)
|
||||
a.listBuilds(w, r)
|
||||
return
|
||||
}
|
||||
b, err := a.enqueueBuild(r.Context(), u.ID, in.Version)
|
||||
if in.Experimental && !u.Admin {
|
||||
fail(w, 403, "administrator required")
|
||||
return
|
||||
}
|
||||
b, err := a.enqueueBuild(r.Context(), u.ID, in.Version, in.Experimental)
|
||||
if err != nil {
|
||||
status := 502
|
||||
if errors.Is(err, errBuildUnsupported) {
|
||||
status = 400
|
||||
}
|
||||
if errors.Is(err, errBuildRateLimited) {
|
||||
status = 429
|
||||
w.Header().Set("Retry-After", "3600")
|
||||
@@ -137,7 +117,7 @@ var errBuildRateLimited = errors.New("build limit: three new tasks per user per
|
||||
func (a *API) lookupActiveBuild(version string) (buildRecord, bool, error) {
|
||||
var b buildRecord
|
||||
var finished, recipe, platform sql.NullString
|
||||
err := a.s.DB.QueryRow("SELECT id,version,status,logs,error,image_id,created_at,requested_by,recipe_rev,platform,finished_at FROM image_builds WHERE version=? AND recipe_rev=? AND platform=? AND status IN ('queued','running','validating','success')", version, imageRecipeRev, imagePlatform).Scan(&b.ID, &b.Version, &b.Status, &b.Logs, &b.Error, &b.ImageID, &b.Created, &b.RequestedBy, &recipe, &platform, &finished)
|
||||
err := a.s.DB.QueryRow("SELECT id,version,status,logs,error,image_id,created_at,requested_by,recipe_rev,platform,finished_at,experimental FROM image_builds WHERE version=? AND recipe_rev=? AND platform=? AND status IN ('queued','running','validating','success')", version, imageRecipeRev, imagePlatform).Scan(&b.ID, &b.Version, &b.Status, &b.Logs, &b.Error, &b.ImageID, &b.Created, &b.RequestedBy, &recipe, &platform, &finished, &b.Experimental)
|
||||
b.RecipeRev, b.Platform, b.Finished = recipe.String, platform.String, finished.String
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return b, false, nil
|
||||
@@ -148,7 +128,17 @@ func (a *API) lookupActiveBuild(version string) (buildRecord, bool, error) {
|
||||
return b, true, nil
|
||||
}
|
||||
|
||||
func (a *API) enqueueBuild(ctx context.Context, uid, version string) (buildRecord, error) {
|
||||
func (a *API) enqueueBuild(ctx context.Context, uid, version string, experiment ...bool) (buildRecord, error) {
|
||||
experimental := len(experiment) > 0 && experiment[0]
|
||||
if experimental {
|
||||
u, err := a.s.UserByID(uid)
|
||||
if err != nil || !u.Admin {
|
||||
return buildRecord{}, errors.New("administrator required")
|
||||
}
|
||||
}
|
||||
if !experimental && !normalBuildAllowed(version) {
|
||||
return buildRecord{}, errBuildUnsupported
|
||||
}
|
||||
if !validBuildVersion(version) {
|
||||
return buildRecord{}, errors.New("a fixed version is required")
|
||||
}
|
||||
@@ -163,6 +153,9 @@ func (a *API) enqueueBuild(ctx context.Context, uid, version string) (buildRecor
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
if found && !experimental && b.Experimental && b.Status != "success" {
|
||||
return b, errBuildUnsupported
|
||||
}
|
||||
if found && b.Status != "success" {
|
||||
a.startBuildWorker()
|
||||
return b, nil
|
||||
@@ -177,6 +170,15 @@ func (a *API) enqueueBuild(ctx context.Context, uid, version string) (buildRecor
|
||||
return b, lookupErr
|
||||
}
|
||||
if id != "" && id == b.ImageID {
|
||||
if !experimental {
|
||||
im, e := a.evaluateImage(ctx, id)
|
||||
if e != nil {
|
||||
return b, e
|
||||
}
|
||||
if !im.Selectable {
|
||||
return b, errBuildUnsupported
|
||||
}
|
||||
}
|
||||
a.startBuildWorker()
|
||||
return b, nil
|
||||
}
|
||||
@@ -187,12 +189,15 @@ func (a *API) enqueueBuild(ctx context.Context, uid, version string) (buildRecor
|
||||
b, found, err = a.lookupActiveBuild(version)
|
||||
}
|
||||
if err == nil && !found {
|
||||
b, err = a.insertBuild(uid, version)
|
||||
b, err = a.insertBuild(uid, version, experimental)
|
||||
}
|
||||
buildQueue.Unlock()
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
if !experimental && found && b.Experimental {
|
||||
return b, errBuildUnsupported
|
||||
}
|
||||
a.startBuildWorker()
|
||||
return b, nil
|
||||
}
|
||||
@@ -204,9 +209,12 @@ func (a *API) enqueueBuild(ctx context.Context, uid, version string) (buildRecor
|
||||
return b, err
|
||||
}
|
||||
if found {
|
||||
if !experimental && b.Experimental {
|
||||
return b, errBuildUnsupported
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
b, err = a.insertBuild(uid, version)
|
||||
b, err = a.insertBuild(uid, version, experimental)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
@@ -216,7 +224,7 @@ func (a *API) enqueueBuild(ctx context.Context, uid, version string) (buildRecor
|
||||
|
||||
// insertBuild applies the per-user rate limit and inserts a queued record;
|
||||
// call with the build queue held.
|
||||
func (a *API) insertBuild(uid, version string) (buildRecord, error) {
|
||||
func (a *API) insertBuild(uid, version string, experiment ...bool) (buildRecord, error) {
|
||||
var count int
|
||||
if err := a.s.DB.QueryRow("SELECT count(*) FROM image_build_requests WHERE user_id=? AND created_at>?", uid, time.Now().Add(-time.Hour).UTC().Format(time.RFC3339Nano)).Scan(&count); err != nil {
|
||||
return buildRecord{}, err
|
||||
@@ -225,6 +233,7 @@ func (a *API) insertBuild(uid, version string) (buildRecord, error) {
|
||||
return buildRecord{}, errBuildRateLimited
|
||||
}
|
||||
b := buildRecord{ID: store.ID(), Version: version, Status: "queued", Created: time.Now().UTC().Format(time.RFC3339Nano), RequestedBy: uid, RecipeRev: imageRecipeRev, Platform: imagePlatform}
|
||||
b.Experimental = len(experiment) > 0 && experiment[0]
|
||||
return b, a.insertBuildRequest(b, uid)
|
||||
}
|
||||
func (a *API) startBuildWorker() {
|
||||
@@ -241,22 +250,56 @@ func (a *API) startBuildWorkerLocked() {
|
||||
}
|
||||
buildQueue.active[a.s.DB] = true
|
||||
go func() {
|
||||
buildWorker.Lock()
|
||||
defer buildWorker.Unlock()
|
||||
var lastCleanupAudit time.Time
|
||||
var nextCleanup time.Time
|
||||
var recoveryErr error
|
||||
for {
|
||||
select {
|
||||
case <-a.s.Done():
|
||||
buildQueue.Lock()
|
||||
delete(buildQueue.active, a.s.DB)
|
||||
buildQueue.Unlock()
|
||||
return
|
||||
default:
|
||||
}
|
||||
if time.Now().After(nextCleanup) {
|
||||
buildWorker.Lock()
|
||||
recovery, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
recoveryErr = a.recoverBuildChecks(recovery)
|
||||
cancel()
|
||||
buildWorker.Unlock()
|
||||
if errors.Is(recoveryErr, errBuildCheckUncertain) {
|
||||
nextCleanup = time.Now().Add(time.Minute)
|
||||
}
|
||||
}
|
||||
if recoveryErr != nil {
|
||||
if time.Since(lastCleanupAudit) >= time.Minute {
|
||||
a.s.AddAudit("", "build.cleanup_error", "", sanitizeBuildLog(recoveryErr.Error()))
|
||||
lastCleanupAudit = time.Now()
|
||||
}
|
||||
}
|
||||
buildQueue.Lock()
|
||||
var b buildRecord
|
||||
err := a.s.DB.QueryRow("SELECT id,version FROM image_builds WHERE status='queued' ORDER BY created_at,id LIMIT 1").Scan(&b.ID, &b.Version)
|
||||
err := a.s.DB.QueryRow("SELECT id,version,experimental,requested_by FROM image_builds WHERE status='queued' ORDER BY created_at,id LIMIT 1").Scan(&b.ID, &b.Version, &b.Experimental, &b.RequestedBy)
|
||||
if err == nil {
|
||||
_, err = a.s.DB.Exec("UPDATE image_builds SET status='running' WHERE id=?", b.ID)
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) || recoveryErr != nil {
|
||||
buildQueue.Unlock()
|
||||
select {
|
||||
case <-a.s.Done():
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
continue
|
||||
}
|
||||
delete(buildQueue.active, a.s.DB)
|
||||
buildQueue.Unlock()
|
||||
return
|
||||
}
|
||||
buildQueue.Unlock()
|
||||
a.runImageBuild(b)
|
||||
nextCleanup = time.Time{}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -282,6 +325,7 @@ func imageBuildContext() (io.Reader, error) {
|
||||
return &buf, nil
|
||||
}
|
||||
func (a *API) runImageBuild(b buildRecord) {
|
||||
buildWorker.Lock()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
logs := ""
|
||||
@@ -301,6 +345,15 @@ func (a *API) runImageBuild(b buildRecord) {
|
||||
}
|
||||
}
|
||||
tarball, err := imageBuildContext()
|
||||
if !b.Experimental && !normalBuildAllowed(b.Version) {
|
||||
err = errBuildUnsupported
|
||||
}
|
||||
if b.Experimental {
|
||||
u, e := a.s.UserByID(b.RequestedBy)
|
||||
if e != nil || !u.Admin {
|
||||
err = errors.New("experimental build requester is not an administrator")
|
||||
}
|
||||
}
|
||||
var id string
|
||||
if err == nil {
|
||||
id, err = a.localBuildImage(ctx, b.Version, "")
|
||||
@@ -325,7 +378,7 @@ func (a *API) runImageBuild(b buildRecord) {
|
||||
_, err = a.s.DB.Exec("UPDATE image_builds SET status='validating',image_id=? WHERE id=?", id, b.ID)
|
||||
}
|
||||
if err == nil {
|
||||
err = a.verifyBuiltImage(ctx, id, b.Version)
|
||||
err = a.verifyBuiltImage(ctx, id, b.Version, b.ID)
|
||||
}
|
||||
if err == nil {
|
||||
err = logErr
|
||||
@@ -337,31 +390,53 @@ func (a *API) runImageBuild(b buildRecord) {
|
||||
appendLog(string(rune(10)) + detail)
|
||||
}
|
||||
finished := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
_, _ = a.s.DB.Exec("UPDATE image_builds SET status=?,logs=?,error=?,image_id=?,finished_at=? WHERE id=?", status, logs, detail, id, finished, b.ID)
|
||||
if status == "success" && id != "" {
|
||||
_, _ = a.s.DB.Exec("INSERT INTO images(image_id,version,recipe_rev,platform,tag,status,built_at) VALUES(?,?,?,?,?,?,?) ON CONFLICT(image_id) DO UPDATE SET version=excluded.version,recipe_rev=excluded.recipe_rev,platform=excluded.platform,tag=excluded.tag,status=excluded.status,built_at=excluded.built_at", id, b.Version, imageRecipeRev, imagePlatform, "dsh-web:"+b.Version+"-"+imageRecipeRev, "available", finished)
|
||||
buildWorker.Unlock()
|
||||
if err := a.persistBuildResult(b, status, logs, detail, id, finished); err != nil {
|
||||
go a.finishBuild(b, status, logs, detail, id, finished)
|
||||
}
|
||||
}
|
||||
func (a *API) verifyBuiltImage(parent context.Context, image, version string) (result error) {
|
||||
func (a *API) verifyBuiltImage(parent context.Context, image, version string, buildIDs ...string) (result error) {
|
||||
ctx, cancel := context.WithTimeout(parent, 90*time.Second)
|
||||
defer cancel()
|
||||
id := store.ID()
|
||||
if len(buildIDs) != 0 {
|
||||
id = buildIDs[0]
|
||||
}
|
||||
if err := a.ensureBuildChecks(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := a.s.DB.Exec("INSERT INTO image_build_checks(build_id,image_id,created_at) VALUES(?,?,?)", id, image, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return err
|
||||
}
|
||||
var created struct {
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
config := map[string]any{"Image": image, "User": "1000:1000", "Labels": map[string]string{"dsh-mgr.role": "build-check"}, "HostConfig": map[string]any{"NetworkMode": "none", "ReadonlyRootfs": true, "CapDrop": []string{"ALL"}, "SecurityOpt": []string{"no-new-privileges:true"}, "Memory": int64(1 << 30), "NanoCpus": int64(1e9), "PidsLimit": 256, "Tmpfs": map[string]string{"/tmp": "rw,exec,nosuid,nodev,size=256m,mode=1777", "/workspace": "rw,nosuid,nodev,size=256m,uid=1000,gid=1000"}}}
|
||||
if err := a.docker.Do(ctx, http.MethodPost, "/containers/create", config, &created); err != nil {
|
||||
rejected := false
|
||||
defer func() {
|
||||
cleanup, c := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer c()
|
||||
var err error
|
||||
if rejected {
|
||||
_, err = a.s.DB.Exec("DELETE FROM image_build_checks WHERE build_id=?", id)
|
||||
} else if created.ID != "" {
|
||||
err = a.deleteBuildCheck(cleanup, id, created.ID)
|
||||
} else {
|
||||
err = a.cleanupBuildCheck(cleanup, id, image)
|
||||
}
|
||||
result = errors.Join(result, err)
|
||||
}()
|
||||
config := map[string]any{"Image": image, "User": "1000:1000", "Labels": map[string]string{"dsh-mgr.role": "build-check", "dsh-mgr.build": id}, "HostConfig": map[string]any{"NetworkMode": "none", "ReadonlyRootfs": true, "CapDrop": []string{"ALL"}, "SecurityOpt": []string{"no-new-privileges:true"}, "Memory": int64(1 << 30), "NanoCpus": int64(1e9), "PidsLimit": 256, "Tmpfs": map[string]string{"/tmp": "rw,exec,nosuid,nodev,size=256m,mode=1777", "/workspace": "rw,nosuid,nodev,size=256m,uid=1000,gid=1000"}}}
|
||||
if err := a.docker.Do(ctx, http.MethodPost, "/containers/create?name="+url.QueryEscape(buildCheckName(id)), config, &created); err != nil {
|
||||
rejected = dockerCreateRejected(err)
|
||||
return err
|
||||
}
|
||||
if created.ID == "" {
|
||||
return errors.New("verification container ID missing")
|
||||
}
|
||||
if _, err := a.s.DB.Exec("UPDATE image_build_checks SET container_id=? WHERE build_id=?", created.ID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
path := "/containers/" + url.PathEscape(created.ID)
|
||||
defer func() {
|
||||
cleanup, c := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer c()
|
||||
err := a.docker.Do(cleanup, http.MethodDelete, path+"?force=1&v=1", nil, nil)
|
||||
result = errors.Join(result, err)
|
||||
}()
|
||||
if err := a.docker.Do(ctx, http.MethodPost, path+"/start", nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -428,28 +503,32 @@ var buildQueue = struct {
|
||||
var buildWorker sync.Mutex
|
||||
|
||||
type imageRecord struct {
|
||||
ID string `json:"id"`
|
||||
ImageID string `json:"image_id"`
|
||||
Version string `json:"version"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Created string `json:"created_at"`
|
||||
RecipeRev string `json:"recipe_rev,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
UsageCount int `json:"usage_count"`
|
||||
Admission string `json:"admission"`
|
||||
AdmissionReason string `json:"admission_reason"`
|
||||
Selectable bool `json:"selectable"`
|
||||
ID string `json:"id"`
|
||||
ImageID string `json:"image_id"`
|
||||
Version string `json:"version"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Created string `json:"created_at"`
|
||||
RecipeRev string `json:"recipe_rev,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
UsageCount int `json:"usage_count"`
|
||||
}
|
||||
type buildRecord struct {
|
||||
ImageID string `json:"image_id,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Logs string `json:"logs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Created string `json:"created_at"`
|
||||
RequestedBy string `json:"requested_by,omitempty"`
|
||||
RecipeRev string `json:"recipe_rev,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Finished string `json:"finished_at,omitempty"`
|
||||
Experimental bool `json:"experimental"`
|
||||
ImageID string `json:"image_id,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Logs string `json:"logs,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Created string `json:"created_at"`
|
||||
RequestedBy string `json:"requested_by,omitempty"`
|
||||
RecipeRev string `json:"recipe_rev,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Finished string `json:"finished_at,omitempty"`
|
||||
}
|
||||
|
||||
func (a *API) images(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -475,26 +554,110 @@ func (a *API) images(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, result)
|
||||
}
|
||||
|
||||
func (a *API) verifiedImages(ctx context.Context) (map[string]string, error) {
|
||||
buildQueue.Lock()
|
||||
defer buildQueue.Unlock()
|
||||
if err := a.ensureBuilds(); err != nil {
|
||||
return nil, err
|
||||
// verifiedImagesTTL bounds how long a cached admission evaluation is reused
|
||||
// by the one-second intent poller.
|
||||
var verifiedImagesTTL = 5 * time.Second
|
||||
|
||||
type evalCacheEntry struct {
|
||||
im imageRecord
|
||||
at time.Time
|
||||
}
|
||||
|
||||
// evaluateImageCached reuses recent per-image evaluations for the intent
|
||||
// poller so repeated ticks do not hammer Docker. Deletions and admission
|
||||
// writes bump imageDeleteGeneration and invalidate every entry.
|
||||
func (a *API) evaluateImageCached(ctx context.Context, gen uint64, id string) (imageRecord, error) {
|
||||
a.evalCacheMu.Lock()
|
||||
if entry, ok := a.evalCache[id]; ok && a.evalCacheGen == gen && time.Since(entry.at) < verifiedImagesTTL {
|
||||
a.evalCacheMu.Unlock()
|
||||
return entry.im, nil
|
||||
}
|
||||
rows, err := a.s.DB.QueryContext(ctx, "SELECT image_id,version FROM image_builds WHERE status='success' AND image_id<>'' AND recipe_rev=? AND platform=?", imageRecipeRev, imagePlatform)
|
||||
a.evalCacheMu.Unlock()
|
||||
im, err := a.evaluateImage(ctx, id)
|
||||
if err != nil {
|
||||
return im, err
|
||||
}
|
||||
a.evalCacheMu.Lock()
|
||||
if a.evalCache == nil || a.evalCacheGen != gen {
|
||||
a.evalCache, a.evalCacheGen = make(map[string]evalCacheEntry), gen
|
||||
}
|
||||
a.evalCache[id] = evalCacheEntry{im: im, at: time.Now()}
|
||||
a.evalCacheMu.Unlock()
|
||||
return im, nil
|
||||
}
|
||||
|
||||
func (a *API) successfulImageIDs(ctx context.Context) ([]string, error) {
|
||||
rows, err := a.s.DB.QueryContext(ctx, "SELECT DISTINCT image_id FROM image_builds WHERE status='success' AND image_id<>''")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
images := make(map[string]string)
|
||||
ids := []string{}
|
||||
for rows.Next() {
|
||||
var id, version string
|
||||
if err := rows.Scan(&id, &version); err != nil {
|
||||
var id string
|
||||
if err = rows.Scan(&id); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
images[id] = version
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return images, rows.Err()
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (a *API) verifiedImages(ctx context.Context) (map[string]string, error) {
|
||||
buildQueue.Lock()
|
||||
err := a.ensureBuilds()
|
||||
buildQueue.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err := a.successfulImageIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := map[string]string{}
|
||||
for _, id := range ids {
|
||||
im, err := a.evaluateImage(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if im.Selectable {
|
||||
result[id] = im.Version
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// verifiedImagesPoll is the intent poller's view: build rows are reread every
|
||||
// tick, but per-image Docker evaluation is cached briefly so a stuck waiting
|
||||
// intent cannot turn the one-second loop into Docker hammering.
|
||||
func (a *API) verifiedImagesPoll(ctx context.Context) (map[string]string, error) {
|
||||
buildQueue.Lock()
|
||||
err := a.ensureBuilds()
|
||||
gen := a.imageDeleteGeneration
|
||||
buildQueue.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err := a.successfulImageIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := map[string]string{}
|
||||
for _, id := range ids {
|
||||
im, err := a.evaluateImageCached(ctx, gen, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if im.Selectable {
|
||||
result[id] = im.Version
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
func (a *API) latest(w http.ResponseWriter, r *http.Request) {
|
||||
if _, _, err := a.user(r); err != nil {
|
||||
@@ -510,7 +673,7 @@ func (a *API) latest(w http.ResponseWriter, r *http.Request) {
|
||||
fail(w, 502, "npm version lookup failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"version": v, "checked_at": time.Now().UTC().Format(time.RFC3339)})
|
||||
writeJSON(w, map[string]any{"version": v, "checked_at": time.Now().UTC().Format(time.RFC3339), "supported": normalBuildAllowed(v), "build_allowed": normalBuildAllowed(v), "admission": defaultAdmission(v, imageRecipeRev, imagePlatform), "admission_reason": "normal builds require a built-in tested tuple; unknown versions require administrator experimental validation and admission"})
|
||||
}
|
||||
func fetchLatest(ctx context.Context, client *http.Client) (string, error) {
|
||||
return fetchNPMVersion(ctx, client, "latest")
|
||||
|
||||
Reference in New Issue
Block a user