Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,24 +702,30 @@ Commit a `mise.lock` and install with `mise install --locked` so pinned
installs need no API call at all. Add a bearer token for `https://api.github.com`
under `upstream.auth` if the fleet exceeds GitHub's anonymous rate limit.

Tools mise downloads from elsewhere, such as Maven from `archive.apache.org`,
can be cached the same way by declaring their version-pinned paths in
`upstream.generic_artifacts` (see [configuration](docs/configuration.md)):
Maven, mvnd, Node and Go downloads are cached the same way once their hosts
are configured as generic upstreams; their version-pinned paths are built in
(see [configuration](docs/configuration.md)):

```yaml
upstream:
generic:
apache: "https://archive.apache.org"
generic_artifacts:
apache:
- 'dist/(?P<name>maven)/maven-3/(?P<version>[^/]+)/binaries/(?P<file>[^/]+)'
nodejs: "https://nodejs.org"
golang: "https://dl.google.com/go"
```

```toml
[settings]
node.mirror_url = "http://localhost:8080/generic/nodejs/dist/"
go.download_mirror = "http://localhost:8080/generic/golang"

[settings.url_replacements]
"https://archive.apache.org/" = "http://localhost:8080/generic/apache/"
```

Other download hosts need their version-pinned paths declared in
`upstream.generic_artifacts`.

## Configuration

The proxy can be configured via:
Expand Down
18 changes: 14 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,18 @@ version-pinned GitHub release assets: they are stored in the artifact cache
and served from it without revalidation, including while the upstream is
down.

Upstreams pointed at these hosts get version-pinned paths built in, whatever
the upstream is named:

| Upstream URL | Cached as artifacts | Package name |
|---|---|---|
| `https://archive.apache.org` | `dist/{project dirs}/{version}/[binaries/]{file}`, where `{version}` starts with a digit | the project dirs, e.g. `maven/maven-3`, `maven/mvnd` |
| `https://nodejs.org` | `dist/v{version}/{file}` | the upstream name |
| `https://dl.google.com/go` | `go{version}.{os}-{arch}.tar.gz`, `.zip` and their `.sha256` | the upstream name |

Other upstreams can declare their own version-pinned paths in
`generic_artifacts`. Each pattern is a regular expression that must match the
`generic_artifacts`. These are tried before any built-in ones, so a configured
pattern that overlaps a built-in decides the package name. Each pattern is a regular expression that must match the
whole path after `/generic/{name}/`, with `version` and `file` named groups
(single path segments) and an optional `name` group for the package name,
which otherwise is the upstream name. Matching downloads are cached like
Expand All @@ -371,10 +381,10 @@ release assets and listed under the `generic` ecosystem:
```yaml
upstream:
generic:
apache: "https://archive.apache.org"
eclipse: "https://download.eclipse.org"
generic_artifacts:
apache:
- 'dist/(?P<name>maven)/maven-3/(?P<version>[^/]+)/binaries/(?P<file>[^/]+)'
eclipse:
- 'jdtls/milestones/(?P<version>[^/]+)/(?P<file>[^/]+)'
```

