add scan on push to quota

This commit is contained in:
Evan Jarrett
2026-02-20 15:17:18 -06:00
parent 76383ec764
commit 33548ecf32
5 changed files with 161 additions and 28 deletions
+2 -2
View File
@@ -249,8 +249,8 @@ func ExampleYAML() ([]byte, error) {
cfg.Quota = quota.Config{
Tiers: map[string]quota.TierConfig{
"deckhand": {Quota: "5GB"},
"bosun": {Quota: "50GB"},
"quartermaster": {Quota: "100GB"},
"bosun": {Quota: "50GB", ScanOnPush: true},
"quartermaster": {Quota: "100GB", ScanOnPush: true},
},
Defaults: quota.DefaultsConfig{
NewCrewTier: "deckhand",
+33 -19
View File
@@ -387,28 +387,42 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
tier = stats.Tier
}
configJSON, _ := json.Marshal(req.Manifest.Config)
layersJSON, _ := json.Marshal(req.Manifest.Layers)
// Check if this tier gets scan-on-push.
// Captain ("owner") always gets scan-on-push.
// When quotas are disabled, all pushes trigger scans (backwards compat).
shouldScan := tier == "owner" ||
h.quotaMgr == nil || !h.quotaMgr.IsEnabled() ||
h.quotaMgr.ScanOnPush(tier)
// Resolve handle for scanner context
_, userHandle, _, resolveErr := atproto.ResolveIdentity(ctx, req.UserDID)
if resolveErr != nil {
userHandle = req.UserDID
}
if shouldScan {
configJSON, _ := json.Marshal(req.Manifest.Config)
layersJSON, _ := json.Marshal(req.Manifest.Layers)
if err := h.scanBroadcaster.Enqueue(&pds.ScanJobEvent{
ManifestDigest: req.ManifestDigest,
Repository: req.Repository,
Tag: req.Tag,
UserDID: req.UserDID,
UserHandle: userHandle,
Tier: tier,
Config: configJSON,
Layers: layersJSON,
}); err != nil {
slog.Error("Failed to enqueue scan job",
// Resolve handle for scanner context
_, userHandle, _, resolveErr := atproto.ResolveIdentity(ctx, req.UserDID)
if resolveErr != nil {
userHandle = req.UserDID
}
if err := h.scanBroadcaster.Enqueue(&pds.ScanJobEvent{
ManifestDigest: req.ManifestDigest,
Repository: req.Repository,
Tag: req.Tag,
UserDID: req.UserDID,
UserHandle: userHandle,
Tier: tier,
Config: configJSON,
Layers: layersJSON,
}); err != nil {
slog.Error("Failed to enqueue scan job",
"repository", req.Repository,
"error", err)
}
} else {
slog.Debug("Scan-on-push skipped for tier",
"tier", tier,
"repository", req.Repository,
"error", err)
"userDid", req.UserDID)
}
}
}
-7
View File
@@ -598,13 +598,6 @@ func (sb *ScanBroadcaster) handleError(sub *ScanSubscriber, msg ScannerMessage)
"error", msg.Error)
}
func truncateError(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen]
}
// drainPendingJobs sends pending/timed-out jobs to a newly connected scanner.
// Collects all pending rows first, closes cursor, then assigns and dispatches
// to avoid holding a SELECT cursor open during UPDATEs (prevents SQLite BUSY).
+30
View File
@@ -24,6 +24,9 @@ type Config struct {
type TierConfig struct {
// Human-readable size limit, e.g. "5GB", "50GB", "1TB".
Quota string `yaml:"quota" comment:"Storage quota limit (e.g. \"5GB\", \"50GB\", \"1TB\")."`
// Whether pushing triggers an immediate vulnerability scan.
ScanOnPush bool `yaml:"scan_on_push" comment:"Trigger vulnerability scan immediately on push. When false, images are still scanned by background scheduling."`
}
// DefaultsConfig represents default settings.
@@ -165,6 +168,33 @@ func (m *Manager) GetDefaultTier() string {
return m.config.Defaults.NewCrewTier
}
// ScanOnPush returns whether scan-on-push is enabled for a tier.
// Follows the same fallback logic as GetTierLimit:
// 1. If quotas disabled → false (caller decides default)
// 2. If tierKey provided and found → that tier's ScanOnPush
// 3. If tierKey not found or empty → use defaults.new_crew_tier
// 4. If default tier not found → false
func (m *Manager) ScanOnPush(tierKey string) bool {
if !m.IsEnabled() {
return false
}
if tierKey != "" {
if tier, ok := m.config.Tiers[tierKey]; ok {
return tier.ScanOnPush
}
}
// Fall back to default tier
if m.config.Defaults.NewCrewTier != "" {
if tier, ok := m.config.Tiers[m.config.Defaults.NewCrewTier]; ok {
return tier.ScanOnPush
}
}
return false
}
// TierCount returns the number of configured tiers
func (m *Manager) TierCount() int {
return len(m.tiers)
+96
View File
@@ -294,6 +294,102 @@ defaults:
}
}
func TestScanOnPush_Disabled(t *testing.T) {
// Quotas disabled → ScanOnPush returns false
m, err := NewManagerFromConfig(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if m.ScanOnPush("bosun") {
t.Error("expected false when quotas disabled")
}
}
func TestScanOnPush_ExplicitTier(t *testing.T) {
cfg := &Config{
Tiers: map[string]TierConfig{
"deckhand": {Quota: "5GB", ScanOnPush: false},
"bosun": {Quota: "50GB", ScanOnPush: true},
"quartermaster": {Quota: "100GB", ScanOnPush: true},
},
Defaults: DefaultsConfig{NewCrewTier: "deckhand"},
}
m, err := NewManagerFromConfig(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if m.ScanOnPush("deckhand") {
t.Error("expected false for deckhand")
}
if !m.ScanOnPush("bosun") {
t.Error("expected true for bosun")
}
if !m.ScanOnPush("quartermaster") {
t.Error("expected true for quartermaster")
}
}
func TestScanOnPush_FallbackToDefault(t *testing.T) {
cfg := &Config{
Tiers: map[string]TierConfig{
"deckhand": {Quota: "5GB", ScanOnPush: false},
"bosun": {Quota: "50GB", ScanOnPush: true},
},
Defaults: DefaultsConfig{NewCrewTier: "deckhand"},
}
m, err := NewManagerFromConfig(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Unknown tier falls back to default (deckhand) which is false
if m.ScanOnPush("unknown") {
t.Error("expected false for unknown tier (fallback to deckhand)")
}
// Empty tier also falls back
if m.ScanOnPush("") {
t.Error("expected false for empty tier (fallback to deckhand)")
}
}
func TestScanOnPush_FallbackToDefaultTrue(t *testing.T) {
cfg := &Config{
Tiers: map[string]TierConfig{
"deckhand": {Quota: "5GB", ScanOnPush: true},
},
Defaults: DefaultsConfig{NewCrewTier: "deckhand"},
}
m, err := NewManagerFromConfig(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Unknown tier falls back to default (deckhand) which is true
if !m.ScanOnPush("unknown") {
t.Error("expected true for unknown tier (fallback to deckhand with scan_on_push: true)")
}
}
func TestScanOnPush_ZeroValue(t *testing.T) {
// When scan_on_push is omitted from config, Go zero value = false
cfg := &Config{
Tiers: map[string]TierConfig{
"deckhand": {Quota: "5GB"}, // ScanOnPush not set
},
Defaults: DefaultsConfig{NewCrewTier: "deckhand"},
}
m, err := NewManagerFromConfig(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if m.ScanOnPush("deckhand") {
t.Error("expected false when scan_on_push is omitted (zero value)")
}
}
func TestNewManager_NoDefaultTier(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "quotas.yaml")