mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
add scan on push to quota
This commit is contained in:
+2
-2
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user