Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/config/mirror.go
Christian Heim 8d96791f1a
Re-mirror container images on a schedule (#508)
* Add mirror.schedules, a list of image tags the server re-mirrors at a fixed interval. Digests are rejected since they never move.

* Run each schedule through the normal mirror, one run after the other. The first run starts at a random point within ten minutes and later runs get ±10 % jitter, so several instances don't hit the registry at once.

* Start the schedules with the server and let shutdown wait for a running refresh before storage and database are closed. Docs included.

* Check cached layers during a mirror without opening them, so a refresh no longer counts as a cache hit and doesn't keep scheduled images at the top of LRU eviction.
2026-10-18 18:33:13 +01:00

108 lines
3.2 KiB
Go

package config
import (
"fmt"
"strings"
"time"
"github.com/git-pkgs/purl"
)
const minMirrorScheduleInterval = time.Minute
// MirrorConfig configures mirroring that the server runs on its own.
type MirrorConfig struct {
// Schedules re-mirror container image tags at a fixed interval, so a
// moved tag is picked up even when nobody pulls it.
Schedules []MirrorSchedule `json:"schedules" yaml:"schedules"`
}
// MirrorSchedule is a set of image PURLs mirrored every Interval.
type MirrorSchedule struct {
// Images are pkg:docker or pkg:oci PURLs with a tag.
Images []string `json:"images" yaml:"images"`
// Interval is how often the images are mirrored, at least one minute
// (e.g. "6h").
Interval string `json:"interval" yaml:"interval"`
// Platforms limit which platforms of an image index are mirrored, as
// os/architecture[/variant]. Empty mirrors every platform.
Platforms []string `json:"platforms" yaml:"platforms"`
interval time.Duration
}
// IntervalDuration returns the parsed Interval. Validate must run first.
func (s *MirrorSchedule) IntervalDuration() time.Duration {
return s.interval
}
// Validate checks every schedule.
func (m *MirrorConfig) Validate() error {
for i := range m.Schedules {
if err := m.Schedules[i].validate(fmt.Sprintf("mirror.schedules[%d]", i)); err != nil {
return err
}
}
return nil
}
func (s *MirrorSchedule) validate(field string) error {
d, err := time.ParseDuration(s.Interval)
if err != nil {
return fmt.Errorf("invalid %s.interval %q: %w", field, s.Interval, err)
}
if d < minMirrorScheduleInterval {
return fmt.Errorf("invalid %s.interval %q: must be at least %s", field, s.Interval, minMirrorScheduleInterval)
}
s.interval = d
if len(s.Images) == 0 {
return fmt.Errorf("%s.images must not be empty", field)
}
for _, image := range s.Images {
if err := validateScheduledImage(image); err != nil {
return fmt.Errorf("invalid %s.images entry %q: %w", field, image, err)
}
}
for _, platform := range s.Platforms {
if err := validatePlatform(platform); err != nil {
return fmt.Errorf("invalid %s.platforms entry %q: %w", field, platform, err)
}
}
return nil
}
func validateScheduledImage(image string) error {
p, err := purl.Parse(image)
if err != nil {
return err
}
if p.Type != "docker" && p.Type != "oci" {
return fmt.Errorf("only pkg:docker and pkg:oci images can be scheduled")
}
if p.Version == "" {
return fmt.Errorf("a tag is required")
}
repositoryURL := p.Qualifiers.Map()["repository_url"]
if p.Type == "oci" && repositoryURL == "" {
return fmt.Errorf("pkg:oci needs a repository_url qualifier")
}
// Only pkg:oci mirrors the tag qualifier next to the digest.
pinned := strings.HasPrefix(p.Version, "sha256:")
if pinned && (p.Type == "docker" || p.Qualifiers.Map()["tag"] == "") {
return fmt.Errorf("a digest never moves, so there is nothing to refresh; use a tag")
}
return nil
}
// validatePlatform matches handler.ParsePlatform, which config cannot import.
func validatePlatform(value string) error {
os, rest, _ := strings.Cut(value, "/")
architecture, variant, _ := strings.Cut(rest, "/")
if os == "" || architecture == "" || strings.Contains(variant, "/") || strings.Contains(value, ",") {
return fmt.Errorf("want os/architecture[/variant]")
}
return nil
}