2026-10-27 13:03:42 +01:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"sort"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/config"
|
2026-10-30 13:54:24 +01:00
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
2026-10-28 16:25:08 +01:00
|
|
|
"github.com/git-pkgs/proxy/internal/handler"
|
2026-10-27 13:03:42 +01:00
|
|
|
"github.com/git-pkgs/proxy/internal/packageurl"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CooldownSummary describes the cooldown configuration on the dashboard. The
|
|
|
|
|
// UI has no auth, so package overrides and patterns are only counted: their
|
|
|
|
|
// names often list internal packages.
|
|
|
|
|
type CooldownSummary struct {
|
|
|
|
|
Enabled bool
|
|
|
|
|
Default string
|
|
|
|
|
Ecosystems []CooldownOverride
|
|
|
|
|
PackageOverrides int
|
|
|
|
|
PackagePatterns int
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-28 16:25:08 +01:00
|
|
|
// CooldownOverride is one ecosystem's cooldown. Unsupported marks an
|
|
|
|
|
// ecosystem the proxy applies no cooldown to, so the setting has no effect.
|
2026-10-27 13:03:42 +01:00
|
|
|
type CooldownOverride struct {
|
2026-10-28 16:25:08 +01:00
|
|
|
Ecosystem string
|
|
|
|
|
Duration string
|
|
|
|
|
Unsupported bool
|
2026-10-27 13:03:42 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-30 13:54:24 +01:00
|
|
|
// PackageVersion is a version on the package page.
|
|
|
|
|
type PackageVersion struct {
|
|
|
|
|
database.Version
|
|
|
|
|
// AvailableAt is when the version leaves cooldown, zero when it is not
|
|
|
|
|
// held back.
|
|
|
|
|
AvailableAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 13:03:42 +01:00
|
|
|
// PackageCooldown is the cooldown that applies to one package, and the kind
|
|
|
|
|
// of rule it comes from.
|
|
|
|
|
type PackageCooldown struct {
|
|
|
|
|
Duration string
|
|
|
|
|
Rule string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cooldownView answers the UI's cooldown questions from the configuration.
|
|
|
|
|
type cooldownView struct {
|
|
|
|
|
cfg config.CooldownConfig
|
|
|
|
|
policy *cooldown.Policy
|
|
|
|
|
packages map[string]string
|
|
|
|
|
// patterns sets every package pattern to the same non-zero duration over
|
|
|
|
|
// a zero default, so a non-zero result means a pattern matched, with the
|
|
|
|
|
// policy's own matching.
|
|
|
|
|
patterns *cooldown.Policy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newCooldownView(cfg config.CooldownConfig, policy *cooldown.Policy) (*cooldownView, error) {
|
|
|
|
|
probe := make(map[string]string, len(cfg.PackagePatterns))
|
|
|
|
|
for glob := range cfg.PackagePatterns {
|
|
|
|
|
probe[glob] = "1h"
|
|
|
|
|
}
|
|
|
|
|
patterns, err := cooldown.NewPolicy(nil, probe)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("configuring cooldown package patterns: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return &cooldownView{cfg: cfg, policy: policy, packages: cfg.NormalizedPackages(), patterns: patterns}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *cooldownView) enabled() bool {
|
|
|
|
|
return v != nil && v.policy != nil && v.policy.Enabled()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// summary returns the dashboard's view of the configuration.
|
|
|
|
|
func (v *cooldownView) summary() CooldownSummary {
|
|
|
|
|
if !v.enabled() {
|
|
|
|
|
return CooldownSummary{}
|
|
|
|
|
}
|
|
|
|
|
s := CooldownSummary{
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Default: formatConfigDuration(v.cfg.Default),
|
|
|
|
|
PackageOverrides: len(v.cfg.Packages),
|
|
|
|
|
PackagePatterns: len(v.cfg.PackagePatterns),
|
|
|
|
|
}
|
|
|
|
|
for eco, d := range v.cfg.Ecosystems {
|
2026-10-28 16:25:08 +01:00
|
|
|
s.Ecosystems = append(s.Ecosystems, CooldownOverride{
|
|
|
|
|
Ecosystem: eco,
|
|
|
|
|
Duration: formatConfigDuration(d),
|
|
|
|
|
Unsupported: !handler.CooldownSupported(eco),
|
|
|
|
|
})
|
2026-10-27 13:03:42 +01:00
|
|
|
}
|
|
|
|
|
sort.Slice(s.Ecosystems, func(i, j int) bool { return s.Ecosystems[i].Ecosystem < s.Ecosystems[j].Ecosystem })
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-28 16:25:08 +01:00
|
|
|
// forPackage returns the cooldown for a package, or nil with cooldown off or
|
|
|
|
|
// for an ecosystem the proxy applies no cooldown to.
|
2026-10-27 13:03:42 +01:00
|
|
|
func (v *cooldownView) forPackage(ecosystem, name string) *PackageCooldown {
|
2026-10-28 16:25:08 +01:00
|
|
|
if !v.enabled() || !handler.CooldownSupported(ecosystem) {
|
2026-10-27 13:03:42 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
purl := packageurl.MakeString(ecosystem, name, "")
|
|
|
|
|
rule := "default"
|
|
|
|
|
if _, ok := v.packages[purl]; ok {
|
|
|
|
|
rule = "package override"
|
|
|
|
|
} else if v.patterns.For(ecosystem, purl) != 0 {
|
|
|
|
|
rule = "package pattern"
|
|
|
|
|
} else if _, ok := v.cfg.Ecosystems[ecosystem]; ok {
|
|
|
|
|
rule = "ecosystem override"
|
|
|
|
|
}
|
|
|
|
|
return &PackageCooldown{Duration: formatCooldown(v.policy.For(ecosystem, purl)), Rule: rule}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-30 13:54:24 +01:00
|
|
|
// availableAt returns when a version published at publishedAt leaves
|
|
|
|
|
// cooldown, or zero when it is not held back at now.
|
|
|
|
|
func (v *cooldownView) availableAt(ecosystem, name string, publishedAt, now time.Time) time.Time {
|
|
|
|
|
if !v.enabled() || !handler.CooldownSupported(ecosystem) || publishedAt.IsZero() {
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
d := v.policy.Evaluate(ecosystem, packageurl.MakeString(ecosystem, name, ""), publishedAt, now)
|
|
|
|
|
if d.Allowed {
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
return d.AvailableAt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// packageVersions returns the versions to list on a package page. A version
|
|
|
|
|
// without an artifact row was only seen in metadata, by a cooldown filter or
|
|
|
|
|
// a download check, and is listed only while it is held back.
|
|
|
|
|
func (v *cooldownView) packageVersions(pkg *database.Package, versions []database.Version, fetched map[string]bool, now time.Time) []PackageVersion {
|
|
|
|
|
rows := make([]PackageVersion, 0, len(versions))
|
|
|
|
|
for _, ver := range versions {
|
|
|
|
|
var at time.Time
|
|
|
|
|
if ver.PublishedAt.Valid {
|
|
|
|
|
at = v.availableAt(pkg.Ecosystem, pkg.Name, ver.PublishedAt.Time, now)
|
|
|
|
|
}
|
|
|
|
|
if at.IsZero() && !fetched[ver.PURL] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
rows = append(rows, PackageVersion{Version: ver, AvailableAt: at})
|
|
|
|
|
}
|
|
|
|
|
return rows
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 13:03:42 +01:00
|
|
|
func formatConfigDuration(s string) string {
|
|
|
|
|
d, err := cooldown.ParseDuration(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
return formatCooldown(d)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-30 13:54:24 +01:00
|
|
|
// cooldownOff is how formatCooldown writes a zero cooldown.
|
|
|
|
|
const cooldownOff = "off"
|
|
|
|
|
|
2026-10-27 13:03:42 +01:00
|
|
|
// formatCooldown writes d as the configuration does: "3d", "12h", "30m",
|
|
|
|
|
// and "off" for zero.
|
|
|
|
|
func formatCooldown(d time.Duration) string {
|
|
|
|
|
const day = 24 * time.Hour
|
|
|
|
|
switch {
|
|
|
|
|
case d <= 0:
|
2026-10-30 13:54:24 +01:00
|
|
|
return cooldownOff
|
2026-10-27 13:03:42 +01:00
|
|
|
case d%day == 0:
|
|
|
|
|
return fmt.Sprintf("%dd", d/day)
|
|
|
|
|
case d%time.Hour == 0:
|
|
|
|
|
return fmt.Sprintf("%dh", d/time.Hour)
|
|
|
|
|
case d%time.Minute == 0:
|
|
|
|
|
return fmt.Sprintf("%dm", d/time.Minute)
|
|
|
|
|
}
|
|
|
|
|
return d.String()
|
|
|
|
|
}
|