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_test.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

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")
}
}