113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Summary queries never load log bodies; history cannot inflate a page response.
|
|
const buildSummaryColumns = "id,version,status,error,image_id,created_at,requested_by,COALESCE(recipe_rev,''),COALESCE(platform,''),COALESCE(finished_at,''),experimental,release_id,source_repository,source_commit"
|
|
|
|
func scanBuildSummary(row interface{ Scan(...any) error }, b *buildRecord) error {
|
|
return row.Scan(&b.ID, &b.Version, &b.Status, &b.Error, &b.ImageID, &b.Created, &b.RequestedBy, &b.RecipeRev, &b.Platform, &b.Finished, &b.Experimental, &b.ReleaseID, &b.SourceRepository, &b.SourceCommit)
|
|
}
|
|
|
|
func (a *API) listBuilds(w http.ResponseWriter, r *http.Request) {
|
|
limit, offset := 20, 0
|
|
for key, dest := range map[string]*int{"limit": &limit, "offset": &offset} {
|
|
if values, ok := r.URL.Query()[key]; ok {
|
|
if len(values) != 1 {
|
|
fail(w, 400, "invalid pagination")
|
|
return
|
|
}
|
|
value, err := strconv.Atoi(values[0])
|
|
if err != nil || value < 0 || (key == "limit" && (value < 1 || value > 100)) {
|
|
fail(w, 400, "invalid pagination")
|
|
return
|
|
}
|
|
*dest = value
|
|
}
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
|
defer cancel()
|
|
buildQueue.Lock()
|
|
err := a.ensureBuilds()
|
|
buildQueue.Unlock()
|
|
if err != nil {
|
|
fail(w, 500, "database error")
|
|
return
|
|
}
|
|
rows, err := a.s.DB.QueryContext(ctx, "SELECT "+buildSummaryColumns+" FROM image_builds ORDER BY created_at DESC,id DESC LIMIT ? OFFSET ?", limit+1, offset)
|
|
if err != nil {
|
|
fail(w, 500, "database error")
|
|
return
|
|
}
|
|
result := []buildRecord{}
|
|
for rows.Next() {
|
|
var b buildRecord
|
|
if err = scanBuildSummary(rows, &b); err != nil {
|
|
break
|
|
}
|
|
result = append(result, b)
|
|
}
|
|
if err == nil {
|
|
err = rows.Err()
|
|
}
|
|
rows.Close()
|
|
if err != nil {
|
|
fail(w, 500, "database error")
|
|
return
|
|
}
|
|
hasMore := len(result) > limit
|
|
if hasMore {
|
|
result = result[:limit]
|
|
}
|
|
if err := a.reconcileBuilds(ctx, result); err != nil {
|
|
fail(w, 502, "docker image reconciliation failed")
|
|
return
|
|
}
|
|
a.startBuildWorker()
|
|
w.Header().Set("X-Has-More", strconv.FormatBool(hasMore))
|
|
writeJSON(w, result)
|
|
}
|
|
|
|
func (a *API) buildDetail(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
|
|
}
|
|
id := strings.TrimPrefix(r.URL.Path, "/api/builds/")
|
|
if id == "" || len(id) > 128 || strings.Contains(id, "/") {
|
|
fail(w, 404, "build not found")
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
|
defer cancel()
|
|
buildQueue.Lock()
|
|
err := a.ensureBuilds()
|
|
buildQueue.Unlock()
|
|
var b buildRecord
|
|
if err == nil {
|
|
err = a.s.DB.QueryRowContext(ctx, "SELECT "+buildSummaryColumns+",logs FROM image_builds WHERE id=?", id).Scan(&b.ID, &b.Version, &b.Status, &b.Error, &b.ImageID, &b.Created, &b.RequestedBy, &b.RecipeRev, &b.Platform, &b.Finished, &b.Experimental, &b.ReleaseID, &b.SourceRepository, &b.SourceCommit, &b.Logs)
|
|
}
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
fail(w, 404, "build not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
fail(w, 500, "database error")
|
|
return
|
|
}
|
|
b.Logs = sanitizeBuildLog(b.Logs)
|
|
writeJSON(w, b)
|
|
}
|