mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-10 16:19:16 -05:00
158 lines
5.4 KiB
Go
158 lines
5.4 KiB
Go
|
|
package config
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/git-pkgs/cooldown"
|
||
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
||
|
|
"github.com/git-pkgs/purl"
|
||
|
|
)
|
||
|
|
|
||
|
|
// parseDayDuration parses a duration that may use a "d" suffix for days, as
|
||
|
|
// cooldown values do. Day counts that are not finite or exceed
|
||
|
|
// maxDurationDays are rejected, since converting them to a time.Duration
|
||
|
|
// would overflow.
|
||
|
|
func parseDayDuration(s string) (time.Duration, error) {
|
||
|
|
if num, ok := strings.CutSuffix(strings.TrimSpace(s), "d"); ok {
|
||
|
|
days, err := strconv.ParseFloat(num, 64)
|
||
|
|
if err == nil && (math.IsNaN(days) || math.IsInf(days, 0) || math.Abs(days) > maxDurationDays) {
|
||
|
|
return 0, fmt.Errorf("invalid duration %q: day count out of range", s)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cooldown.ParseDuration(s)
|
||
|
|
}
|
||
|
|
|
||
|
|
// parseRetentionDuration parses one retention value. field names the
|
||
|
|
// setting in error messages.
|
||
|
|
func parseRetentionDuration(field, value string) (time.Duration, error) {
|
||
|
|
d, err := parseDayDuration(value)
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid %s: %w", field, err)
|
||
|
|
}
|
||
|
|
if d < 0 {
|
||
|
|
return 0, fmt.Errorf("invalid %s %q: must not be negative", field, value)
|
||
|
|
}
|
||
|
|
return d, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// RetentionRules validates storage.retention against the ecosystems
|
||
|
|
// registered with reg and resolves it. An ecosystem or package that reg
|
||
|
|
// does not support is an error, so a rule is never silently ignored.
|
||
|
|
func (c *Config) RetentionRules(reg *retention.Registry) (retention.Rules, error) {
|
||
|
|
r := c.Storage.Retention
|
||
|
|
rules := retention.Rules{
|
||
|
|
Ecosystems: map[string]time.Duration{},
|
||
|
|
Packages: map[string]time.Duration{},
|
||
|
|
}
|
||
|
|
|
||
|
|
var err error
|
||
|
|
if rules.Default, err = parseRetentionDuration("storage.retention.default", r.Default); err != nil {
|
||
|
|
return retention.Rules{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
if r.SweepInterval != "" {
|
||
|
|
d, err := parseDayDuration(r.SweepInterval)
|
||
|
|
if err != nil {
|
||
|
|
return retention.Rules{}, fmt.Errorf("invalid storage.retention.sweep_interval: %w", err)
|
||
|
|
}
|
||
|
|
if d < minRetentionSweepInterval {
|
||
|
|
return retention.Rules{}, fmt.Errorf("invalid storage.retention.sweep_interval %q: must be at least %s",
|
||
|
|
r.SweepInterval, minRetentionSweepInterval)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, key := range sortedKeys(r.Ecosystems) {
|
||
|
|
field := "storage.retention.ecosystems." + key
|
||
|
|
if !retention.IsKnown(key) {
|
||
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: unknown ecosystem %q (valid: %s)",
|
||
|
|
field, key, strings.Join(retention.KnownKeys, ", "))
|
||
|
|
}
|
||
|
|
if _, ok := reg.Lookup(key); !ok {
|
||
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: retention is not supported for %q in this version", field, key)
|
||
|
|
}
|
||
|
|
d, err := parseRetentionDuration(field, r.Ecosystems[key])
|
||
|
|
if err != nil {
|
||
|
|
return retention.Rules{}, err
|
||
|
|
}
|
||
|
|
rules.Ecosystems[key] = d
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, key := range sortedKeys(r.Packages) {
|
||
|
|
field := fmt.Sprintf("storage.retention.packages[%q]", key)
|
||
|
|
canonical, err := retentionPackageKey(reg, key)
|
||
|
|
if err != nil {
|
||
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: %w", field, err)
|
||
|
|
}
|
||
|
|
d, err := parseRetentionDuration(field, r.Packages[key])
|
||
|
|
if err != nil {
|
||
|
|
return retention.Rules{}, err
|
||
|
|
}
|
||
|
|
if prev, ok := rules.Packages[canonical]; ok && prev != d {
|
||
|
|
return retention.Rules{}, fmt.Errorf("invalid %s: conflicts with another key for package %s", field, canonical)
|
||
|
|
}
|
||
|
|
rules.Packages[canonical] = d
|
||
|
|
}
|
||
|
|
|
||
|
|
return rules, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// retentionPackageKey returns the stored package PURL a configured package
|
||
|
|
// key stands for.
|
||
|
|
func retentionPackageKey(reg *retention.Registry, key string) (string, error) {
|
||
|
|
// A julia PURL needs a uuid qualifier to parse at all, and the julia
|
||
|
|
// handler stores none, so no key could ever match one of its packages.
|
||
|
|
if strings.HasPrefix(strings.ToLower(key), "pkg:julia/") {
|
||
|
|
return "", fmt.Errorf("package rules are not supported for julia")
|
||
|
|
}
|
||
|
|
p, err := purl.Parse(key)
|
||
|
|
if err != nil {
|
||
|
|
return "", fmt.Errorf("not a valid package PURL: %w", err)
|
||
|
|
}
|
||
|
|
if p.Version != "" {
|
||
|
|
return "", fmt.Errorf("package PURL must not carry a version")
|
||
|
|
}
|
||
|
|
// A rule names a whole package; qualifiers and subpaths would be
|
||
|
|
// dropped on the way to the stored PURL, so refuse them rather than
|
||
|
|
// let "?type=pom" quietly cover every file of the package.
|
||
|
|
if len(p.Qualifiers) > 0 || p.Subpath != "" {
|
||
|
|
return "", fmt.Errorf("package PURL must not carry qualifiers or a subpath")
|
||
|
|
}
|
||
|
|
canonical, ok := reg.Canonical(p)
|
||
|
|
if ok {
|
||
|
|
return canonical, nil
|
||
|
|
}
|
||
|
|
ecosystem, registered := reg.KeyForDBEcosystem(purl.PURLTypeToEcosystem(p.Type))
|
||
|
|
if !registered {
|
||
|
|
return "", fmt.Errorf("retention is not supported for %s packages in this version", p.Type)
|
||
|
|
}
|
||
|
|
if spec, _ := reg.Lookup(ecosystem); spec.CanonicalPackage == nil {
|
||
|
|
return "", fmt.Errorf("package rules are not supported for %s; use storage.retention.ecosystems.%s", ecosystem, ecosystem)
|
||
|
|
}
|
||
|
|
return "", fmt.Errorf("does not match how %s packages are stored; see the package rule forms in docs/configuration.md", ecosystem)
|
||
|
|
}
|
||
|
|
|
||
|
|
func sortedKeys(m map[string]string) []string {
|
||
|
|
keys := make([]string, 0, len(m))
|
||
|
|
for k := range m {
|
||
|
|
keys = append(keys, k)
|
||
|
|
}
|
||
|
|
sort.Strings(keys)
|
||
|
|
return keys
|
||
|
|
}
|
||
|
|
|
||
|
|
// ParseRetentionSweepInterval returns how often the retention sweep runs.
|
||
|
|
func (c *Config) ParseRetentionSweepInterval() time.Duration {
|
||
|
|
if c.Storage.Retention.SweepInterval == "" {
|
||
|
|
return defaultRetentionSweepInterval
|
||
|
|
}
|
||
|
|
d, err := parseDayDuration(c.Storage.Retention.SweepInterval)
|
||
|
|
if err != nil || d < minRetentionSweepInterval {
|
||
|
|
return defaultRetentionSweepInterval
|
||
|
|
}
|
||
|
|
return d
|
||
|
|
}
|