mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 04:37:06 +00:00
227 lines
5.3 KiB
Go
227 lines
5.3 KiB
Go
package scanner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
)
|
|
|
|
// Queue manages a pool of workers for scanning container images
|
|
type Queue struct {
|
|
jobs chan *ScanJob
|
|
results chan *ScanResult
|
|
workers int
|
|
wg sync.WaitGroup
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewQueue creates a new scanner queue with the specified number of workers
|
|
func NewQueue(workers int, bufferSize int) *Queue {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &Queue{
|
|
jobs: make(chan *ScanJob, bufferSize),
|
|
results: make(chan *ScanResult, bufferSize),
|
|
workers: workers,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
// Start starts the worker pool
|
|
// The workerFunc is called for each job to perform the actual scanning
|
|
func (q *Queue) Start(workerFunc func(context.Context, *ScanJob) (*ScanResult, error)) {
|
|
slog.Info("Starting scanner worker pool", "workers", q.workers)
|
|
|
|
for i := 0; i < q.workers; i++ {
|
|
q.wg.Add(1)
|
|
go q.worker(i, workerFunc)
|
|
}
|
|
|
|
// Start result handler goroutine
|
|
q.wg.Add(1)
|
|
go q.resultHandler()
|
|
}
|
|
|
|
// worker processes jobs from the queue
|
|
func (q *Queue) worker(id int, workerFunc func(context.Context, *ScanJob) (*ScanResult, error)) {
|
|
defer q.wg.Done()
|
|
|
|
slog.Info("Scanner worker started", "worker_id", id)
|
|
|
|
for {
|
|
select {
|
|
case <-q.ctx.Done():
|
|
slog.Info("Scanner worker shutting down", "worker_id", id)
|
|
return
|
|
|
|
case job, ok := <-q.jobs:
|
|
if !ok {
|
|
slog.Info("Scanner worker: jobs channel closed", "worker_id", id)
|
|
return
|
|
}
|
|
|
|
slog.Info("Scanner worker processing job",
|
|
"worker_id", id,
|
|
"repository", job.Repository,
|
|
"tag", job.Tag,
|
|
"digest", job.ManifestDigest)
|
|
|
|
result, err := workerFunc(q.ctx, job)
|
|
if err != nil {
|
|
slog.Error("Scanner worker failed to process job",
|
|
"worker_id", id,
|
|
"repository", job.Repository,
|
|
"tag", job.Tag,
|
|
"error", err)
|
|
continue
|
|
}
|
|
|
|
// Send result to results channel
|
|
select {
|
|
case q.results <- result:
|
|
slog.Info("Scanner worker completed job",
|
|
"worker_id", id,
|
|
"repository", job.Repository,
|
|
"tag", job.Tag,
|
|
"vulnerabilities", result.Summary.Total)
|
|
case <-q.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// resultHandler processes scan results (for logging and metrics)
|
|
func (q *Queue) resultHandler() {
|
|
defer q.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-q.ctx.Done():
|
|
return
|
|
|
|
case result, ok := <-q.results:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Log the result
|
|
slog.Info("Scan completed",
|
|
"repository", result.Job.Repository,
|
|
"tag", result.Job.Tag,
|
|
"digest", result.Job.ManifestDigest,
|
|
"critical", result.Summary.Critical,
|
|
"high", result.Summary.High,
|
|
"medium", result.Summary.Medium,
|
|
"low", result.Summary.Low,
|
|
"total", result.Summary.Total,
|
|
"scanner", result.ScannerVersion)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enqueue adds a job to the queue
|
|
func (q *Queue) Enqueue(jobAny any) error {
|
|
// Type assert to ScanJob (can be map or struct from HandleNotifyManifest)
|
|
var job *ScanJob
|
|
|
|
switch v := jobAny.(type) {
|
|
case *ScanJob:
|
|
job = v
|
|
case map[string]interface{}:
|
|
// Convert map to ScanJob (from HandleNotifyManifest)
|
|
job = &ScanJob{
|
|
ManifestDigest: v["manifestDigest"].(string),
|
|
Repository: v["repository"].(string),
|
|
Tag: v["tag"].(string),
|
|
UserDID: v["userDID"].(string),
|
|
UserHandle: v["userHandle"].(string),
|
|
}
|
|
|
|
// Parse config blob reference
|
|
if configMap, ok := v["config"].(map[string]interface{}); ok {
|
|
job.Config = atproto.BlobReference{
|
|
Digest: configMap["digest"].(string),
|
|
Size: convertToInt64(configMap["size"]),
|
|
MediaType: configMap["mediaType"].(string),
|
|
}
|
|
}
|
|
|
|
// Parse layers
|
|
if layersSlice, ok := v["layers"].([]interface{}); ok {
|
|
slog.Info("Parsing layers from scan job",
|
|
"layersFound", len(layersSlice))
|
|
job.Layers = make([]atproto.BlobReference, len(layersSlice))
|
|
for i, layerAny := range layersSlice {
|
|
if layerMap, ok := layerAny.(map[string]interface{}); ok {
|
|
job.Layers[i] = atproto.BlobReference{
|
|
Digest: layerMap["digest"].(string),
|
|
Size: convertToInt64(layerMap["size"]),
|
|
MediaType: layerMap["mediaType"].(string),
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
slog.Warn("No layers found in scan job map",
|
|
"layersType", fmt.Sprintf("%T", v["layers"]),
|
|
"layersValue", v["layers"])
|
|
}
|
|
default:
|
|
return fmt.Errorf("invalid job type: %T", jobAny)
|
|
}
|
|
|
|
select {
|
|
case q.jobs <- job:
|
|
slog.Info("Enqueued scan job",
|
|
"repository", job.Repository,
|
|
"tag", job.Tag,
|
|
"digest", job.ManifestDigest)
|
|
return nil
|
|
case <-q.ctx.Done():
|
|
return q.ctx.Err()
|
|
}
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the queue, waiting for all workers to finish
|
|
func (q *Queue) Shutdown() {
|
|
slog.Info("Shutting down scanner queue")
|
|
|
|
// Close the jobs channel to signal no more jobs
|
|
close(q.jobs)
|
|
|
|
// Wait for all workers to finish
|
|
q.wg.Wait()
|
|
|
|
// Close results channel
|
|
close(q.results)
|
|
|
|
// Cancel context
|
|
q.cancel()
|
|
|
|
slog.Info("Scanner queue shut down complete")
|
|
}
|
|
|
|
// Len returns the number of jobs currently in the queue
|
|
func (q *Queue) Len() int {
|
|
return len(q.jobs)
|
|
}
|
|
|
|
// convertToInt64 converts an interface{} number to int64, handling both float64 and int64
|
|
func convertToInt64(v interface{}) int64 {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return int64(n)
|
|
case int64:
|
|
return n
|
|
case int:
|
|
return int64(n)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|