mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
* 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.
89 lines
4.3 KiB
Go
89 lines
4.3 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMirrorConfigValidate(t *testing.T) {
|
|
const digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
tests := []struct {
|
|
name string
|
|
schedule MirrorSchedule
|
|
wantErr bool
|
|
}{
|
|
{name: "docker tags", schedule: MirrorSchedule{Images: []string{"pkg:docker/library/alpine@3.20", "pkg:docker/nginx@stable"}, Interval: "6h"}},
|
|
{name: "oci with tag", schedule: MirrorSchedule{Images: []string{"pkg:oci/app@v1?repository_url=ghcr.io/acme/app"}, Interval: "1m"}},
|
|
{name: "oci digest with tag", schedule: MirrorSchedule{Images: []string{"pkg:oci/app@" + digest + "?repository_url=ghcr.io/acme/app&tag=v1"}, Interval: "1h"}},
|
|
{name: "platforms", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@3"}, Interval: "1h", Platforms: []string{"linux/amd64", "linux/arm64/v8"}}},
|
|
{name: "missing interval", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@3"}}, wantErr: true},
|
|
{name: "bad interval", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@3"}, Interval: "often"}, wantErr: true},
|
|
{name: "interval under a minute", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@3"}, Interval: "30s"}, wantErr: true},
|
|
{name: "no images", schedule: MirrorSchedule{Interval: "1h"}, wantErr: true},
|
|
{name: "unparsable purl", schedule: MirrorSchedule{Images: []string{"alpine:3"}, Interval: "1h"}, wantErr: true},
|
|
{name: "not an image", schedule: MirrorSchedule{Images: []string{"pkg:npm/lodash@4.17.21"}, Interval: "1h"}, wantErr: true},
|
|
{name: "no tag", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine"}, Interval: "1h"}, wantErr: true},
|
|
{name: "docker digest", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@" + digest}, Interval: "1h"}, wantErr: true},
|
|
{name: "docker digest with tag qualifier", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@" + digest + "?tag=3"}, Interval: "1h"}, wantErr: true},
|
|
{name: "oci digest only", schedule: MirrorSchedule{Images: []string{"pkg:oci/app@" + digest + "?repository_url=ghcr.io/acme/app"}, Interval: "1h"}, wantErr: true},
|
|
{name: "oci without repository_url", schedule: MirrorSchedule{Images: []string{"pkg:oci/app@v1"}, Interval: "1h"}, wantErr: true},
|
|
{name: "platform without architecture", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@3"}, Interval: "1h", Platforms: []string{"linux"}}, wantErr: true},
|
|
{name: "platform list in one entry", schedule: MirrorSchedule{Images: []string{"pkg:docker/alpine@3"}, Interval: "1h", Platforms: []string{"linux/amd64,linux/arm64"}}, wantErr: true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
m := MirrorConfig{Schedules: []MirrorSchedule{tt.schedule}}
|
|
err := m.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMirrorScheduleIntervalDuration(t *testing.T) {
|
|
m := MirrorConfig{Schedules: []MirrorSchedule{{Images: []string{"pkg:docker/alpine@3"}, Interval: "6h"}}}
|
|
if err := m.Validate(); err != nil {
|
|
t.Fatalf("Validate: %v", err)
|
|
}
|
|
if got := m.Schedules[0].IntervalDuration(); got != 6*time.Hour {
|
|
t.Errorf("IntervalDuration() = %v, want 6h", got)
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsMirrorSchedulesWithoutCachedArtifacts(t *testing.T) {
|
|
cfg := Default()
|
|
cfg.Mirror.Schedules = []MirrorSchedule{{Images: []string{"pkg:docker/alpine@3"}, Interval: "1h"}}
|
|
cfg.Storage.CacheArtifacts = false
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Error("Validate() accepted mirror.schedules with storage.cache_artifacts: false")
|
|
}
|
|
}
|
|
|
|
func TestLoadYAMLMirrorSchedules(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
data := `
|
|
mirror:
|
|
schedules:
|
|
- images: [pkg:docker/library/alpine@3.20]
|
|
interval: 6h
|
|
platforms: [linux/amd64]
|
|
- images: [pkg:docker/nginx@stable]
|
|
interval: 30s
|
|
`
|
|
if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(cfg.Mirror.Schedules) != 2 || cfg.Mirror.Schedules[0].Platforms[0] != "linux/amd64" {
|
|
t.Fatalf("schedules = %+v", cfg.Mirror.Schedules)
|
|
}
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Error("Validate() accepted a 30s interval in the second schedule")
|
|
}
|
|
}
|