Skip to content

Commit 6d97809

Browse files
gantoclaude
andcommitted
Add Gentoo distfiles proxy support with cache exclude feature
Introduce per-repository `exclude` config field and `"*"` wildcard support in `suffixes` to cache all files except specified exclusions. Add Gentoo repository config, e2e test, and documentation snippets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3bc005c commit 6d97809

13 files changed

Lines changed: 280 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased](https://github.com/ganto/pkgproxy/commits/HEAD/)
88

9+
### Added
10+
11+
- Per-repository `exclude` config field to prevent specific filenames or suffixes from being cached
12+
- Support caching Gentoo distfiles with `suffixes: ["*"]` wildcard and `exclude` list
13+
- Gentoo e2e test using `emerge --fetchonly` in a `gentoo/stage3` container
14+
915
## [v0.1.2](https://github.com/ganto/pkgproxy/releases/tag/v0.1.2) - 2026-03-28
1016

1117
### Fixed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ $(if $(filter rockylinux,$(1)),TestRockyLinux,\
107107
$(if $(filter debian,$(1)),TestDebian,\
108108
$(if $(filter ubuntu,$(1)),TestUbuntu,\
109109
$(if $(filter archlinux,$(1)),TestArch,\
110-
$(error Unknown DISTRO: $(1). Use one of: fedora centos-stream almalinux rockylinux debian ubuntu archlinux)))))))))
110+
$(if $(filter gentoo,$(1)),TestGentoo,\
111+
$(error Unknown DISTRO: $(1). Use one of: fedora centos-stream almalinux rockylinux debian ubuntu archlinux gentoo))))))))))
111112
endef
112113

113114
.PHONY: e2e

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ Can be used for any type of RPM-based enterprise distribution. E.g. `/etc/yum.re
135135
baseurl=http://<pkgproxy>:8080/epel/$releasever/Everything/$basearch/
136136
```
137137

138+
### Gentoo
139+
140+
`/etc/portage/make.conf`:
141+
```
142+
GENTOO_MIRRORS="http://<pkgproxy>:8080/gentoo"
143+
```
144+
138145
### Fedora
139146

