package api import ( "context" "database/sql" "encoding/json" "errors" "io" "net/http" "strings" "sync" "time" ) // Admission writes serialize with new runtime operations, including their Docker side effects. var admissionMu sync.RWMutex var errBuildUnsupported = errors.New("version is not supported for normal builds; administrator experimental build required") func defaultAdmission(version, recipe, platform string) string { if recipe == "7" && platform == "linux/amd64" { switch version { case "0.2.0-rc.1": return "compatible" case "0.2.0-rc.2": return "recommended" } } return "existing_only" } func supportedLayout(recipe string) bool { switch recipe { case "3", "4", "5", "6", "7": return true } return false } func normalBuildAllowed(version string) bool { return positiveAdmission(defaultAdmission(version, imageRecipeRev, imagePlatform)) } func positiveAdmission(state string) bool { return state == "recommended" || state == "compatible" } func (a *API) compatibility(w http.ResponseWriter, r *http.Request) { if _, _, err := a.user(r); err != nil { fail(w, 401, "authentication required") return } if r.Method != http.MethodGet { fail(w, 405, "method not allowed") return } entries := []map[string]any{} for _, v := range []string{"0.2.0-rc.1", "0.2.0-rc.2"} { entries = append(entries, map[string]any{"version": v, "recipe_rev": imageRecipeRev, "platform": imagePlatform, "state": defaultAdmission(v, imageRecipeRev, imagePlatform), "build_allowed": normalBuildAllowed(v), "reason": "only linux/amd64 recipe 7 is built-in; successful exact image verification still required"}) } writeJSON(w, map[string]any{"recipe_rev": imageRecipeRev, "platform": imagePlatform, "entries": entries, "unknown": map[string]any{"state": "existing_only", "build_allowed": false, "reason": "not accepted; administrator experimental build and explicit admission required"}, "experimental_admin_only": true}) } func (a *API) evaluateImage(ctx context.Context, id string) (imageRecord, error) { im := imageRecord{ID: id, ImageID: id, Status: "missing", Admission: "existing_only", AdmissionReason: "local image missing"} actual, err := a.inspectLocalImage(ctx, id) if err != nil || actual == nil { return im, err } im.Version = actual.Config.Labels["dsh-mgr.version"] im.RecipeRev = actual.Config.Labels["dsh-mgr.recipe"] im.Platform = actual.platform() im.Created = actual.Created im.Status = "unverified" im.AdmissionReason = "successful exact image identity verification required" if actual.ID != id || actual.Config.Labels["dsh-mgr.role"] != "runtime" { im.Version = "" return im, nil } var count, experimental int err = a.s.DB.QueryRowContext(ctx, "SELECT count(*),COALESCE(min(experimental),0) FROM image_builds WHERE image_id=? AND version=? AND recipe_rev=? AND platform=? AND status='success'", id, im.Version, im.RecipeRev, im.Platform).Scan(&count, &experimental) if err != nil { return im, err } var state, evidence string err = a.s.DB.QueryRowContext(ctx, "SELECT state,evidence FROM image_admissions WHERE image_id=? AND version=? AND recipe_rev=? AND platform=?", id, im.Version, im.RecipeRev, im.Platform).Scan(&state, &evidence) if err != nil && !errors.Is(err, sql.ErrNoRows) { return im, err } if state == "blocked" { im.Admission = state im.Status = state im.AdmissionReason = evidence return im, nil } if count == 0 { return im, nil } im.Admission = defaultAdmission(im.Version, im.RecipeRev, im.Platform) im.AdmissionReason = "no accepted compatibility evidence for this tuple" if positiveAdmission(im.Admission) { im.AdmissionReason = "built-in exact tuple compatibility" } if experimental != 0 { im.Admission = "existing_only" im.Status = "experimental" im.AdmissionReason = "experimental validation succeeded; administrator acceptance required" } else { im.Status = "existing_only" } if state != "" { im.Admission = state im.AdmissionReason = evidence im.Status = state } if !supportedLayout(im.RecipeRev) || im.Platform != imagePlatform { im.Admission = "existing_only" im.Status = "existing_only" im.AdmissionReason = "historical validation retained; current mount layout or platform not supported" } im.Selectable = positiveAdmission(im.Admission) if im.Selectable { im.Status = "available" } return im, nil } func (a *API) imageAdmission(w http.ResponseWriter, r *http.Request) { u, _, err := a.user(r) if err != nil { fail(w, 401, "authentication required") return } if !u.Admin { fail(w, 403, "administrator required") return } if r.Method != http.MethodPost { fail(w, 405, "method not allowed") return } var in struct { ImageID string `json:"image_id"` State string `json:"state"` Evidence string `json:"evidence"` } dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8192)) dec.DisallowUnknownFields() if dec.Decode(&in) != nil || dec.Decode(new(any)) != io.EOF || !runtimeImageID.MatchString(in.ImageID) || strings.TrimSpace(in.Evidence) == "" || (!positiveAdmission(in.State) && in.State != "existing_only" && in.State != "blocked") { fail(w, 400, "image_id, valid state and evidence required") return } if len(in.Evidence) > 4096 { fail(w, 400, "evidence must not exceed 4096 UTF-8 bytes") return } admissionMu.Lock() defer admissionMu.Unlock() buildQueue.Lock() err = a.ensureBuilds() buildQueue.Unlock() if err != nil { fail(w, 500, "database error") return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() im, err := a.evaluateImage(ctx, in.ImageID) if err != nil { fail(w, 502, "image inspection failed") return } if im.Status == "missing" || im.Version == "" || im.RecipeRev == "" || im.Platform == "" { fail(w, 400, "real runtime image identity required") return } if positiveAdmission(in.State) { var count int err = a.s.DB.QueryRowContext(ctx, "SELECT count(*) FROM image_builds WHERE image_id=? AND version=? AND recipe_rev=? AND platform=? AND status='success'", im.ImageID, im.Version, im.RecipeRev, im.Platform).Scan(&count) if err != nil || count == 0 || !supportedLayout(im.RecipeRev) || im.Platform != imagePlatform { fail(w, 400, "successful exact validation and supported mount layout/platform required") return } } buildQueue.Lock() defer buildQueue.Unlock() tx, err := a.s.DB.BeginTx(ctx, nil) if err != nil { fail(w, 500, "database error") return } defer tx.Rollback() _, err = tx.ExecContext(ctx, "INSERT INTO image_admissions(image_id,version,recipe_rev,platform,state,evidence,admitted_by,admitted_at) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(image_id,version,recipe_rev,platform) DO UPDATE SET state=excluded.state,evidence=excluded.evidence,admitted_by=excluded.admitted_by,admitted_at=excluded.admitted_at", im.ImageID, im.Version, im.RecipeRev, im.Platform, in.State, strings.TrimSpace(in.Evidence), u.ID, time.Now().UTC().Format(time.RFC3339Nano)) if err != nil { fail(w, 500, "database error") return } if _, err = tx.ExecContext(ctx, "INSERT INTO audit(uid,action,target,detail,at) VALUES(?,?,?,?,?)", u.ID, "image.admission", im.ImageID, in.State+": "+in.Evidence, time.Now().Unix()); err != nil { fail(w, 500, "audit persistence failed") return } if err = tx.Commit(); err != nil { fail(w, 500, "database error") return } a.imageDeleteGeneration++ im.Admission = in.State im.AdmissionReason = strings.TrimSpace(in.Evidence) im.Selectable = positiveAdmission(in.State) im.Status = in.State if im.Selectable { im.Status = "available" } writeJSON(w, im) }