mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 16:54:15 +00:00
more ui fixes and scanner fixes
This commit is contained in:
@@ -31,8 +31,8 @@ func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
db.SetRegistryURL(featuredCards, h.RegistryURL)
|
||||
|
||||
// Fetch recently updated repositories (top 18 by last push - 6 rows)
|
||||
recentCards, err := db.GetRepoCards(h.ReadOnlyDB, 18, currentUserDID, db.SortByLastUpdate)
|
||||
// Fetch recently updated repositories (top 24 by last push - 6 rows at 4-col xl)
|
||||
recentCards, err := db.GetRepoCards(h.ReadOnlyDB, 24, currentUserDID, db.SortByLastUpdate)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching recent repos: %v", err)
|
||||
recentCards = []db.RepoCardData{}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<symbol id="loader" viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></symbol>
|
||||
<symbol id="loader-2" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></symbol>
|
||||
<symbol id="moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></symbol>
|
||||
<symbol id="package" viewBox="0 0 24 24"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><polyline points="3.29 7 12 12 20.71 7"/><path d="m7.5 4.27 9 5.15"/></symbol>
|
||||
<symbol id="pencil" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></symbol>
|
||||
<symbol id="plus" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></symbol>
|
||||
<symbol id="radio-tower" viewBox="0 0 24 24"><path d="M4.9 16.1C1 12.2 1 5.8 4.9 1.9"/><path d="M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"/><circle cx="12" cy="9" r="2"/><path d="M16.2 4.8c2 2 2.26 5.11.8 7.47"/><path d="M19.1 1.9a9.96 9.96 0 0 1 0 14.1"/><path d="M9.5 18h5"/><path d="m8 22 4-11 4 11"/></symbol>
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
@@ -15,7 +15,7 @@
|
||||
- .HasMore: bool - whether to show Load More button
|
||||
*/}}
|
||||
{{ if .Repositories }}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3{{ if eq .Columns 4 }} xl:grid-cols-4{{ end }} gap-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3{{ if eq .Columns 4 }} xl:grid-cols-4{{ end }} gap-4">
|
||||
{{ range .Repositories }}
|
||||
{{ template "repo-card" . }}
|
||||
{{ end }}
|
||||
|
||||
@@ -13,3 +13,22 @@
|
||||
</button>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "image-ref" }}
|
||||
{{/*
|
||||
Image reference component - shows a short image reference with a copy button
|
||||
that copies the full pull command. Used in dense card layouts where the full
|
||||
command would truncate.
|
||||
|
||||
Expects: dict with
|
||||
- Display: string - short form shown in the UI (e.g. "alice.bsky.social/myapp:v1.2.3")
|
||||
- Copy: string - full command copied to clipboard (e.g. "docker pull atcr.io/alice.bsky.social/myapp:v1.2.3")
|
||||
*/}}
|
||||
<div class="cmd group !w-full">
|
||||
{{ icon "package" "size-4 shrink-0 text-base-content/60" }}
|
||||
<code class="flex-1">{{ .Display }}</code>
|
||||
<button class="btn btn-ghost btn-xs shrink-0 sm:opacity-0 sm:group-hover:opacity-100 focus:opacity-100 transition-opacity" data-cmd="{{ .Copy }}" aria-label="Copy pull command to clipboard">
|
||||
{{ icon "copy" "size-4" }}
|
||||
</button>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- LastUpdated: time.Time (optional) - Last push time
|
||||
- RegistryURL: string - Registry URL for docker commands (e.g., "atcr.io")
|
||||
*/}}
|
||||
<div class="card card-interactive bg-base-200 border-2 border-base-300 p-6 flex flex-col justify-between min-h-60 w-full" data-href="/r/{{ .OwnerHandle }}/{{ .Repository }}">
|
||||
<div class="card card-interactive bg-base-200 border-2 border-base-300 p-4 flex flex-col justify-between min-h-60 w-full" data-href="/r/{{ .OwnerHandle }}/{{ .Repository }}">
|
||||
<div class="flex gap-4 items-start">
|
||||
{{ if .IconURL }}
|
||||
<img src="{{ resizeImage .IconURL 96 }}" alt="{{ .Repository }}" loading="lazy" width="48" height="48" class="w-12 rounded-lg object-cover shrink-0">
|
||||
@@ -47,19 +47,27 @@
|
||||
<div class="flex-1 flex flex-col justify-end py-2 min-w-0">
|
||||
{{ if eq .ArtifactType "helm-chart" }}
|
||||
{{ if .Tag }}
|
||||
{{ template "docker-command" (printf "helm pull oci://%s/%s/%s --version %s" .RegistryURL .OwnerHandle .Repository .Tag) }}
|
||||
{{ template "image-ref" (dict
|
||||
"Display" (printf "%s/%s:%s" .OwnerHandle .Repository .Tag)
|
||||
"Copy" (printf "helm pull oci://%s/%s/%s --version %s" .RegistryURL .OwnerHandle .Repository .Tag)) }}
|
||||
{{ else }}
|
||||
{{ template "docker-command" (printf "helm pull oci://%s/%s/%s" .RegistryURL .OwnerHandle .Repository) }}
|
||||
{{ template "image-ref" (dict
|
||||
"Display" (printf "%s/%s" .OwnerHandle .Repository)
|
||||
"Copy" (printf "helm pull oci://%s/%s/%s" .RegistryURL .OwnerHandle .Repository)) }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ if .Tag }}
|
||||
{{ template "docker-command" (printf "%s pull %s/%s/%s:%s" (ociClientName .OciClient) .RegistryURL .OwnerHandle .Repository .Tag) }}
|
||||
{{ template "image-ref" (dict
|
||||
"Display" (printf "%s/%s:%s" .OwnerHandle .Repository .Tag)
|
||||
"Copy" (printf "%s pull %s/%s/%s:%s" (ociClientName .OciClient) .RegistryURL .OwnerHandle .Repository .Tag)) }}
|
||||
{{ else }}
|
||||
{{ template "docker-command" (printf "%s pull %s/%s/%s" (ociClientName .OciClient) .RegistryURL .OwnerHandle .Repository) }}
|
||||
{{ template "image-ref" (dict
|
||||
"Display" (printf "%s/%s" .OwnerHandle .Repository)
|
||||
"Copy" (printf "%s pull %s/%s/%s" (ociClientName .OciClient) .RegistryURL .OwnerHandle .Repository)) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="flex justify-between items-center pt-3 border-t border-base-300">
|
||||
<div class="flex justify-between items-center pt-3 -mx-4 px-4 border-t border-base-300">
|
||||
<div class="flex gap-6 items-center">
|
||||
{{ template "star" (dict "IsStarred" .IsStarred "StarCount" .StarCount) }}
|
||||
{{ template "pull-count" (dict "PullCount" .PullCount) }}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{{ if .RecentRepos }}
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-6">What's New</h2>
|
||||
{{ template "card-grid" (dict "Repositories" .RecentRepos) }}
|
||||
{{ template "card-grid" (dict "Repositories" .RecentRepos "Columns" 4) }}
|
||||
</section>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="w-full">
|
||||
{{ template "card-grid" (dict "Repositories" .Repositories "EmptyMessage" "No images yet.") }}
|
||||
{{ template "card-grid" (dict "Repositories" .Repositories "Columns" 4 "EmptyMessage" "No images yet.") }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{{/* Search results partial - renders repository cards in a grid */}}
|
||||
{{ template "card-grid" (dict
|
||||
"Repositories" .Repositories
|
||||
"Columns" 4
|
||||
"EmptyIcon" "search-x"
|
||||
"EmptyMessage" "No repositories found matching your search."
|
||||
"EmptySubtext" "Try a different search term or browse the homepage."
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<symbol id="loader" viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></symbol>
|
||||
<symbol id="loader-2" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></symbol>
|
||||
<symbol id="moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></symbol>
|
||||
<symbol id="package" viewBox="0 0 24 24"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><polyline points="3.29 7 12 12 20.71 7"/><path d="m7.5 4.27 9 5.15"/></symbol>
|
||||
<symbol id="pencil" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></symbol>
|
||||
<symbol id="plus" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></symbol>
|
||||
<symbol id="radio-tower" viewBox="0 0 24 24"><path d="M4.9 16.1C1 12.2 1 5.8 4.9 1.9"/><path d="M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"/><circle cx="12" cy="9" r="2"/><path d="M16.2 4.8c2 2 2.26 5.11.8 7.47"/><path d="M19.1 1.9a9.96 9.96 0 0 1 0 14.1"/><path d="M9.5 18h5"/><path d="m8 22 4-11 4 11"/></symbol>
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -126,9 +125,23 @@ func scanVulnerabilities(ctx context.Context, s *sbom.SBOM, vulnDBPath string) (
|
||||
return reportJSON, digest, summary, nil
|
||||
}
|
||||
|
||||
// grypeDBConfig returns the distribution and installation configs used for
|
||||
// all Grype DB load/update calls. Kept in one place so both the initial load
|
||||
// and the periodic reload see identical settings.
|
||||
func grypeDBConfig(vulnDBPath string) (distribution.Config, installation.Config) {
|
||||
return distribution.DefaultConfig(), installation.Config{
|
||||
DBRootDir: vulnDBPath,
|
||||
ValidateAge: true,
|
||||
ValidateChecksum: true,
|
||||
MaxAllowedBuiltAge: 14 * 24 * time.Hour, // tolerates upstream publish gaps
|
||||
}
|
||||
}
|
||||
|
||||
// loadVulnDatabase loads the Grype vulnerability database with caching and
|
||||
// automatic refresh. The cached DB is returned if loaded less than
|
||||
// vulnDBRefreshAge ago. On a stale or missing DB, it downloads a fresh copy.
|
||||
// vulnDBRefreshAge ago. On a stale or missing DB, Grype downloads a fresh copy
|
||||
// in the same call (update=true) — a single curator handles everything so
|
||||
// there is no chance of a double-curator update+load seeing different state.
|
||||
func loadVulnDatabase(ctx context.Context, vulnDBPath string) (vulnerability.Provider, error) {
|
||||
vulnDBLock.RLock()
|
||||
if vulnDB != nil && time.Since(vulnDBLoaded) < vulnDBRefreshAge {
|
||||
@@ -155,40 +168,32 @@ func loadVulnDatabase(ctx context.Context, vulnDBPath string) (vulnerability.Pro
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("Loading Grype vulnerability database", "path", vulnDBPath)
|
||||
slog.Info("Loading Grype vulnerability database", "path", vulnDBPath, "tmpdir", os.Getenv("TMPDIR"))
|
||||
|
||||
if err := os.MkdirAll(vulnDBPath, 0755); err != nil {
|
||||
if err := os.MkdirAll(vulnDBPath, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create vulnerability database directory: %w", err)
|
||||
}
|
||||
|
||||
distConfig := distribution.DefaultConfig()
|
||||
installConfig := installation.Config{
|
||||
DBRootDir: vulnDBPath,
|
||||
ValidateAge: true,
|
||||
ValidateChecksum: true,
|
||||
MaxAllowedBuiltAge: 14 * 24 * time.Hour, // 2 weeks — tolerates upstream publish gaps
|
||||
}
|
||||
distConfig, installConfig := grypeDBConfig(vulnDBPath)
|
||||
|
||||
// Try loading existing DB first (no network)
|
||||
store, status, err := grype.LoadVulnerabilityDB(distConfig, installConfig, false)
|
||||
// update=true: a single grype curator checks the upstream feed, downloads
|
||||
// if needed, activates, and then opens the reader — all in one call. If
|
||||
// the upstream is unreachable but the on-disk DB is still valid, it falls
|
||||
// back to serving the existing DB.
|
||||
store, status, err := grype.LoadVulnerabilityDB(distConfig, installConfig, true)
|
||||
if err != nil {
|
||||
slog.Warn("Vulnerability database load failed, attempting update", "error", err)
|
||||
|
||||
// Download fresh DB
|
||||
if updateErr := updateVulnDatabase(vulnDBPath); updateErr != nil {
|
||||
return nil, fmt.Errorf("failed to update vulnerability database: %w (original: %w)", updateErr, err)
|
||||
}
|
||||
|
||||
// Retry loading after update
|
||||
store, status, err = grype.LoadVulnerabilityDB(distConfig, installConfig, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load vulnerability database after update (status=%v): %w", status, err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to load vulnerability database: %w", err)
|
||||
}
|
||||
|
||||
age := "unknown"
|
||||
if !status.Built.IsZero() {
|
||||
age = time.Since(status.Built).Round(time.Minute).String()
|
||||
}
|
||||
slog.Info("Vulnerability database loaded",
|
||||
"built", status.Built,
|
||||
"schemaVersion", status.SchemaVersion)
|
||||
"age", age,
|
||||
"schemaVersion", status.SchemaVersion,
|
||||
"path", status.Path)
|
||||
|
||||
if vulnDB != nil {
|
||||
vulnDB.Close()
|
||||
@@ -198,67 +203,13 @@ func loadVulnDatabase(ctx context.Context, vulnDBPath string) (vulnerability.Pro
|
||||
return vulnDB, nil
|
||||
}
|
||||
|
||||
// initializeVulnDatabase ensures a fresh vulnerability database exists on startup.
|
||||
func initializeVulnDatabase(vulnDBPath, tmpDir string) error {
|
||||
// initializeVulnDatabase primes the in-memory DB cache on startup so the first
|
||||
// scan doesn't pay the download cost. Caller is responsible for TMPDIR being
|
||||
// set to a path on the same filesystem as vulnDBPath — see WorkerPool.Start.
|
||||
func initializeVulnDatabase(vulnDBPath string) error {
|
||||
slog.Info("Initializing vulnerability database", "path", vulnDBPath)
|
||||
|
||||
grpeTmpDir := filepath.Join(tmpDir, "grype-dl")
|
||||
if err := os.MkdirAll(grpeTmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
oldTmpDir := os.Getenv("TMPDIR")
|
||||
os.Setenv("TMPDIR", grpeTmpDir)
|
||||
defer func() {
|
||||
if oldTmpDir != "" {
|
||||
os.Setenv("TMPDIR", oldTmpDir)
|
||||
} else {
|
||||
os.Unsetenv("TMPDIR")
|
||||
}
|
||||
}()
|
||||
|
||||
return updateVulnDatabase(vulnDBPath)
|
||||
}
|
||||
|
||||
// updateVulnDatabase downloads a fresh vulnerability database if needed.
|
||||
// The curator internally checks whether an update is necessary (DB missing,
|
||||
// stale, or update-check cooldown expired) so this is safe to call often.
|
||||
func updateVulnDatabase(vulnDBPath string) error {
|
||||
if err := os.MkdirAll(vulnDBPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create database directory: %w", err)
|
||||
}
|
||||
|
||||
distConfig := distribution.DefaultConfig()
|
||||
installConfig := installation.Config{
|
||||
DBRootDir: vulnDBPath,
|
||||
ValidateAge: true,
|
||||
ValidateChecksum: true,
|
||||
MaxAllowedBuiltAge: 14 * 24 * time.Hour,
|
||||
}
|
||||
|
||||
downloader, err := distribution.NewClient(distConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create database downloader: %w", err)
|
||||
}
|
||||
|
||||
curator, err := installation.NewCurator(installConfig, downloader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create database curator: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("Checking vulnerability database for updates...")
|
||||
updated, err := curator.Update()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update vulnerability database: %w", err)
|
||||
}
|
||||
|
||||
if updated {
|
||||
slog.Info("Vulnerability database updated successfully")
|
||||
} else {
|
||||
slog.Info("Vulnerability database is up to date")
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err := loadVulnDatabase(context.Background(), vulnDBPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func countVulnerabilitiesBySeverity(matches match.Matches) scanner.VulnerabilitySummary {
|
||||
|
||||
@@ -37,20 +37,29 @@ func NewWorkerPool(cfg *config.Config, q *queue.JobQueue, c *client.HoldClient)
|
||||
|
||||
// Start launches worker goroutines
|
||||
func (wp *WorkerPool) Start(ctx context.Context) {
|
||||
// Point TMPDIR at the configured tmp dir so Grype's DB download
|
||||
// (go-getter zstd decompression can be 1 GB+) and stereoscope's layer
|
||||
// extraction both land on the same partition as the scanner volume —
|
||||
// NOT on /tmp, which is typically tmpfs with ~400 MB and would silently
|
||||
// fail mid-extract. This must be set before any scanner/grype goroutine
|
||||
// starts and must never be restored to a smaller default mid-process.
|
||||
if wp.cfg.Vuln.TmpDir != "" {
|
||||
if err := os.MkdirAll(wp.cfg.Vuln.TmpDir, 0o755); err != nil {
|
||||
slog.Warn("Failed to create scanner tmp dir", "path", wp.cfg.Vuln.TmpDir, "error", err)
|
||||
}
|
||||
os.Setenv("TMPDIR", wp.cfg.Vuln.TmpDir)
|
||||
}
|
||||
|
||||
// Initialize vuln database on startup if enabled
|
||||
if wp.cfg.Vuln.Enabled {
|
||||
go func() {
|
||||
if err := initializeVulnDatabase(wp.cfg.Vuln.DBPath, wp.cfg.Vuln.TmpDir); err != nil {
|
||||
if err := initializeVulnDatabase(wp.cfg.Vuln.DBPath); err != nil {
|
||||
slog.Error("Failed to initialize vulnerability database", "error", err)
|
||||
slog.Warn("Vulnerability scanning will be disabled until database is available")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Point TMPDIR at the configured tmp dir so stereoscope's internal
|
||||
// layer extraction uses the same partition (not /tmp which may be small)
|
||||
os.Setenv("TMPDIR", wp.cfg.Vuln.TmpDir)
|
||||
|
||||
for i := 0; i < wp.cfg.Scanner.Workers; i++ {
|
||||
wp.wg.Add(1)
|
||||
go wp.worker(ctx, i)
|
||||
|
||||
Reference in New Issue
Block a user