diff --git a/api.yaml b/api.yaml index ef14a64..9fe5ffb 100644 --- a/api.yaml +++ b/api.yaml @@ -176,6 +176,17 @@ paths: content: application/rss+xml: + /summary: + get: + summary: Get a summary. + responses: + "200": + description: Summary + content: + application/json: + schema: + $ref: "#/components/schemas/Summary" + components: schemas: ImagePage: @@ -186,25 +197,7 @@ components: items: $ref: "#/components/schemas/Image" summary: - type: object - properties: - images: - description: Total number of images - type: number - outdated: - description: Total number of outdated images - type: number - vulnerable: - description: Total number of vulnerable images - type: number - processing: - description: Total number of unprocessed images - type: number - required: - - images - - outdated - - vulnerable - - processing + $ref: "#/components/schemas/Summary" pagination: $ref: "#/components/schemas/PaginationMetadata" required: @@ -384,3 +377,24 @@ components: type: type: string enum: ["imageUpdated"] + + Summary: + type: object + properties: + images: + description: Total number of images + type: number + outdated: + description: Total number of outdated images + type: number + vulnerable: + description: Total number of vulnerable images + type: number + processing: + description: Total number of unprocessed images + type: number + required: + - images + - outdated + - vulnerable + - processing diff --git a/internal/api/server.go b/internal/api/server.go index 9d1a622..3defd92 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -300,6 +300,14 @@ func NewServer(api *store.Store, hub *events.Hub[store.Event], processQueue chan } }) + s.mux.HandleFunc("GET /api/v1/summary", func(w http.ResponseWriter, r *http.Request) { + ctx, span := httputil.SpanFromRequest(r) + span.SetAttributes(semconv.HTTPRoute("/api/v1/summary")) + + response, err := api.Summary(ctx) + s.handleJSONResponse(w, r, response, err) + }) + return s } diff --git a/internal/store/store.go b/internal/store/store.go index 443a19a..4cb44f1 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -828,80 +828,14 @@ func (s *Store) ListImages(ctx context.Context, options *ListImageOptions) (*mod offset := page * limit - // Total images - res, err := s.db.QueryContext(ctx, `SELECT COUNT(1) FROM images;`) - if err != nil { - return nil, err - } - - if !res.Next() { - return nil, res.Err() - } - - var totalImages int - if err := res.Scan(&totalImages); err != nil { - res.Close() - return nil, err - } - res.Close() - - // Total outdated images - res, err = s.db.QueryContext(ctx, `SELECT COUNT(1) FROM images WHERE latestReference IS NOT NULL AND reference != latestReference;`) - if err != nil { - return nil, err - } - - if !res.Next() { - return nil, res.Err() - } - - var totalOutdatedImages int - if err := res.Scan(&totalOutdatedImages); err != nil { - res.Close() - return nil, err - } - res.Close() - - // Total vulnerable images - res, err = s.db.QueryContext(ctx, `SELECT COUNT(DISTINCT reference) FROM images_vulnerabilities;`) - if err != nil { - return nil, err - } - - if !res.Next() { - return nil, res.Err() - } - - var totalVulnerableImages int - if err := res.Scan(&totalVulnerableImages); err != nil { - res.Close() - return nil, err - } - res.Close() + var result models.ImagePage + result.Images = make([]models.Image, 0) - // Total raw images - res, err = s.db.QueryContext(ctx, `SELECT COUNT(1) FROM raw_images;`) + summary, err := s.Summary(ctx) if err != nil { return nil, err } - - if !res.Next() { - return nil, res.Err() - } - - var totalRawImages int - if err := res.Scan(&totalRawImages); err != nil { - res.Close() - return nil, err - } - res.Close() - - var result models.ImagePage - result.Images = make([]models.Image, 0) - result.Summary.Images = totalImages - result.Summary.Outdated = totalOutdatedImages - result.Summary.Vulnerable = totalVulnerableImages - result.Summary.Processing = totalRawImages - totalImages + result.Summary = *summary orderClause := "" switch sort { @@ -950,7 +884,7 @@ func (s *Store) ListImages(ctx context.Context, options *ListImageOptions) (*mod } else if options.Query != "" { args = append(args, ftsEscape(options.Query)) } - res, err = statement.QueryContext(ctx, args...) + res, err := statement.QueryContext(ctx, args...) statement.Close() if err != nil { return nil, err @@ -1086,6 +1020,83 @@ func (s *Store) DeleteNonPresent(ctx context.Context, references []string) (int6 return rowsAffected, nil } +func (s *Store) Summary(ctx context.Context) (*models.ImagePageSummary, error) { + // Total images + res, err := s.db.QueryContext(ctx, `SELECT COUNT(1) FROM images;`) + if err != nil { + return nil, err + } + + if !res.Next() { + return nil, res.Err() + } + + var totalImages int + if err := res.Scan(&totalImages); err != nil { + res.Close() + return nil, err + } + res.Close() + + // Total outdated images + res, err = s.db.QueryContext(ctx, `SELECT COUNT(1) FROM images WHERE latestReference IS NOT NULL AND reference != latestReference;`) + if err != nil { + return nil, err + } + + if !res.Next() { + return nil, res.Err() + } + + var totalOutdatedImages int + if err := res.Scan(&totalOutdatedImages); err != nil { + res.Close() + return nil, err + } + res.Close() + + // Total vulnerable images + res, err = s.db.QueryContext(ctx, `SELECT COUNT(DISTINCT reference) FROM images_vulnerabilities;`) + if err != nil { + return nil, err + } + + if !res.Next() { + return nil, res.Err() + } + + var totalVulnerableImages int + if err := res.Scan(&totalVulnerableImages); err != nil { + res.Close() + return nil, err + } + res.Close() + + // Total raw images + res, err = s.db.QueryContext(ctx, `SELECT COUNT(1) FROM raw_images;`) + if err != nil { + return nil, err + } + + if !res.Next() { + return nil, res.Err() + } + + var totalRawImages int + if err := res.Scan(&totalRawImages); err != nil { + res.Close() + return nil, err + } + res.Close() + + return &models.ImagePageSummary{ + Images: totalImages, + Outdated: totalOutdatedImages, + Vulnerable: totalVulnerableImages, + Processing: totalRawImages - totalImages, + }, nil +} + func (s *Store) Close() error { return s.db.Close() }