From 51743c68408f976f81d451359d9703269a3ee487 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 18 Oct 2026 12:39:21 +0100 Subject: [PATCH 1/2] Use shared cooldown package-pattern policy --- go.mod | 2 +- go.sum | 4 +- internal/cooldownpolicy/policy.go | 114 ----------- internal/cooldownpolicy/policy_test.go | 223 --------------------- internal/handler/cooldown_patterns_test.go | 5 +- internal/server/server.go | 3 +- 6 files changed, 6 insertions(+), 345 deletions(-) delete mode 100644 internal/cooldownpolicy/policy.go delete mode 100644 internal/cooldownpolicy/policy_test.go diff --git a/go.mod b/go.mod index 670f884e..1d882982 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ecr v1.66.2 github.com/git-pkgs/archives v0.8.1 github.com/git-pkgs/artifacts v0.2.1 - github.com/git-pkgs/cooldown v0.2.0 + github.com/git-pkgs/cooldown v0.2.1-0.20261018113502-618a58a2fe21 github.com/git-pkgs/enrichment v0.7.2 github.com/git-pkgs/gcs v0.1.0 github.com/git-pkgs/integrity v0.2.0 diff --git a/go.sum b/go.sum index d2ed2447..0a9a621b 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/git-pkgs/archives v0.8.1 h1:IcT1bVsAec8SqJ4m9vTnD644KiadQZ4ERrWnm/30t github.com/git-pkgs/archives v0.8.1/go.mod h1:gIyPXPQr5e+YrMqA5KQ7x6pLrFXHNF1+CjdedNJdGUU= github.com/git-pkgs/artifacts v0.2.1 h1:VwdxR4yTDaqBZ34h0slxQBVyr2Xfa+QGOKCKviMx2xk= github.com/git-pkgs/artifacts v0.2.1/go.mod h1:Otosgq52pXT5UNN7lh6s/lszpWKVDO8XrOjN1M70/IA= -github.com/git-pkgs/cooldown v0.2.0 h1:0MWPHtkzZgvCR0wdiQeyvMea/dxgw9tParH1zzaFopc= -github.com/git-pkgs/cooldown v0.2.0/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E= +github.com/git-pkgs/cooldown v0.2.1-0.20261018113502-618a58a2fe21 h1:oHoyOzOxsLDFUFCWyB7zG3VqACi7K+8ZcnO/t2vY4eI= +github.com/git-pkgs/cooldown v0.2.1-0.20261018113502-618a58a2fe21/go.mod h1:FnTg8wufEALATC9Sm/9AHjLdA+u9eVn96e9BqXXjGB0= github.com/git-pkgs/enrichment v0.7.2 h1:flD/M89HxkOegUi6tC8EOspFiIMsl7m0o576IQKOp48= github.com/git-pkgs/enrichment v0.7.2/go.mod h1:xGyc/iz7u1UnbrOHk8lIX5ZaNF3mKFDEZc3VqBJ/LOk= github.com/git-pkgs/gcs v0.1.0 h1:E3awGtsO0xZyHT9FUfEwMHjMkRxw41Bh+c7lgTmPvBo= diff --git a/internal/cooldownpolicy/policy.go b/internal/cooldownpolicy/policy.go deleted file mode 100644 index 096c4a4a..00000000 --- a/internal/cooldownpolicy/policy.go +++ /dev/null @@ -1,114 +0,0 @@ -// Package cooldownpolicy applies package-pattern overrides to cooldown checks. -package cooldownpolicy - -import ( - "fmt" - "path" - "sort" - "strings" - "time" - - "github.com/git-pkgs/cooldown" -) - -// Policy applies exact PURL overrides before package-pattern overrides. -type Policy struct { - base *cooldown.Config - patterns []pattern - enabled bool -} - -type pattern struct { - glob string - duration time.Duration - config *cooldown.Config -} - -// New creates a Policy using the supplied exact and pattern overrides. -func New(base *cooldown.Config, packagePatterns map[string]string) (*Policy, error) { - if base == nil { - base = &cooldown.Config{} - } - - keys := make([]string, 0, len(packagePatterns)) - for glob := range packagePatterns { - keys = append(keys, glob) - } - sort.Strings(keys) - patterns := make([]pattern, 0, len(packagePatterns)) - seen := make(map[string]pattern) - enabled := base.Enabled() - for _, glob := range keys { - value := packagePatterns[glob] - if strings.ContainsAny(glob, "[\\") { - return nil, fmt.Errorf("invalid cooldown package pattern %q: character classes and escapes are not supported", glob) - } - canonicalGlob := strings.ReplaceAll(glob, "@", "%40") - if _, err := path.Match(canonicalGlob, ""); err != nil { - return nil, fmt.Errorf("invalid cooldown package pattern %q: %w", glob, err) - } - duration, err := cooldown.ParseDuration(value) - if err != nil { - return nil, fmt.Errorf("invalid cooldown duration for package pattern %q: %w", glob, err) - } - if previous, exists := seen[canonicalGlob]; exists { - if previous.duration != duration { - return nil, fmt.Errorf("conflicting cooldown package patterns %q and %q", previous.glob, glob) - } - continue - } - seen[canonicalGlob] = pattern{glob: glob, duration: duration} - config := &cooldown.Config{Default: value} - enabled = config.Enabled() || enabled - patterns = append(patterns, pattern{glob: canonicalGlob, duration: duration, config: config}) - } - sort.Slice(patterns, func(i, j int) bool { - left, right := literalLength(patterns[i].glob), literalLength(patterns[j].glob) - if left != right { - return left > right - } - return patterns[i].glob < patterns[j].glob - }) - - return &Policy{base: base, patterns: patterns, enabled: enabled}, nil -} - -func literalLength(glob string) int { - return len(glob) - strings.Count(glob, "*") - strings.Count(glob, "?") -} - -// For returns the duration, with exact overrides taking precedence over patterns. -func (p *Policy) For(ecosystem, packagePURL string) time.Duration { - return p.configFor(packagePURL).For(ecosystem, packagePURL) -} - -func (p *Policy) configFor(packagePURL string) *cooldown.Config { - if _, exact := p.base.Packages[packagePURL]; exact { - return p.base - } - - for _, candidate := range p.patterns { - matched, _ := path.Match(candidate.glob, packagePURL) - if !matched { - continue - } - return candidate.config - } - - return p.base -} - -// IsAllowed reports whether the package version has completed its cooldown. -func (p *Policy) IsAllowed(ecosystem, packagePURL string, publishedAt time.Time) bool { - return p.Evaluate(ecosystem, packagePURL, publishedAt, time.Now()).Allowed -} - -// Evaluate returns the cooldown decision at the supplied evaluation time. -func (p *Policy) Evaluate(ecosystem, packagePURL string, publishedAt, evaluatedAt time.Time) cooldown.Decision { - return p.configFor(packagePURL).Evaluate(ecosystem, packagePURL, publishedAt, evaluatedAt) -} - -// Enabled reports whether any configured cooldown can filter a package version. -func (p *Policy) Enabled() bool { - return p.enabled -} diff --git a/internal/cooldownpolicy/policy_test.go b/internal/cooldownpolicy/policy_test.go deleted file mode 100644 index 9e956c14..00000000 --- a/internal/cooldownpolicy/policy_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package cooldownpolicy - -import ( - "strings" - "testing" - "time" - - "github.com/git-pkgs/cooldown" -) - -func TestPatternOverride(t *testing.T) { - policy, err := New(&cooldown.Config{ - Default: "7d", - Ecosystems: map[string]string{"npm": "7d"}, - }, map[string]string{ - "pkg:npm/@example/*": "0", - }) - if err != nil { - t.Fatalf("New returned error: %v", err) - } - - if !policy.IsAllowed("npm", "pkg:npm/%40example/widget", time.Now()) { - t.Fatal("matching package pattern should disable cooldown") - } - if policy.IsAllowed("npm", "pkg:npm/public-package", time.Now()) { - t.Fatal("non-matching package should use ecosystem cooldown") - } -} - -func TestExactOverrideTakesPrecedenceOverPattern(t *testing.T) { - purl := "pkg:npm/%40example/widget" - policy, err := New(&cooldown.Config{ - Default: "7d", - Packages: map[string]string{purl: "2d"}, - }, map[string]string{ - "pkg:npm/@example/*": "0", - }) - if err != nil { - t.Fatalf("New returned error: %v", err) - } - - if policy.IsAllowed("npm", purl, time.Now()) { - t.Fatal("exact package override should take precedence over pattern") - } -} - -func TestMoreSpecificPatternTakesPrecedence(t *testing.T) { - policy, err := New(&cooldown.Config{Default: "7d"}, map[string]string{ - "pkg:npm/@example/*": "0", - "pkg:npm/@example/critical": "2d", - }) - if err != nil { - t.Fatalf("New returned error: %v", err) - } - - if policy.IsAllowed("npm", "pkg:npm/%40example/critical", time.Now()) { - t.Fatal("more specific pattern should take precedence") - } -} - -func TestNewRejectsInvalidPattern(t *testing.T) { - for _, glob := range []string{"pkg:npm/[", "pkg:npm/[@a]*", "pkg:npm/[abcdef]*", `pkg:npm/\*`, `pkg:npm/\@example/*`} { - t.Run(glob, func(t *testing.T) { - if _, err := New(nil, map[string]string{glob: "0"}); err == nil || !strings.Contains(err.Error(), "character classes and escapes are not supported") { - t.Fatalf("unsupported pattern %q: error = %v", glob, err) - } - }) - } -} - -func TestQuestionMarkPattern(t *testing.T) { - policy, err := New(&cooldown.Config{Default: "7d"}, map[string]string{"pkg:npm/@example/widget-?": "0"}) - if err != nil { - t.Fatal(err) - } - for _, tc := range []struct { - purl string - want time.Duration - }{ - {"pkg:npm/%40example/widget-a", 0}, - {"pkg:npm/%40example/widget-ab", 7 * 24 * time.Hour}, - {"pkg:npm/%40example/widget-/", 7 * 24 * time.Hour}, - } { - if got := policy.For("npm", tc.purl); got != tc.want { - t.Errorf("For(%q) = %s, want %s", tc.purl, got, tc.want) - } - } -} - -func TestNormalizedPatternCollisions(t *testing.T) { - for _, duration := range []string{"0", "24h"} { - t.Run(duration, func(t *testing.T) { - for range 20 { - policy, err := New(nil, map[string]string{ - "pkg:npm/@example/*": "1d", - "pkg:npm/%40example/*": duration, - }) - if duration == "0" { - if err == nil || !strings.Contains(err.Error(), `conflicting cooldown package patterns "pkg:npm/%40example/*" and "pkg:npm/@example/*"`) { - t.Fatalf("conflict error = %v", err) - } - continue - } - if err != nil { - t.Fatal(err) - } - if got := policy.For("npm", "pkg:npm/%40example/widget"); got != 24*time.Hour { - t.Fatalf("duration = %s", got) - } - } - }) - } -} - -func TestForPrecedence(t *testing.T) { - policy, err := New(&cooldown.Config{ - Default: "1h", - Ecosystems: map[string]string{"npm": "2h"}, - Packages: map[string]string{"pkg:npm/%40example/exact": "0"}, - }, map[string]string{ - "pkg:npm/@example/*": "3h", - "pkg:npm/@example/specific*": "4h", - "pkg:npm/@example/ab*": "5h", - "pkg:npm/@example/a*c": "6h", - }) - if err != nil { - t.Fatal(err) - } - for _, tc := range []struct { - ecosystem, purl string - want time.Duration - }{ - {"npm", "pkg:npm/%40example/exact", 0}, - {"npm", "pkg:npm/%40example/widget", 3 * time.Hour}, - {"npm", "pkg:npm/%40example/specific-widget", 4 * time.Hour}, - {"npm", "pkg:npm/%40example/abc", 6 * time.Hour}, - {"npm", "pkg:npm/other", 2 * time.Hour}, - {"npm", "pkg:npm/%40example/nested/package", 2 * time.Hour}, - {"cargo", "pkg:cargo/serde", time.Hour}, - } { - t.Run(tc.purl, func(t *testing.T) { - if got := policy.For(tc.ecosystem, tc.purl); got != tc.want { - t.Errorf("For = %s, want %s", got, tc.want) - } - }) - } -} - -func TestPatternOnlyPolicy(t *testing.T) { - policy, err := New(nil, map[string]string{"pkg:nuget/example.*": "24h"}) - if err != nil { - t.Fatal(err) - } - if !policy.Enabled() { - t.Fatal("pattern-only policy should be enabled") - } - for _, tc := range []struct { - published time.Time - allowed bool - }{ - {time.Now().Add(-time.Hour), false}, - {time.Now().Add(-48 * time.Hour), true}, - {time.Time{}, true}, - } { - if got := policy.IsAllowed("nuget", "pkg:nuget/example.widget", tc.published); got != tc.allowed { - t.Errorf("published=%s: allowed=%t, want %t", tc.published, got, tc.allowed) - } - } - if !policy.IsAllowed("nuget", "pkg:nuget/other", time.Now()) { - t.Error("unmatched package should have no cooldown") - } -} - -func TestDisabledPolicy(t *testing.T) { - for _, patterns := range []map[string]string{nil, {"pkg:npm/@example/*": "0"}} { - policy, err := New(nil, patterns) - if err != nil { - t.Fatal(err) - } - if policy.Enabled() { - t.Error("zero cooldown should be disabled") - } - } -} - -func TestNewRejectsInvalidDuration(t *testing.T) { - if _, err := New(nil, map[string]string{"pkg:npm/*": "invalid"}); err == nil { - t.Fatal("invalid duration accepted") - } -} - -func TestEvaluate(t *testing.T) { - policy, err := New(&cooldown.Config{ - Default: "48h", - Ecosystems: map[string]string{"npm": "72h"}, - Packages: map[string]string{"pkg:npm/%40example/exact": "0"}, - }, map[string]string{"pkg:npm/@example/*": "7d"}) - if err != nil { - t.Fatal(err) - } - published := time.Date(2026, time.October, 1, 0, 0, 0, 0, time.UTC) - eligible := published.Add(7 * 24 * time.Hour) - for _, tc := range []struct { - name, ecosystem, purl string - published, at time.Time - want cooldown.Decision - }{ - {"waiting", "npm", "pkg:npm/%40example/widget", published, eligible.Add(-time.Nanosecond), cooldown.Decision{Cooldown: 7 * 24 * time.Hour, AvailableAt: eligible, Reason: cooldown.ReasonWaiting}}, - {"boundary", "npm", "pkg:npm/%40example/widget", published, eligible, cooldown.Decision{Allowed: true, Cooldown: 7 * 24 * time.Hour, AvailableAt: eligible, Reason: cooldown.ReasonElapsed}}, - {"elapsed", "npm", "pkg:npm/%40example/widget", published, eligible.Add(time.Hour), cooldown.Decision{Allowed: true, Cooldown: 7 * 24 * time.Hour, AvailableAt: eligible, Reason: cooldown.ReasonElapsed}}, - {"unknown publication", "npm", "pkg:npm/%40example/widget", time.Time{}, eligible, cooldown.Decision{Allowed: true, Cooldown: 7 * 24 * time.Hour, Reason: cooldown.ReasonUnknownPublicationTime}}, - {"exact exemption", "npm", "pkg:npm/%40example/exact", published, published, cooldown.Decision{Allowed: true, AvailableAt: published, Reason: cooldown.ReasonDisabled}}, - {"exemption without publication", "npm", "pkg:npm/%40example/exact", time.Time{}, eligible, cooldown.Decision{Allowed: true, Reason: cooldown.ReasonUnknownPublicationTime}}, - {"ecosystem fallback", "npm", "pkg:npm/other", published, published, cooldown.Decision{Cooldown: 72 * time.Hour, AvailableAt: published.Add(72 * time.Hour), Reason: cooldown.ReasonWaiting}}, - {"global fallback", "cargo", "pkg:cargo/serde", published, published, cooldown.Decision{Cooldown: 48 * time.Hour, AvailableAt: published.Add(48 * time.Hour), Reason: cooldown.ReasonWaiting}}, - } { - t.Run(tc.name, func(t *testing.T) { - if got := policy.Evaluate(tc.ecosystem, tc.purl, tc.published, tc.at); got != tc.want { - t.Errorf("Evaluate = %+v, want %+v", got, tc.want) - } - }) - } -} diff --git a/internal/handler/cooldown_patterns_test.go b/internal/handler/cooldown_patterns_test.go index 83034cf8..71fcc235 100644 --- a/internal/handler/cooldown_patterns_test.go +++ b/internal/handler/cooldown_patterns_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/git-pkgs/cooldown" - "github.com/git-pkgs/proxy/internal/cooldownpolicy" ) func TestNuGetCooldownPackagePatterns(t *testing.T) { @@ -26,7 +25,7 @@ func TestNuGetCooldownPackagePatterns(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { p, db, store, fetcher := setupTestProxy(t) - policy, err := cooldownpolicy.New(&cooldown.Config{Default: tc.defaultDuration, Packages: tc.exact}, map[string]string{"pkg:nuget/test*": tc.pattern}) + policy, err := cooldown.NewPolicy(&cooldown.Config{Default: tc.defaultDuration, Packages: tc.exact}, map[string]string{"pkg:nuget/test*": tc.pattern}) if err != nil { t.Fatal(err) } @@ -56,7 +55,7 @@ func TestNuGetCooldownPackagePatterns(t *testing.T) { func TestNPMCooldownPatternExemptionRespectsDenylist(t *testing.T) { p, db, store, fetcher := setupTestProxy(t) var err error - p.Cooldown, err = cooldownpolicy.New(&cooldown.Config{Default: "7d"}, map[string]string{"pkg:npm/@example/*": "0"}) + p.Cooldown, err = cooldown.NewPolicy(&cooldown.Config{Default: "7d"}, map[string]string{"pkg:npm/@example/*": "0"}) if err != nil { t.Fatal(err) } diff --git a/internal/server/server.go b/internal/server/server.go index d9699cd4..815ee0b4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -70,7 +70,6 @@ import ( swaggerdoc "github.com/git-pkgs/proxy/docs/swagger" "github.com/git-pkgs/proxy/internal/accesslog" "github.com/git-pkgs/proxy/internal/config" - "github.com/git-pkgs/proxy/internal/cooldownpolicy" "github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/denylist" "github.com/git-pkgs/proxy/internal/enrichment" @@ -239,7 +238,7 @@ func (s *Server) serve(listener net.Listener) error { proxy := handler.NewProxy(s.db, s.storage, handler.LimitArtifactSize(fetcher, s.cfg.ParseArtifactMaxSize()), resolver, s.logger) proxy.HTTPClient = metadataClient proxy.RateLimits = s.rateLimits - cooldownPolicy, err := cooldownpolicy.New(cd, s.cfg.Cooldown.PackagePatterns) + cooldownPolicy, err := cooldown.NewPolicy(cd, s.cfg.Cooldown.PackagePatterns) if err != nil { return fmt.Errorf("configuring cooldown policy: %w", err) } From 5fe84cd9958931bd790d86930d4003873b10ddc7 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Mon, 19 Oct 2026 08:20:05 +0100 Subject: [PATCH 2/2] Use cooldown v0.3.0 release --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1d882982..b81da1f8 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ecr v1.66.2 github.com/git-pkgs/archives v0.8.1 github.com/git-pkgs/artifacts v0.2.1 - github.com/git-pkgs/cooldown v0.2.1-0.20261018113502-618a58a2fe21 + github.com/git-pkgs/cooldown v0.3.0 github.com/git-pkgs/enrichment v0.7.2 github.com/git-pkgs/gcs v0.1.0 github.com/git-pkgs/integrity v0.2.0 diff --git a/go.sum b/go.sum index 0a9a621b..52eb13a7 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/git-pkgs/archives v0.8.1 h1:IcT1bVsAec8SqJ4m9vTnD644KiadQZ4ERrWnm/30t github.com/git-pkgs/archives v0.8.1/go.mod h1:gIyPXPQr5e+YrMqA5KQ7x6pLrFXHNF1+CjdedNJdGUU= github.com/git-pkgs/artifacts v0.2.1 h1:VwdxR4yTDaqBZ34h0slxQBVyr2Xfa+QGOKCKviMx2xk= github.com/git-pkgs/artifacts v0.2.1/go.mod h1:Otosgq52pXT5UNN7lh6s/lszpWKVDO8XrOjN1M70/IA= -github.com/git-pkgs/cooldown v0.2.1-0.20261018113502-618a58a2fe21 h1:oHoyOzOxsLDFUFCWyB7zG3VqACi7K+8ZcnO/t2vY4eI= -github.com/git-pkgs/cooldown v0.2.1-0.20261018113502-618a58a2fe21/go.mod h1:FnTg8wufEALATC9Sm/9AHjLdA+u9eVn96e9BqXXjGB0= +github.com/git-pkgs/cooldown v0.3.0 h1:eAl2CQFwqXxluoVP9hHmropAf1VoV3gSxLMkN+NEu8A= +github.com/git-pkgs/cooldown v0.3.0/go.mod h1:FnTg8wufEALATC9Sm/9AHjLdA+u9eVn96e9BqXXjGB0= github.com/git-pkgs/enrichment v0.7.2 h1:flD/M89HxkOegUi6tC8EOspFiIMsl7m0o576IQKOp48= github.com/git-pkgs/enrichment v0.7.2/go.mod h1:xGyc/iz7u1UnbrOHk8lIX5ZaNF3mKFDEZc3VqBJ/LOk= github.com/git-pkgs/gcs v0.1.0 h1:E3awGtsO0xZyHT9FUfEwMHjMkRxw41Bh+c7lgTmPvBo=