140147
`/etc/yum.repos.d/fedora.repo` (adjust other repositories accordingly):
@@ -193,7 +200,7 @@ Run tests for a specific distribution and release:
193200
make e2e DISTRO=fedora RELEASE=42
194201
```
195202

196-
Supported `DISTRO` values: `fedora`, `centos-stream`, `almalinux`, `rockylinux`, `debian`, `ubuntu`, `archlinux`.
203+
Supported `DISTRO` values: `fedora`, `centos-stream`, `almalinux`, `rockylinux`, `debian`, `ubuntu`, `archlinux`, `gentoo`.
197204

198205
When adding support for a new Linux distribution, corresponding e2e tests should be added as well.
199206

configs/pkgproxy.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ repositories:
5656
mirrors:
5757
- https://mirror.init7.net/fedora/epel/
5858
- https://dl.fedoraproject.org/pub/epel/
59+
gentoo:
60+
suffixes:
61+
- "*"
62+
exclude:
63+
- layout.conf
64+
- timestamp.mirmon
65+
- timestamp.dev-local
66+
mirrors:
67+
- https://mirror.init7.net/gentoo/
68+
- https://pkg.adfinis-on-exoscale.ch/gentoo/
69+
- https://distfiles.gentoo.org/
5970
fedora:
6071
suffixes:
6172
- .drpm
Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
## 1. Cache exclude feature
22

3-
- [ ] 1.1 Add `Exclude []string` field to `Repository` struct in `pkg/pkgproxy/repository.go`; in `validateConfig`, if a repository's `suffixes` list contains `"*"` alongside other entries, log a `slog.Warn` naming the repository and the redundant suffixes
4-
- [ ] 1.2 Add `Exclude []string` field to `CacheConfig` in `pkg/cache/cache.go`
5-
- [ ] 1.3 Pass `Exclude` from `Repository` into `CacheConfig` when constructing upstreams in `proxy.go`
6-
- [ ] 1.4 Update `IsCacheCandidate` in `cache.go` to: run exclude check first (exact name + suffix), then handle `"*"` wildcard, then existing suffix logic
7-
- [ ] 1.5 Add unit tests for `IsCacheCandidate` covering: wildcard match, exclude exact name, exclude suffix, exclude overrides wildcard, exclude overrides explicit suffix, no exclude field
8-
- [ ] 1.6 Add unit test for `validateConfig` covering: wildcard with redundant explicit suffixes emits a warning and returns no error
3+
- [x] 1.1 Add `Exclude []string` field to `Repository` struct in `pkg/pkgproxy/repository.go`; in `validateConfig`, if a repository's `suffixes` list contains `"*"` alongside other entries, log a `slog.Warn` naming the repository and the redundant suffixes
4+
- [x] 1.2 Add `Exclude []string` field to `CacheConfig` in `pkg/cache/cache.go`
5+
- [x] 1.3 Pass `Exclude` from `Repository` into `CacheConfig` when constructing upstreams in `proxy.go`
6+
- [x] 1.4 Update `IsCacheCandidate` in `cache.go` to: run exclude check first (exact name + suffix), then handle `"*"` wildcard, then existing suffix logic
7+
- [x] 1.5 Add unit tests for `IsCacheCandidate` covering: wildcard match, exclude exact name, exclude suffix, exclude overrides wildcard, exclude overrides explicit suffix, no exclude field
8+
- [x] 1.6 Add unit test for `validateConfig` covering: wildcard with redundant explicit suffixes emits a warning and returns no error
99

1010
## 2. Gentoo repository config
1111

12-
- [ ] 2.1 Add `gentoo` entry to `configs/pkgproxy.yaml` with `suffixes: ["*"]`, `exclude: [layout.conf, timestamp.mirmon, timestamp.dev-local]`, and mirrors: `mirror.init7.net`, `pkg.adfinis-on-exoscale.ch`, `distfiles.gentoo.org`
12+
- [x] 2.1 Add `gentoo` entry to `configs/pkgproxy.yaml` with `suffixes: ["*"]`, `exclude: [layout.conf, timestamp.mirmon, timestamp.dev-local]`, and mirrors: `mirror.init7.net`, `pkg.adfinis-on-exoscale.ch`, `distfiles.gentoo.org`
1313

1414
## 3. E2e test
1515

16-
- [ ] 3.1 Add `assertNotCached` helper to `test/e2e/e2e_test.go` that asserts no file matching a given name exists anywhere under a cache subdirectory
17-
- [ ] 3.2 Write `test/e2e/test-gentoo.sh` shell script that: downloads `portage-latest.tar.xz` directly from `distfiles.gentoo.org`, unpacks it to `/var/db/repos/gentoo`, sets `GENTOO_MIRRORS` in `make.conf` to point at pkgproxy, runs `emerge --fetchonly app-text/tree`, then fetches `distfiles/layout.conf` via `wget` through the proxy
18-
- [ ] 3.3 Add `TestGentoo` to `test/e2e/e2e_test.go` using `docker.io/gentoo/stage3:latest`, mounting the script, asserting tree source archive is cached under `gentoo/distfiles/`, and asserting `layout.conf` is NOT cached using `assertNotCached`
16+
- [x] 3.1 Add `assertNotCached` helper to `test/e2e/e2e_test.go` that asserts no file matching a given name exists anywhere under a cache subdirectory
17+
- [x] 3.2 Write `test/e2e/test-gentoo.sh` shell script that: downloads `portage-latest.tar.xz` directly from `distfiles.gentoo.org`, unpacks it to `/var/db/repos/gentoo`, sets `GENTOO_MIRRORS` in `make.conf` to point at pkgproxy, runs `emerge --fetchonly app-text/tree`, then fetches `distfiles/layout.conf` via `wget` through the proxy
18+
- [x] 3.3 Add `TestGentoo` to `test/e2e/e2e_test.go` using `docker.io/gentoo/stage3:latest`, mounting the script, asserting tree source archive is cached under `gentoo/distfiles/`, and asserting `layout.conf` is NOT cached using `assertNotCached`
1919

2020
## 3b. Makefile
2121

22-
- [ ] 3b.1 Add `gentoo → TestGentoo` mapping to the `distroToTest` macro in `Makefile` so `make e2e DISTRO=gentoo` works; add `gentoo` to the error message's list of valid values
22+
- [x] 3b.1 Add `gentoo → TestGentoo` mapping to the `distroToTest` macro in `Makefile` so `make e2e DISTRO=gentoo` works; add `gentoo` to the error message's list of valid values
2323

2424
## 4. Documentation
2525

26-
- [ ] 4.1 Add Gentoo `make.conf` snippet to `README.md`
27-
- [ ] 4.2 Add Gentoo `make.conf` snippet to the HTTP landing page (`pkg/pkgproxy/landing.go` or template)
28-
- [ ] 4.3 Update `CHANGELOG.md` `[Unreleased]` section with new features
26+
- [x] 4.1 Add Gentoo `make.conf` snippet to `README.md`
27+
- [x] 4.2 Add Gentoo `make.conf` snippet to the HTTP landing page (`pkg/pkgproxy/landing.go` or template)
28+
- [x] 4.3 Update `CHANGELOG.md` `[Unreleased]` section with new features

pkg/cache/cache.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ type CacheConfig struct {
5656

5757
// List of file suffixes that will be cached
5858
FileSuffixes []string
59+
60+
// List of filenames or suffixes that are never cached
61+
Exclude []string
5962
}
6063

6164
func New(cfg *CacheConfig) FileCache {
@@ -104,17 +107,30 @@ func (c *cache) GetFileSuffixes() []string {
104107

105108
// Verifies if the given file URI is candidate to be cached
106109
func (c *cache) IsCacheCandidate(uri string) bool {
107-
ca := false
108-
109110
name := utils.FilenameFromURI(uri)
111+
112+
// Exclude check first: exact name match or suffix match.
113+
for _, entry := range c.config.Exclude {
114+
if name == entry || strings.HasSuffix(name, entry) {
115+
return false
116+
}
117+
}
118+
119+
// Wildcard: "*" in suffixes means cache everything (that wasn't excluded).
120+
for _, suffix := range c.GetFileSuffixes() {
121+
if suffix == "*" {
122+
return true
123+
}
124+
}
125+
126+
// Existing suffix-match logic.
110127
for _, suffix := range c.GetFileSuffixes() {
111128
if strings.HasSuffix(name, suffix) {
112-
ca = true
113-
break
129+
return true
114130
}
115131
}
116132

117-
return ca
133+
return false
118134
}
119135

120136
// Verifies if the file is already cached

pkg/cache/cache_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,87 @@ func TestCommitTempFile(t *testing.T) {
187187
})
188188
}
189189

190+
func TestIsCacheCandidate(t *testing.T) {
191+
tests := []struct {
192+
name string
193+
suffixes []string
194+
exclude []string
195+
uri string
196+
want bool
197+
}{
198+
{
199+
name: "wildcard matches any file",
200+
suffixes: []string{"*"},
201+
uri: "/repo/distfiles/ab/somefile.tar.xz",
202+
want: true,
203+
},
204+
{
205+
name: "exclude exact name blocks caching",
206+
suffixes: []string{"*"},
207+
exclude: []string{"layout.conf"},
208+
uri: "/repo/distfiles/layout.conf",
209+
want: false,
210+
},
211+
{
212+
name: "exclude suffix blocks caching",
213+
suffixes: []string{"*"},
214+
exclude: []string{".sig"},
215+
uri: "/repo/distfiles/ab/somefile.tar.xz.sig",
216+
want: false,
217+
},
218+
{
219+
name: "exclude overrides wildcard",
220+
suffixes: []string{"*"},
221+
exclude: []string{"timestamp.mirmon"},
222+
uri: "/repo/distfiles/timestamp.mirmon",
223+
want: false,
224+
},
225+
{
226+
name: "exclude overrides explicit suffix",
227+
suffixes: []string{".rpm"},
228+
exclude: []string{"verylarge.rpm"},
229+
uri: "/repo/verylarge.rpm",
230+
want: false,
231+
},
232+
{
233+
name: "no exclude field behaves normally",
234+
suffixes: []string{".rpm"},
235+
uri: "/repo/package.rpm",
236+
want: true,
237+
},
238+
{
239+
name: "no exclude field rejects non-matching suffix",
240+
suffixes: []string{".rpm"},
241+
uri: "/repo/readme.txt",
242+
want: false,
243+
},
244+
{
245+
name: "wildcard without exclude matches everything",
246+
suffixes: []string{"*"},
247+
uri: "/repo/anything",
248+
want: true,
249+
},
250+
{
251+
name: "non-matching exclude does not affect caching",
252+
suffixes: []string{"*"},
253+
exclude: []string{"layout.conf"},
254+
uri: "/repo/distfiles/ab/somefile.crate",
255+
want: true,
256+
},
257+
}
258+
259+
for _, tt := range tests {
260+
t.Run(tt.name, func(t *testing.T) {
261+
c := New(&CacheConfig{
262+
BasePath: "/cache",
263+
FileSuffixes: tt.suffixes,
264+
Exclude: tt.exclude,
265+
})
266+
assert.Equal(t, tt.want, c.IsCacheCandidate(tt.uri))
267+
})
268+
}
269+
}
270+
190271
func TestSaveToDiskStillWorks(t *testing.T) {
191272
baseDir := t.TempDir()
192273
c := New(&CacheConfig{BasePath: baseDir})

pkg/pkgproxy/landing.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ var snippetFuncs = map[string]func(string) string{
8282
"# metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-$releasever&arch=$basearch\n" +
8383
"baseurl=http://" + addr + "/epel/$releasever/Everything/$basearch/"
8484
},
85+
"gentoo": func(addr string) string {
86+
return "GENTOO_MIRRORS=\"http://" + addr + "/gentoo\""
87+
},
8588
"fedora": func(addr string) string {
8689
return "[fedora]\n" +
8790
"# metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch\n" +

pkg/pkgproxy/proxy.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func New(config *PkgProxyConfig) PkgProxy {
134134
cache: cache.New(&cache.CacheConfig{
135135
BasePath: config.CacheBasePath,
136136
FileSuffixes: config.RepositoryConfig.Repositories[repo].CacheSuffixes,
137+
Exclude: config.RepositoryConfig.Repositories[repo].Exclude,
137138
}),
138139
mirrors: mirrors,
139140
retries: retries,

pkg/pkgproxy/repository.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package pkgproxy
55
import (
66
"errors"
77
"fmt"
8+
"log/slog"
89
"os"
910
"path/filepath"
1011
"regexp"
@@ -22,6 +23,7 @@ type RepoConfig struct {
2223

2324
type Repository struct {
2425
CacheSuffixes []string `yaml:"suffixes"`
26+
Exclude []string `yaml:"exclude,omitempty"`
2527
Mirrors []string `yaml:"mirrors"`
2628
Retries int `yaml:"retries,omitempty"`
2729
}
@@ -62,6 +64,20 @@ func validateConfig(config *RepoConfig) error {
6264
if repoConfig.Mirrors == nil {
6365
return fmt.Errorf("missing required key for repository '%s': mirrors", handle)
6466
}
67+
// Warn if suffixes contains "*" alongside other entries (redundant).
68+
hasWildcard := false
69+
var redundant []string
70+
for _, s := range repoConfig.CacheSuffixes {
71+
if s == "*" {
72+
hasWildcard = true
73+
} else {
74+
redundant = append(redundant, s)
75+
}
76+
}
77+
if hasWildcard && len(redundant) > 0 {
78+
slog.Warn("repository has wildcard suffix '*' with redundant explicit suffixes",
79+
"repository", handle, "redundant_suffixes", redundant)
80+
}
6581
}
6682
return nil
6783
}

0 commit comments

Comments
 (0)