Only list paths whose content never changes for a given version: a match is
Expand Down
45 changes: 35 additions & 10 deletions internal/handler/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ const (
// and served without revalidation once fetched.
var githubReleaseAssetPattern = regexp.MustCompile(`^([^/]+)/([^/]+)/releases/download/([^/]+)/([^/]+)$`)

// builtinGenericArtifacts maps well-known download hosts to patterns for
// their version-pinned paths. They apply to any generic upstream configured
// with that base URL, after the upstream's own generic_artifacts.
var builtinGenericArtifacts = map[string][]string{
// dist/{project dirs}/{version}/[binaries/]{file}, e.g. maven/maven-3 or maven/mvnd.
"https://archive.apache.org": {
`dist/(?P<name>.+?)/(?P<version>[0-9][^/]*)/(?:binaries/)?(?P<file>[^/]+)`,
},
"https://nodejs.org": {
`dist/(?P<version>v[0-9][^/]*)/(?P<file>[^/]+)`,
},
// The version is only in the filename: go1.23.2.linux-amd64.tar.gz.
"https://dl.google.com/go": {
`(?P<file>go(?P<version>[0-9][^/]*?)\.[a-z0-9]+-[a-z0-9]+\.(?:tar\.gz|zip)(?:\.sha256)?)`,
},
}

// GenericHandler proxies plain HTTP downloads from configured upstream base
// URLs. Each configured upstream is mounted at /generic/{name}/ and the
// remaining request path (and query string) is appended to the upstream URL.
Expand All @@ -38,7 +55,7 @@ var githubReleaseAssetPattern = regexp.MustCompile(`^([^/]+)/([^/]+)/releases/do
// assets, and is pointed at by URL-rewriting settings on the client.
//
// Release-asset paths ({owner}/{repo}/releases/download/{tag}/{asset}), and
// paths matching the upstream's configured artifact patterns, are
// paths matching the upstream's built-in or configured artifact patterns, are
// version-pinned and cached in the shared artifact cache, so they keep being
// served when the upstream is unreachable. Every other path is served through
// the metadata cache: fresh within the metadata TTL, revalidated with the
Expand All @@ -63,22 +80,30 @@ func NewGenericHandler(proxy *Proxy, repositories map[string]string, artifactPat
for name, upstreamURL := range repositories {
h.repositories[name] = strings.TrimSuffix(upstreamURL, "/")
}
// The first matching pattern wins, so configured patterns go first.
if len(artifactPatterns) > 0 {
for name, patterns := range artifactPatterns[0] {
for _, pattern := range patterns {
re, err := config.CompileGenericArtifactPattern(pattern)
if err != nil {
proxy.Logger.Warn("skipping generic artifact pattern",
"repository", name, "pattern", pattern, "error", err)
continue
}
h.artifactPatterns[name] = append(h.artifactPatterns[name], re)
}
h.addArtifactPatterns(name, patterns)
}
}
for name, upstreamURL := range h.repositories {
h.addArtifactPatterns(name, builtinGenericArtifacts[upstreamURL])
}
return h
}

func (h *GenericHandler) addArtifactPatterns(name string, patterns []string) {
for _, pattern := range patterns {
re, err := config.CompileGenericArtifactPattern(pattern)
if err != nil {
h.proxy.Logger.Warn("skipping generic artifact pattern",
"repository", name, "pattern", pattern, "error", err)
continue
}
h.artifactPatterns[name] = append(h.artifactPatterns[name], re)
}
}

// Routes returns the HTTP handler for generic download requests.
// Mount this at /generic on your router.
func (h *GenericHandler) Routes() http.Handler {
Expand Down
52 changes: 52 additions & 0 deletions internal/handler/generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,58 @@ func TestMatchArtifactPattern(t *testing.T) {
}
}

func TestGenericHandler_BuiltinArtifactPatterns(t *testing.T) {
h := NewGenericHandler(testProxy(), map[string]string{
"apache": "https://archive.apache.org/",
"node": "https://nodejs.org",
"golang": "https://dl.google.com/go",
"mirror": "https://mirror.example.com",
}, map[string][]string{
"apache": {`extra/(?P<version>[^/]+)/(?P<file>[^/]+)`, testMavenPattern},
})

tests := []struct {
repository string
path string
want genericArtifact
ok bool
}{
// The configured testMavenPattern overlaps the built-in and takes precedence.
{"apache", "dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz",
genericArtifact{name: "maven", version: "3.9.9", filename: "apache-maven-3.9.9-bin.tar.gz"}, true},
{"apache", "dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz.sha512",
genericArtifact{name: "maven", version: "3.9.9", filename: "apache-maven-3.9.9-bin.tar.gz.sha512"}, true},
{"apache", "dist/maven/maven-4/4.0.0/binaries/apache-maven-4.0.0-bin.tar.gz",
genericArtifact{name: "maven/maven-4", version: "4.0.0", filename: "apache-maven-4.0.0-bin.tar.gz"}, true},
{"apache", "dist/maven/mvnd/1.0.6/maven-mvnd-1.0.6-linux-amd64.tar.gz",
genericArtifact{name: "maven/mvnd", version: "1.0.6", filename: "maven-mvnd-1.0.6-linux-amd64.tar.gz"}, true},
{"apache", "dist/maven/maven-3/", genericArtifact{}, false},
{"apache", "dist/maven/maven-3/3.9.9/source/apache-maven-3.9.9-src.tar.gz", genericArtifact{}, false},
{"apache", "extra/1.0/file.txt", genericArtifact{name: "apache", version: "1.0", filename: "file.txt"}, true},
{"node", "dist/v22.11.0/node-v22.11.0-linux-x64.tar.gz",
genericArtifact{name: "node", version: "v22.11.0", filename: "node-v22.11.0-linux-x64.tar.gz"}, true},
{"node", "dist/v22.11.0/SHASUMS256.txt",
genericArtifact{name: "node", version: "v22.11.0", filename: "SHASUMS256.txt"}, true},
{"node", "dist/index.json", genericArtifact{}, false},
{"golang", "go1.23.2.linux-amd64.tar.gz",
genericArtifact{name: "golang", version: "1.23.2", filename: "go1.23.2.linux-amd64.tar.gz"}, true},
{"golang", "go1.23.2.darwin-arm64.tar.gz.sha256",
genericArtifact{name: "golang", version: "1.23.2", filename: "go1.23.2.darwin-arm64.tar.gz.sha256"}, true},
{"golang", "go1.24rc1.windows-amd64.zip",
genericArtifact{name: "golang", version: "1.24rc1", filename: "go1.24rc1.windows-amd64.zip"}, true},
{"mirror", "dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz", genericArtifact{}, false},
}

for _, tt := range tests {
t.Run(tt.repository+"/"+tt.path, func(t *testing.T) {
got, ok := h.parseArtifact(tt.repository, tt.path)
if ok != tt.ok || got != tt.want {
t.Errorf("parseArtifact(%q, %q) = (%+v, %v), want (%+v, %v)", tt.repository, tt.path, got, ok, tt.want, tt.ok)
}
})
}
}

func TestGenericHandler_ArtifactPatternIsCachedAndOtherPathsAreNot(t *testing.T) {
const (
artifactPath = "/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz"
Expand Down
Loading