Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-10 08:09:17 -05:00

Compare commits

..
Author SHA1 Message Date
Andrew Nesbitt
ac137a3289
Extract portable npm metadata transformations 2026-10-18 17:59:14 +01:00
463 changed files with 4774 additions and 63857 deletions

View file

@ -1,17 +0,0 @@
# Version and commit come in as build args, so the build doesn't need .git.
.git
.github
# Local build output and runtime data
/proxy
dist/
build/
cache/
*.db
*.tar.gz
# Local config and secrets
.env
.env.local
proxy.cfg
config.yaml

View file

@ -31,40 +31,6 @@ jobs:
- name: Test
run: go test -v -race ./...
postgres:
name: test (postgres)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17@sha256:b8faeff7712663c2773a190181e1246e37e364b887611c04658c28b44e2408fe
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: proxy
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d proxy"
--health-interval 2s
--health-timeout 5s
--health-retries 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
# The packages with tests that need PROXY_DATABASE_URL. -p 1 because
# their tests drop and recreate tables in the one shared database.
- name: Test
env:
PROXY_DATABASE_URL: postgres://postgres:test@localhost:5432/proxy?sslmode=disable
run: go test -v -race -p 1 ./internal/database/... ./internal/handler/... ./internal/server/...
lint:
runs-on: ubuntu-latest
steps:
@ -78,7 +44,7 @@ jobs:
go-version-file: go.mod
- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.1
- name: golangci-lint
run: golangci-lint run ./...
@ -110,13 +76,3 @@ jobs:
--set 'ingress.hosts[0].paths[0].path=/' \
--set 'ingress.hosts[0].paths[0].pathType=Prefix' \
>/dev/null
helm template proxy deploy/charts/proxy \
--set 'extraVolumes[0].name=corp-ca' \
--set 'extraVolumes[0].configMap.name=corp-ca' \
--set 'extraVolumeMounts[0].name=corp-ca' \
--set 'extraVolumeMounts[0].mountPath=/etc/proxy/ca' \
--set 'extraVolumeMounts[0].readOnly=true' \
--set 'extraInitContainers[0].name=wait-for-db' \
--set 'extraInitContainers[0].image=busybox:1.37' \
--set 'extraInitContainers[0].command={sh,-c,exit 0}' \
>/dev/null

View file

@ -1,137 +0,0 @@
name: Monthly release
# Cuts a release on the 1st of each month when main is green and something
# has merged since the last release: a patch release, or a new minor version
# if a pull request labelled "breaking" was merged (a new major version from
# 1.0 on). A breaking release is created as a draft, and its images and chart
# are published only when someone publishes the draft, after writing the
# upgrade notes. Scheduled runs only report what they would do until the
# repository variable MONTHLY_RELEASE is "true".
on:
schedule:
- cron: "0 6 1 * *"
workflow_dispatch:
inputs:
dry_run:
description: Only report what would be released
type: boolean
default: true
permissions: {}
concurrency:
group: monthly-release
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
checks: read
pull-requests: read
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
fetch-depth: 0
persist-credentials: false
- name: Decide whether to release
id: plan
env:
EVENT: ${{ github.event_name }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
ENABLED: ${{ vars.MONTHLY_RELEASE }}
run: |
set -euo pipefail
if [ "$EVENT" = workflow_dispatch ]; then
dry_run=$INPUT_DRY_RUN
elif [ "$ENABLED" = true ]; then
dry_run=false
else
dry_run=true
fi
echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT"
sha=$(git rev-parse HEAD)
last=$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' --exclude '*-*')
# A breaking release waits as a draft for its upgrade notes. Building
# on its tag would publish those changes without them. Older tags
# were released by hand and may have no release at all.
draft=$(gh release view "$last" -R "$REPO" --json isDraft --jq .isDraft 2>/dev/null || echo false)
if [ "$draft" = true ]; then
echo "::warning::$last is still a draft, publish it first"
exit 0
fi
merged=$(git rev-list --count "$last..$sha")
if [ "$merged" -eq 0 ]; then
echo "Nothing merged since $last"
exit 0
fi
# Every check on main's HEAD must have finished green. This run
# shows up as a check on the same commit, so it is left out.
bad=$(gh api --paginate "repos/$REPO/commits/$sha/check-runs?per_page=100" \
--jq ".check_runs[]
| select(.html_url | contains(\"/actions/runs/$GITHUB_RUN_ID/\") | not)
| select(.status != \"completed\"
or (.conclusion | IN(\"success\", \"skipped\", \"neutral\") | not))
| .name")
if [ -n "$bad" ]; then
echo "::warning::main ($sha) is not green, skipping: $(echo "$bad" | sort -u | paste -sd, -)"
exit 0
fi
since=$(git log -1 --format=%cI "$last")
breaking=$(gh pr list -R "$REPO" --state merged --base main --limit 1000 \
--search "merged:>$since label:breaking" --json number --jq '.[].number')
IFS=. read -r major minor patch <<<"${last#v}"
if [ -z "$breaking" ]; then
tag="v$major.$minor.$((patch + 1))"
else
echo "Breaking changes merged since $last: #$(echo "$breaking" | sort -n | paste -sd, - | sed 's/,/, #/g')"
if [ "$major" -eq 0 ]; then
tag="v0.$((minor + 1)).0"
else
tag="v$((major + 1)).0.0"
fi
fi
echo "$merged commits since $last, next release $tag at $sha"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "draft=$([ -n "$breaking" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
# A tag pushed with GITHUB_TOKEN doesn't start other workflows, so the
# release workflows are dispatched on the new tag instead.
- name: Tag and start the release workflows
if: steps.plan.outputs.tag != ''
env:
TAG: ${{ steps.plan.outputs.tag }}
SHA: ${{ steps.plan.outputs.sha }}
DRY_RUN: ${{ steps.plan.outputs.dry_run }}
DRAFT: ${{ steps.plan.outputs.draft }}
run: |
set -euo pipefail
if [ "$DRAFT" = true ]; then
what="start release.yml for a draft release"
else
what="start release.yml and publish.yml"
fi
if [ "$DRY_RUN" = true ]; then
echo "Dry run: would tag $TAG at $SHA and $what"
exit 0
fi
gh api "repos/$REPO/git/refs" -f ref="refs/tags/$TAG" -f sha="$SHA" >/dev/null
gh workflow run release.yml -R "$REPO" --ref "$TAG" -f draft="$DRAFT"
if [ "$DRAFT" = true ]; then
echo "::notice::$TAG is a draft release: add the upgrade notes and publish it to push the images and chart"
else
gh workflow run publish.yml -R "$REPO" --ref "$TAG"
fi
echo "Tagged $TAG at $SHA"

View file

@ -5,10 +5,6 @@ on:
push:
tags:
- "v*"
# A draft release made by monthly-release.yml is published by hand. Releases
# published with GITHUB_TOKEN don't start this.
release:
types: [published]
permissions: {}
@ -38,7 +34,7 @@ jobs:
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to the Container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -61,9 +57,6 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.meta.outputs.version }}
COMMIT=${{ github.sha }}
provenance: mode=max
sbom: true

View file

@ -4,13 +4,6 @@ on:
push:
tags:
- "v*"
# Started by monthly-release.yml on the tag it creates.
workflow_dispatch:
inputs:
draft:
description: Create the release as a draft
type: boolean
default: false
permissions:
contents: write
@ -18,7 +11,6 @@ permissions:
jobs:
release:
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
steps:
@ -35,9 +27,9 @@ jobs:
go-version-file: go.mod
cache: false
- uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7.3.0
- uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
version: "~> v2"
args: release --clean${{ inputs.draft && ' --draft' || '' }}
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -34,4 +34,4 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4

View file

@ -26,7 +26,3 @@ linters:
- goconst
- dupl
- mnd
formatters:
enable:
- gofmt

View file

@ -15,7 +15,6 @@ builds:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64

View file

@ -212,24 +212,6 @@ Add PyPI registry support
- Add tests for wheel and sdist downloads
```
## Reviewing and Merging
Maintainers are the members of the proxy team in the git-pkgs org. Any maintainer can review and merge. A pull request needs green CI and an approval from a maintainer other than its author. A "request changes" review from any maintainer blocks it until that reviewer approves or withdraws the review.
Using AI to write or review is fine, but whoever approves a pull request should understand it and stand behind it. If it touches an ecosystem you don't use, you can ask someone who does to look before you approve.
Open an issue before a bigger change: anything that changes behaviour users rely on, new configuration that would be hard to remove, or a change that fans out into other git-pkgs modules. If no maintainer objects within a few days, open the pull request; it's reviewed like any other. Objections are worked out in the issue, with @andrew breaking ties for now. Where possible, deprecate before removing: an old configuration key keeps working, with a warning, in at least one release before the one that removes it.
## Releases
A release is cut on the 1st of each month by `.github/workflows/monthly-release.yml` when `main` is green and something has merged since the last tag. It is a patch release, or a new minor version if a pull request labelled `breaking` was merged since then (a new major version from 1.0 on), so label pull requests that change behaviour users rely on.
A patch release is published on its own. A breaking release is created as a draft, without images or a chart: write the upgrade notes in it and publish it, which starts `publish.yml` for the images and the chart. Until then the monthly run releases nothing.
Scheduled runs only report what they would release until the repository variable `MONTHLY_RELEASE` is set to `true`. The workflow can also be started by hand from the Actions tab, as a dry run by default.
To release by hand, push a `v*` tag.
## Questions?
Open an issue on GitHub if you have questions or need help getting started.

View file

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.27.3-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.26.7-alpine AS builder
WORKDIR /src
@ -14,15 +14,11 @@ COPY . .
# Build the binary for the target platform
ARG TARGETARCH
ARG VERSION=dev
ARG COMMIT=unknown
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
-ldflags="-s -w -X main.Version=${VERSION} -X main.Commit=${COMMIT}" \
-o /proxy ./cmd/proxy
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags="-s -w" -o /proxy ./cmd/proxy
FROM alpine:3.24.3
FROM alpine:3.24.2
RUN apk add --no-cache ca-certificates git
RUN apk add --no-cache ca-certificates
COPY --from=builder /proxy /usr/local/bin/proxy
@ -35,9 +31,5 @@ WORKDIR /data
EXPOSE 8080
# /health checks the database and storage and can take up to ten seconds.
HEALTHCHECK --interval=30s --timeout=11s --start-period=10s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
ENTRYPOINT ["proxy"]
CMD ["serve"]

334
README.md
View file

@ -20,8 +20,6 @@ A 3-day cooldown means that when `lodash` publishes version `4.18.0`, your build
Resolution order: package override, then ecosystem override, then global default. This lets you set a conservative default and carve out exceptions for packages where you need faster updates. See [docs/configuration.md](docs/configuration.md) for the full config reference.
One instance can offer several cooldown policies through [named profiles](docs/configuration.md#cooldown-profiles). Configure `cooldown.profiles` and point clients at paths such as `/cooldown/3d/npm/` or `/cooldown/7d/cargo/`. Profiles share the package cache; existing URLs keep using the global policy.
## Artifact Scanning
Cooldown only looks at a version's publish timestamp — it never inspects the actual bytes. Artifact scanning closes that gap: when enabled, every artifact is staged into storage and scanned by one or more external services (trivy, ClamAV, Wiz, or anything else that speaks a small HTTP/JSON contract) before it's committed to the cache and served to clients.
@ -49,11 +47,11 @@ The proxy never uploads artifact bytes to a scanner. Each scanner is notified wi
| npm | JavaScript | Yes | ✓ |
| Cargo | Rust | Yes | ✓ |
| RubyGems | Ruby | Yes | ✓ |
| Go proxy | Go | Yes† | ✓ |
| Go proxy | Go | | ✓ |
| Hex | Elixir | Yes* | ✓ |
| pub.dev | Dart | Yes | ✓ |
| PyPI | Python | Yes | ✓ |
| Maven | Java | Yes‡ | ✓ |
| Maven | Java | | ✓ |
| Gradle Build Cache | Java/Kotlin | | ✓ |
| NuGet | .NET | Yes | ✓ |
| Composer | PHP | Yes | ✓ |
@ -68,20 +66,14 @@ The proxy never uploads artifact bytes to a scanner. Each scanner is notified wi
| RPM | RHEL/Fedora | | ✓ |
| Alpine | Alpine Linux | | ✓ |
| Arch | Arch Linux | | ✓ |
| FreeBSD | FreeBSD (ports and pkgbase) | | ✓ |
| Chef Supermarket | Chef | | ✓ |
| Chef | Chef | | ✗ |
| Generic | Any | | ✓ |
| Git | Any | | ✓ |
| Helm | Kubernetes | Yes | ✓ |
| Vagrant | Vagrant | | ✗ |
Cooldown requires publish timestamps in metadata. Registries without a "Yes" in the cooldown column either don't expose timestamps or haven't been wired up yet.
\* Hex cooldown requires disabling registry signature verification (`HEX_UNSAFE_REGISTRY=1`) since the proxy re-encodes the protobuf payload. Origin verification can stay enabled.
† Go cooldown uses the time in each version's `.info`, which is the commit time of the tagged revision, not when the tag was pushed.
‡ Maven cooldown uses the `Last-Modified` header of each version's `.pom`. On Maven Central that is the deploy time; a Nexus or Artifactory upstream may report when it cached the file instead. SNAPSHOT versions are not held back.
\* Hex cooldown requires disabling registry signature verification (`HEX_NO_VERIFY_REPO_ORIGIN=1`) since the proxy re-encodes the protobuf payload.
## Install
@ -89,11 +81,7 @@ Cooldown requires publish timestamps in metadata. Registries without a "Yes" in
brew install git-pkgs/git-pkgs/proxy
```
Or download a binary from the [releases page](https://github.com/git-pkgs/proxy/releases), or run the container image:
```bash
docker run -p 8080:8080 -v proxy-data:/data ghcr.io/git-pkgs/proxy:latest
```
Or download a binary from the [releases page](https://github.com/git-pkgs/proxy/releases).
### Helm
@ -110,33 +98,6 @@ using SQLite and filesystem artifact storage under `/data`. See
[`deploy/charts/proxy/values.yaml`](deploy/charts/proxy/values.yaml) for ingress,
external database and object-storage configuration options.
The SQLite database needs a local disk or a block-backed volume, even with one
replica; `ReadWriteOnce` alone doesn't guarantee that, since the storage class
can still be NFS. If the cluster only offers network filesystem storage, use
Postgres. Artifact storage can stay on NFS, separately from the database. See
[SQLite](docs/configuration.md#sqlite).
To trust a private CA, for example when upstreams sit behind a TLS-intercepting
corporate proxy or are internal registries signed by an in-house CA, mount the
certificate with `extraVolumes` and `extraVolumeMounts` and add its directory
to `SSL_CERT_DIR`. The proxy process makes all upstream TLS connections itself,
Git fetches included, so no other configuration is needed. Public CAs stay
trusted alongside the private one:
```yaml
extraVolumes:
- name: corp-ca
configMap:
name: corp-ca # e.g. key corp-ca.crt, PEM-encoded
extraVolumeMounts:
- name: corp-ca
mountPath: /etc/proxy/ca
readOnly: true
extraEnv:
- name: SSL_CERT_DIR
value: /etc/ssl/certs:/etc/proxy/ca
```
## Quick Start
```bash
@ -150,8 +111,7 @@ go build -o proxy ./cmd/proxy
./proxy -listen :3000 -base-url https://proxy.example.com
```
The proxy is now running. Configure your package managers to use it. For image
builds, see [Using proxy in Docker builds](docs/docker-builds.md).
The proxy is now running. Configure your package managers to use it.
## OpenAPI (Swagger)
@ -199,12 +159,9 @@ through the proxy: the audit and signing-key endpoints are passed through to the
configured upstream registry, with upstream authentication applied. Advisories
therefore come from upstream's database, not from the proxy's own vulnerability
data, and versions withheld by [cooldown](#version-cooldown) are not excluded
from the report. Packuments point each version's attestations (provenance) at
the proxy, which fetches them from the package's registry and caches them;
attestations of versions held back by cooldown or blocked by the denylist are
not served.
from the report.
To serve private packages through the same registry URL, see [Private package routes](docs/configuration.md#private-package-routes). To see new releases before `metadata_ttl` runs out, see [Change feeds](docs/configuration.md#change-feeds).
To serve private packages through the same registry URL, see [Private package routes](docs/configuration.md#private-package-routes).
### Cargo
@ -255,14 +212,6 @@ the `go` command contacts the database directly as before. Set
checksum databases are listed under `upstream.go_sumdbs`, see
[Go checksum database](docs/configuration.md#go-checksum-database).
govulncheck can use the proxy for the Go vulnerability database:
```bash
govulncheck -db http://localhost:8080/go/vulndb ./...
```
See [Go vulnerability database](docs/configuration.md#go-vulnerability-database).
### Homebrew
Point Homebrew's JSON API and artifact domain at the proxy:
@ -274,7 +223,7 @@ export HOMEBREW_ARTIFACT_DOMAIN=http://localhost:8080
The artifact domain proxies manifests and bottle blobs under `/v2/homebrew/core/`. GHCR routing is limited to that repository. Source archives, cask application downloads, custom tap artifacts, and legacy flat-file bottle mirrors use Homebrew's normal fallback URLs. Keep fallback enabled by leaving `HOMEBREW_ARTIFACT_DOMAIN_NO_FALLBACK` unset.
Enable `cache_metadata` or set `PROXY_CACHE_METADATA=true` to retain Homebrew JSON API responses for offline fallback. Bottle blobs and their OCI manifests are cached without this setting. `proxy mirror pkg:brew/<formula>` pre-fetches a formula's bottles (see [Homebrew bottles](docs/configuration.md#homebrew-bottles)).
Enable `cache_metadata` or set `PROXY_CACHE_METADATA=true` to retain Homebrew JSON API responses for offline fallback. Bottle blobs and their OCI manifests are cached without this setting.
The upstreams default to `https://formulae.brew.sh/api` for the JSON API and `https://ghcr.io` for artifacts. To chain this proxy to another proxy, configure its Homebrew endpoints as the upstreams:
@ -288,10 +237,10 @@ The equivalent environment variables are `PROXY_UPSTREAM_HOMEBREW_API` and `PROX
### Hex (Elixir)
Configure the mirror:
Configure in `~/.hex/hex.config`:
```bash
mix hex.config mirror_url http://localhost:8080/hex
```erlang
{default_url, <<"http://localhost:8080/hex">>}.
```
Or set the environment variable:
@ -300,14 +249,6 @@ Or set the environment variable:
export HEX_MIRROR=http://localhost:8080/hex
```
When the proxy applies a cooldown, also set:
```bash
export HEX_UNSAFE_REGISTRY=1
```
Filtering releases removes the registry signature. The proxy preserves the package name and repository fields, so origin verification can stay enabled. Without a cooldown, leave signature verification enabled too.
### pub.dev (Dart/Flutter)
Set the PUB_HOSTED_URL environment variable:
@ -349,8 +290,6 @@ Add to your `~/.m2/settings.xml`:
The `/maven/` endpoint uses Maven Central as primary upstream and falls back to the Gradle Plugin Portal for Gradle plugin marker metadata and related artifacts when the primary upstream returns not found.
Further Maven repositories can be configured under `upstream.maven_repositories` and are served at `/maven/{name}/`, each from its own upstream (see [Maven repositories](docs/configuration.md#maven-repositories)). Point a second `<repository>` or Gradle `maven(url = ...)` entry at `http://localhost:8080/maven/{name}/`.
For Gradle plugin resolution via the same proxy endpoint:
```kotlin
@ -363,20 +302,20 @@ pluginManagement {
### Gradle HTTP Build Cache
Configure in `settings.gradle.kts`:
Configure in `settings.gradle(.kts)`:
```kotlin
buildCache {
local {
enabled = false
}
remote<HttpBuildCache> {
url = uri("http://localhost:8080/gradle/")
push = true
}
}
```
Enable build caching with `org.gradle.caching=true` in `gradle.properties`, or pass `--build-cache` when running Gradle. The local cache stays enabled and remote uploads are disabled by default.
To upload from CI, set `gradle.build_cache.read_only` to `false` on the proxy and add `isPush = System.getenv("CI") != null` inside the client's `remote<HttpBuildCache>` block. See [docs/configuration.md](docs/configuration.md#gradle-build-cache).
### NuGet
Configure in `nuget.config`:
@ -417,16 +356,7 @@ Or set globally:
composer config -g repositories.proxy composer http://localhost:8080/composer
```
`composer audit`, `audit.block-insecure` and Packagist's malware filter list
work through the proxy when `upstream.composer_repository` offers them in its
`packages.json`, as repo.packagist.org does. The proxy advertises them in its
own `packages.json` and passes the advisories and filter list requests through
to the upstream uncached. Names of packages in `upstream.composer_routes` or
`hosted.composer_vendors` are removed from advisories requests before they are
sent. Versions withheld by [cooldown](#version-cooldown) are not excluded from
the report.
To serve private packages through the same URL, see [Private package routes](docs/configuration.md#private-package-routes). To see new releases before `metadata_ttl` runs out, see [Change feeds](docs/configuration.md#change-feeds).
To serve private packages through the same URL, see [Private package routes](docs/configuration.md#private-package-routes).
### Conan (C/C++)
@ -439,24 +369,6 @@ conan remote disable conancenter
Or configure in `~/.conan2/remotes.json`.
### Chef Supermarket (Berkshelf, Policyfiles)
Point Berkshelf at the proxy in your `Berksfile`:
```ruby
source "http://localhost:8080/chef"
```
Or set the default source of a Policyfile:
```ruby
default_source :supermarket, "http://localhost:8080/chef"
```
The proxy serves the `/universe` dependency index and the cookbook API, rewriting every download location to itself while leaving dependency constraints unchanged. Cookbook archives are fetched through the Supermarket download redirect and cached, so they stay available when the Supermarket is down. With `cache_metadata` on, the universe and cookbook metadata follow the usual TTL and stale fallback.
To proxy a private Supermarket instead of `supermarket.chef.io`, set `upstream.chef` (or `PROXY_UPSTREAM_CHEF`) and add credentials under `upstream.auth` if it needs them. See [docs/configuration.md](docs/configuration.md#upstream-registries).
### Conda
Configure in `~/.condarc`:
@ -469,17 +381,12 @@ default_channels:
- http://localhost:8080/conda/main
```
`default_channels` also routes requests for `defaults` through the proxy, including `-c defaults` and `environment.yml` files that name it.
Or set via command:
```bash
conda config --add channels http://localhost:8080/conda/main
conda config --add default_channels http://localhost:8080/conda/main
```
pixi and other rattler-based clients use sharded repodata ([CEP 16](https://github.com/conda/ceps/blob/main/cep-0016.md)) where a channel offers it, as conda-forge does. Point them at the same channel URLs, for example `pixi init -c http://localhost:8080/conda/conda-forge`. The shard index and shards are cached as metadata when `cache_metadata` is on.
### CRAN (R)
Set the repository in R:
@ -498,8 +405,6 @@ local({
})
```
Source packages, Windows and macOS binaries (including the `bin/macosx/big-sur-arm64/` layout used by R 4.3 and later) are cached as artifacts. `PACKAGES` indexes and the `src/contrib/Meta/*.rds` files that `remotes::install_version` and pak read are cached as metadata.
### Julia
Set the Pkg server before starting Julia:
@ -727,20 +632,16 @@ indexes unchanged. To keep APT from selecting a denied version, configure
### RPM / Yum / DNF
In the `[fedora]` section of your existing repository file under `/etc/yum.repos.d/`, replace the `metalink` or `mirrorlist` entry with:
Configure yum/dnf to use the proxy in `/etc/yum.repos.d/proxy.repo`:
```ini
[proxy-fedora]
name=Fedora via Proxy
baseurl=http://localhost:8080/rpm/releases/$releasever/Everything/$basearch/os/
enabled=1
gpgcheck=0
```
In the `[updates]` section of `fedora-updates.repo`, replace its `metalink` or `mirrorlist` entry with:
```ini
baseurl=http://localhost:8080/rpm/updates/$releasever/Everything/$basearch/
```
Keep the existing `gpgkey` and signature settings, including `gpgcheck=1`. The proxy serves RPM packages unchanged, so their signatures remain valid.
Then:
```bash
@ -770,11 +671,6 @@ signature verification keeps working. Indexes use the metadata cache
(`metadata_ttl`, stale fallback); `.apk` packages are stored in the shared
artifact cache and remain available when the upstream is unreachable.
Set `upstream.apk_snapshots` (e.g. `"90d"`) to keep replaced indexes and serve
them by date at `/apk/alpine@2026-10-01/v3.22/main`, so pinned package versions
stay installable after the mirror moves on. See
[Alpine APK snapshots](docs/configuration.md#alpine-apk-snapshots).
To proxy other mirrors or private repositories, configure named upstreams
under `upstream.apk` (this replaces the built-in default; re-add `alpine` if
you still want it):
@ -858,62 +754,6 @@ Avoid `mirror.archlinuxarm.org` as the upstream. It redirects each request to a
changing regional mirror over plain HTTP, so the database and the packages can come
from mirrors with a different sync state.
### FreeBSD / pkg
Override the repositories in `/usr/local/etc/pkg/repos/FreeBSD.conf`. Settings
there are merged with `/etc/pkg/FreeBSD.conf`, so only the URL and mirror type
change and the signature settings stay. The default repository name `freebsd`
proxies the official mirror (`https://pkg.freebsd.org`). On FreeBSD 15:
```
FreeBSD-ports: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/quarterly",
mirror_type: "none"
}
FreeBSD-ports-kmods: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/kmods_quarterly_${VERSION_MINOR}",
mirror_type: "none"
}
FreeBSD-base: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/base_release_${VERSION_MINOR}",
mirror_type: "none"
}
```
FreeBSD 14 has a single repository called `FreeBSD`:
```
FreeBSD: {
url: "http://localhost:8080/freebsd/freebsd/${ABI}/quarterly",
mirror_type: "none"
}
```
Then:
```bash
pkg update
```
Keep the path after `${ABI}` that `pkg -vv` shows for each repository
(`quarterly`, `latest`, `base_latest`, ...) and replace only the host part.
The repository catalogues (`meta.conf`, `packagesite.pkg`, `data.pkg`) carry
their signature inside the archive and are served byte-for-byte unchanged
through the metadata cache (`metadata_ttl`, stale fallback). Enable
`cache_metadata` so `pkg update` keeps working while the upstream is down.
Packages under `Hashed/` have their content hash in the file name, so they are
stored in the shared artifact cache as `pkg:bsd/freebsd/{name}@{version}` and
remain available when the upstream is unreachable.
Other files are passed through uncached. That includes the bootstrap package
under `Latest/`, whose signature file is rewritten with every repository build,
and package files without a hash in their name, which can be rebuilt under the
same name. The denylist still applies to unhashed package files. Since only
cached artifacts can be scanned, both kinds of package file are refused while
a scanner covers `freebsd`; use repositories built with `pkg repo --hash` and
bootstrap pkg without the proxy.
### GitHub Releases / mise (aqua backend)
Configure named generic upstreams:
@ -937,26 +777,8 @@ Release assets are cached permanently after the first download and keep
installing while GitHub is down. Tag lookups through `api.github.com` are
cached for `metadata_ttl` and served stale during an outage or rate limit.
Commit a `mise.lock` and install with `mise install --locked` so pinned
installs need no API call at all.
If the fleet exceeds GitHub's anonymous rate limit, add a bearer token for
`https://api.github.com` under `upstream.auth`. Everyone who can reach the
proxy can then make requests with that token, so the proxy requires
`generic_paths` listing the API paths it may serve, and the token should be a
fine-grained one with read-only access to public repositories:
```yaml
upstream:
generic_paths:
github-api:
- "repos/*/*/releases"
- "repos/*/*/releases/**"
- "repos/*/*/tags"
auth:
"https://api.github.com":
type: bearer
token: "${GITHUB_TOKEN}"
```
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.
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
@ -982,28 +804,6 @@ go.download_mirror = "http://localhost:8080/generic/golang"
Other download hosts need their version-pinned paths declared in
`upstream.generic_artifacts`.
### Git repositories
Configure named Git servers:
```yaml
upstream:
git:
github: "https://github.com"
```
Then point clones at the proxy:
```bash
git config --global url."http://localhost:8080/git/github/".insteadOf https://github.com/
```
Each repository is cached as a bare clone and refreshed whenever its branches
or tags change upstream. Git LFS objects are cached in the artifact cache;
git-lfs uses the proxy for them with no further setup. See [configuration](docs/configuration.md#git-repositories)
for setting the rewrite through environment variables, size limits, private
repositories and outage behavior.
## Configuration
The proxy can be configured via:
@ -1055,7 +855,7 @@ storage:
url: "file:///var/cache/proxy/artifacts"
max_size: "10GB" # Optional: evict LRU when exceeded
retention:
default: "30d" # Optional: evict unused artifacts after 30 days; excludes generic
default: "30d" # Optional: evict artifacts not downloaded for 30 days
database:
driver: "sqlite"
@ -1089,7 +889,7 @@ Run with config file:
### PostgreSQL
SQLite is the default and works well for single-node deployments, with the database file on a local disk or block-backed volume. For multi-node setups, when only network filesystem storage such as NFS is available, or if you prefer a managed database, switch to Postgres:
SQLite is the default and works well for single-node deployments. For multi-node setups or if you prefer a managed database, switch to Postgres:
```yaml
database:
@ -1197,9 +997,6 @@ proxy mirror pkg:npm/lodash@4.17.21 pkg:cargo/serde@1.0.0
# Mirror all versions of a package
proxy mirror pkg:npm/lodash
# Mirror only the newest version of a package
proxy mirror --latest pkg:npm/lodash
# Mirror from a CycloneDX or SPDX SBOM
proxy mirror --sbom sbom.cdx.json
@ -1211,14 +1008,9 @@ proxy mirror --concurrency 8 pkg:npm/lodash@4.17.21
# Mirror a container image for one platform
proxy mirror --platform linux/amd64 pkg:docker/library/alpine@3.20
# Mirror a commit of an upstream.git repository into the git cache
proxy mirror pkg:git/github.com/git-pkgs/purl@0123456789abcdef0123456789abcdef01234567
```
The mirror command accepts the same storage and database flags as `serve`. Already-cached artifacts are skipped. If any package version fails to mirror, the command lists the errors and exits with status 1, so a CI step that warms the cache fails on a partial mirror.
With `scanning.enabled`, the mirror command refuses every package and container image so nothing is cached unscanned. Each one is reported as failed, so the command exits with status 1. Scanners fetch the staged bytes from the running server, which this command does not start. Use the mirror API below instead, since its jobs run inside the server and go through the pre-cache scan. `--dry-run` still lists what would be mirrored.
The mirror command accepts the same storage and database flags as `serve`. Already-cached artifacts are skipped.
A mirror API is also available when the server is running:
@ -1312,9 +1104,6 @@ Recently cached:
| `GET /npm/*` | npm registry protocol |
| `POST /npm/-/npm/v1/security/*` | npm/pnpm/Yarn audit endpoints, passed through to upstream |
| `GET /npm/-/npm/v1/keys` | npm registry signing keys, passed through to upstream |
| `GET /npm/-/package/{name}/dist-tags` | `npm dist-tag ls`, answered from the packument the proxy serves (cooldown and pushed versions applied); changing tags is not supported |
| `GET /npm/-/npm/v1/attestations/{name}@{version}` | npm provenance attestations of a version, fetched from the package's registry and cached |
| `GET /npm/-/v1/search`, `GET /npm/-/ping` | `npm search` and `npm ping`, passed through to upstream uncached |
| `GET /cargo/*` | Cargo sparse index protocol |
| `GET /gem/*` | RubyGems protocol |
| `GET /go/*` | Go module proxy protocol |
@ -1324,10 +1113,7 @@ Recently cached:
| `GET /maven/*` | Maven repository protocol |
| `GET /nuget/*` | NuGet V3 API |
| `GET /composer/*` | Composer/Packagist protocol |
| `GET`, `POST /composer/api/security-advisories/` | Composer security advisories, passed through to upstream without private package names |
| `GET /composer/lists/summary.json` | Composer filter list summary, passed through to upstream uncached |
| `GET /conan/*` | Conan C/C++ protocol |
| `GET /chef/*` | Chef Supermarket protocol (universe, cookbook API and downloads) |
| `GET /conda/*` | Conda/Anaconda protocol |
| `GET /cran/*` | CRAN (R) protocol |
| `GET /julia/*` | Julia Pkg server protocol |
@ -1338,9 +1124,7 @@ Recently cached:
| `GET /v2/homebrew/core/*` | Homebrew core bottle manifests and blobs from GHCR |
| `GET /apk/{repository}/*` | Alpine APK repository protocol |
| `GET /arch/{repository}/*` | Arch Linux pacman repository protocol |
| `GET /freebsd/{repository}/*` | FreeBSD pkg repository protocol |
| `GET /generic/{name}/*` | Generic HTTP download proxy (GitHub release assets, mise/aqua) |
| `GET /git/{name}/{repo}/info/refs`, `POST /git/{name}/{repo}/git-upload-pack` | Git smart HTTP protocol, read-only |
| `GET /debian/*` | Debian/APT repository protocol (main archive) |
| `GET /debian/{repository}/*` | Debian/APT repository protocol (named archive, e.g. security) |
| `GET /rpm/*` | RPM/Yum repository protocol |
@ -1363,25 +1147,6 @@ Registered when `hosted.enabled` is set. Pushing and deleting require `Authoriza
| `GET /api/hosted/{ecosystem}/{name}` | List the archives pushed for a package |
| `DELETE /api/hosted/{ecosystem}/{name}/{version}` | Delete a pushed version |
### Projects API
Registered when `projects.enabled` is set. Uploading and deleting require `Authorization: Bearer <projects.token>`. See [Project inventory](docs/configuration.md#project-inventory).
| Endpoint | Description |
|----------|-------------|
| `PUT /api/projects/{project}/sbom` | Replace a project's recorded package versions with the ones the SBOM in the body lists |
| `GET /api/projects` | List projects, or with `?purl=pkg:npm/lodash@4.17.20` the projects using a package or version |
| `GET /api/projects/{project}` | A project's recorded package versions |
| `DELETE /api/projects/{project}` | Delete a project |
### Cache API
Registered when `cache_delete` is set. It has no authentication of its own.
| Endpoint | Description |
|----------|-------------|
| `DELETE /api/cache/{ecosystem}/{name}/{version}` | Delete a version's cached files, or only `?filename=` |
### Enrichment API
The proxy provides REST endpoints for package metadata enrichment, vulnerability scanning, and outdated detection.
@ -1535,7 +1300,9 @@ Response:
{
"cached_artifacts": 142,
"total_size_bytes": 523456789,
"total_size": "499.2 MB"
"total_size": "499.2 MB",
"storage_url": "file:///path/to/cache/artifacts",
"database_path": "./cache/proxy.db"
}
```
@ -1575,7 +1342,6 @@ The proxy serves a web UI under `/ui`. No separate frontend build is needed -- t
- **Version detail** (`/ui/package/{ecosystem}/{name}/{version}`) -- per-version metadata, integrity hash, artifact cache status, and hit counts.
- **Source browser** (`/ui/package/{ecosystem}/{name}/{version}/browse`) -- browse files inside cached archives with syntax highlighting for text files and image previews.
- **Version diff** (`/ui/package/{ecosystem}/{name}/compare/{v1}...{v2}`) -- side-by-side diff of two cached versions showing added, removed, and changed files.
- **Projects** (`/ui/projects`) -- with the [project inventory](docs/configuration.md#project-inventory) enabled, the package versions each repository's uploaded SBOM lists, marked when outdated. Package pages then show which projects use each version.
## Monitoring
@ -1585,17 +1351,11 @@ The proxy exposes Prometheus metrics at `GET /metrics`. All metric names are pre
|--------|------|--------|-------------|
| `proxy_requests_total` | counter | `ecosystem`, `status` | Proxy responses by package ecosystem and HTTP status |
| `proxy_request_duration_seconds` | histogram | `ecosystem`, `status` | Proxy request duration |
| `proxy_cache_hits_total` | counter | `ecosystem`, `kind` | Cache hits. `kind` is `artifact` or `metadata`. |
| `proxy_cache_misses_total` | counter | `ecosystem`, `kind` | Cache misses. `kind` is `artifact` or `metadata`; metadata misses are counted only while `cache_metadata` is on. |
| `proxy_cache_hits_total` | counter | `ecosystem` | Cache hits |
| `proxy_cache_misses_total` | counter | `ecosystem` | Cache misses |
| `proxy_cache_size_bytes` | gauge | | Total size of cached artifacts |
| `proxy_cached_artifacts_total` | gauge | | Number of cached artifacts |
| `proxy_metadata_cache_served_bytes_total` | counter | `ecosystem` | Metadata bytes served from the metadata cache. |
| `proxy_metadata_cache_size_bytes` | gauge | `ecosystem` | Size of cached metadata documents per ecosystem. |
| `proxy_metadata_cache_entries` | gauge | `ecosystem` | Number of cached metadata documents per ecosystem. |
| `proxy_artifacts_evicted_total` | counter | `reason`, `ecosystem` | Artifacts evicted by the size limit (`lru`) or by age (`retention`) |
| `proxy_metadata_evicted_total` | counter | `ecosystem` | Cached metadata documents evicted by the size limit |
| `proxy_metadata_expired_by_change_feed_total` | counter | `ecosystem` | Cached metadata documents set to revalidate because an upstream [change feed](docs/configuration.md#change-feeds) listed them |
| `proxy_fetch_lease_waits_total` | counter | `kind` | Cache misses (`artifact` or `metadata`) that waited for another instance sharing the database to fetch them |
| `proxy_upstream_fetch_duration_seconds` | histogram | `ecosystem` | Time spent fetching from upstream |
| `proxy_upstream_errors_total` | counter | `ecosystem`, `error_type` | Upstream fetch failures |
| `proxy_storage_operation_duration_seconds` | histogram | `operation` | Storage read/write latency |
@ -1655,17 +1415,11 @@ For history, trends and alerting, scrape `/metrics` with Prometheus. That is the
The same figures are available as JSON from `GET /stats`, which reports `downloaded_bytes`, `downloads` and an `ecosystems` array carrying the per-ecosystem breakdown, served from the same 60-second snapshot the page and the gauges read. When the aggregation fails with no snapshot to fall back on, the response carries `stats_unavailable: true` rather than passing zeros off as a count -- the endpoint keeps answering with the artifact count and cache size either way.
#### Artifact and metadata caches
The proxy keeps two caches. Artifacts (tarballs, wheels, crates and so on) live in storage and are what `proxy_cache_size_bytes`, `proxy_ecosystem_downloaded_bytes` and the other artifact figures describe. Registry metadata is cached separately when `cache_metadata` is on. It is stored persistently in the `metadata_cache` table rather than held in memory.
The `kind` label on `proxy_cache_hits_total` and `proxy_cache_misses_total` keeps the two apart, so an artifact hit rate is `sum(rate(proxy_cache_hits_total{kind="artifact"}[5m]))` over the same expression with misses added. Metadata has its own size and entry gauges plus `proxy_metadata_cache_served_bytes_total`. That one is a process counter rather than a database total, because metadata hits are not recorded per row. On `/ui/analytics` metadata appears in its own card and in the Runtime card's metadata lookups, never mixed into the artifact figures. The card and the two gauges read one 60-second snapshot, like the artifact figures. The bundled Grafana dashboard's hit ratio covers artifacts only, while its hits and misses panel shows each kind as its own series.
#### Three ecosystem label sets
`ecosystem` means three slightly different things across `/metrics`, and queries that join across them need to know which.
**From the package record, normalized.** The six `proxy_ecosystem_*` gauges, `proxy_cache_hits_total`, `proxy_cache_misses_total`, the three `proxy_metadata_cache_*` metrics, `proxy_artifacts_evicted_total`, `proxy_metadata_evicted_total`, `proxy_metadata_expired_by_change_feed_total`, `proxy_integrity_failures_total` and the scan metrics. Aliases collapse here: `gem` reads as `rubygems`, `composer` as `packagist`, `go` as `golang`.
**From the package record, normalized.** The six `proxy_ecosystem_*` gauges, `proxy_cache_hits_total`, `proxy_cache_misses_total`, `proxy_artifacts_evicted_total`, `proxy_integrity_failures_total` and the scan metrics. Aliases collapse here: `gem` reads as `rubygems`, `composer` as `packagist`, `go` as `golang`.
**From the request path.** `proxy_requests_total`, `proxy_request_duration_seconds` and `proxy_response_bytes_total`. The names mostly coincide with the normalized ones -- these also report `rubygems`, `packagist` and `golang` -- but the Debian route reports `debian` where the package record says `deb`, and any path that is not a package endpoint reports `other`, which corresponds to no ecosystem at all.
@ -1726,7 +1480,7 @@ Failing checks include an `"error"` field. Storage failures also include a `"ste
`circuit_breakers` reports the state of each upstream's artifact-fetch circuit breaker (`"open"` or `"closed"`), keyed by upstream host — or by the `hostless-url-<digest>` placeholder described under [Monitoring](#monitoring) where the fetch URL has no host to read. The key is omitted until the proxy has fetched an artifact from at least one upstream, and a host appears only once a breaker has been created for it. Breakers trip after repeated upstream failures and retry the upstream after an exponential backoff. While one is open, artifact downloads for that host return HTTP 502 on a cache miss without contacting the upstream; already-cached artifacts are still served from storage, since the cache is checked before the fetcher. A breaker is reported as `"open"` throughout its backoff, including the half-open window in which it admits one probe request to test recovery. Breaker state is per process and in memory, so a restart clears it, but a restart is not needed for recovery: the backoff keeps retrying for as long as the breaker is open, so it closes on its own once the upstream serves again.
`upstream_holds` lists upstream hosts that answered the proxy with HTTP 429, or 503 with a `Retry-After`, and the time until which the proxy sends them nothing. Every request to a held host, metadata and artifacts alike, fails locally for the duration the upstream asked for (capped at one hour), or for one minute doubling up to fifteen when the upstream gave no `Retry-After`. Cached metadata is served stale during a hold, and anything uncached returns HTTP 503 with the remaining `Retry-After` so a client can pace itself too. Metadata requests do not go through the circuit breaker, so without the hold a rate-limited upstream would keep receiving one request per cache miss, which is exactly the traffic it is asking the proxy to stop sending. The first non-limited response after a hold ends clears it, and `proxy_upstream_rate_limit_holds_total` counts the holds placed. Holds are recorded in the database, so instances sharing one honour each other's holds and list them here too. The key is omitted while no host is held.
`upstream_holds` lists upstream hosts that answered the proxy with HTTP 429, or 503 with a `Retry-After`, and the time until which the proxy sends them nothing. Every request to a held host, metadata and artifacts alike, fails locally for the duration the upstream asked for (capped at one hour), or for one minute doubling up to fifteen when the upstream gave no `Retry-After`. Cached metadata is served stale during a hold, and anything uncached returns HTTP 503 with the remaining `Retry-After` so a client can pace itself too. Metadata requests do not go through the circuit breaker, so without the hold a rate-limited upstream would keep receiving one request per cache miss, which is exactly the traffic it is asking the proxy to stop sending. The first non-limited response after a hold ends clears it, and `proxy_upstream_rate_limit_holds_total` counts the holds placed. The key is omitted while no host is held.
An open breaker does **not** set `status` to `"error"` or change the HTTP status code: it reports a specific upstream refusing to serve, not this proxy being unfit to receive traffic, and failing the readiness probe over one unhealthy upstream would pull the pod out of rotation for every other ecosystem too. Use `proxy_circuit_breaker_state` for alerting on it.
@ -1772,23 +1526,13 @@ sudo systemctl start proxy
### Docker
Images for linux/amd64 and linux/arm64 are published to `ghcr.io/git-pkgs/proxy` for each release, tagged with the version and `latest`:
```bash
docker run -p 8080:8080 -v proxy-data:/data ghcr.io/git-pkgs/proxy:latest
```
To build the image from the repo instead:
A Dockerfile is included in the repo. Build and run:
```bash
docker build -t proxy .
docker run -p 8080:8080 -v proxy-data:/data proxy
```
The image has a health check that requests `/health` on port 8080. It checks the database and storage, so a container reports unhealthy when either fails. If you change the port with `PROXY_LISTEN`, override it with `--health-cmd`.
With `--read-only`, also pass `--tmpfs /tmp`: the proxy writes some uploads to a temporary file before reading them.
With Postgres and S3:
```bash
@ -1878,12 +1622,6 @@ cache/artifacts/
└── nginx-1.24.0-1.fc39.x86_64.rpm
```
To delete one version's cached files, so the next request fetches them again, enable `cache_delete` and use the Delete buttons on the version page in the web UI, or the [cache API](docs/configuration.md#cache-delete-api):
```bash
curl -X DELETE 'http://localhost:8080/api/cache/maven/com.google.guava:guava/32.1.3-jre?filename=guava-32.1.3-jre.jar'
```
Cache metadata is stored in SQLite (default) or PostgreSQL. To clear a local cache:
```bash

View file

@ -1,8 +1,6 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
@ -11,12 +9,9 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"text/tabwriter"
"time"
"github.com/git-pkgs/proxy/internal/handler"
)
const (
@ -24,11 +19,10 @@ const (
hostedTokenEnv = "PROXY_HOSTED_TOKEN"
hostedClientTimeout = 10 * time.Minute
pushArgCount = 4 // ecosystem, name, version, file
listArgCount = 2 // ecosystem, name
deleteArgCount = 3 // ecosystem, name, version
deleteMaxArgCount = 4 // ecosystem, name, version, file for generic files
tablePadding = 2
pushArgCount = 4 // ecosystem, name, version, file
listArgCount = 2 // ecosystem, name
deleteArgCount = 3 // ecosystem, name, version
tablePadding = 2
)
// hostedArtifact mirrors the JSON the /api/hosted endpoints return.
@ -75,16 +69,15 @@ func newHostedCommand(name, usage, summary string, getenv func(string) string) h
return cmd
}
// parse parses flags and checks for between minArgs and maxArgs positional
// arguments.
func (c hostedCommand) parse(args []string, minArgs, maxArgs int) ([]string, error) {
// parse parses flags and checks for exactly want positional arguments.
func (c hostedCommand) parse(args []string, want int) ([]string, error) {
if err := c.flags.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
c.flags.Usage()
}
return nil, fmt.Errorf("%w\nusage: %s", err, c.usage)
}
if n := c.flags.NArg(); n < minArgs || n > maxArgs {
if c.flags.NArg() != want {
return nil, fmt.Errorf("usage: %s", c.usage)
}
return c.flags.Args(), nil
@ -112,7 +105,7 @@ func hostedToken(getenv func(string) string) (string, error) {
func runPush(args []string, getenv func(string) string, stdout io.Writer, client *http.Client) error {
cmd := newHostedCommand("push", "proxy hosted push [-url URL] <ecosystem> <name> <version> <file>",
"Push a package archive to the proxy", getenv)
positional, err := cmd.parse(args, pushArgCount, pushArgCount)
positional, err := cmd.parse(args, pushArgCount)
if err != nil {
return err
}
@ -128,32 +121,12 @@ func runPush(args []string, getenv func(string) string, stdout io.Writer, client
}
defer func() { _ = file.Close() }()
// The proxy checks the upload against this, so a truncated or corrupted
// transfer is refused rather than stored.
digest := sha256.New()
size, err := io.Copy(digest, file)
req, err := http.NewRequest(http.MethodPut, cmd.endpoint(ecosystem, name, version), file)
if err != nil {
return err
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
segments := []string{version}
if handler.HostedFileInPath(ecosystem) {
segments = append(segments, filepath.Base(path))
}
endpoint := cmd.endpoint(ecosystem, name, segments...) + "?sha256=" + hex.EncodeToString(digest.Sum(nil))
req, err := http.NewRequest(http.MethodPut, endpoint, file)
if err != nil {
return err
}
req.ContentLength = size
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/octet-stream")
// The proxy refuses an existing file or a bad token before reading the
// body, so the file is only uploaded once it would be accepted.
req.Header.Set("Expect", "100-continue")
var pushed hostedPushResponse
if err := doHosted(client, req, http.StatusCreated, &pushed); err != nil {
@ -166,7 +139,7 @@ func runPush(args []string, getenv func(string) string, stdout io.Writer, client
func runList(args []string, getenv func(string) string, stdout io.Writer, client *http.Client) error {
cmd := newHostedCommand("list", "proxy hosted list [-url URL] <ecosystem> <name>", "List pushed package archives", getenv)
positional, err := cmd.parse(args, listArgCount, listArgCount)
positional, err := cmd.parse(args, listArgCount)
if err != nil {
return err
}
@ -188,20 +161,16 @@ func runList(args []string, getenv func(string) string, stdout io.Writer, client
}
func runDelete(args []string, getenv func(string) string, stdout io.Writer, client *http.Client) error {
cmd := newHostedCommand("delete", "proxy hosted delete [-url URL] <ecosystem> <name> <version> [file]",
"Delete a pushed version, or one generic file of it", getenv)
positional, err := cmd.parse(args, deleteArgCount, deleteMaxArgCount)
cmd := newHostedCommand("delete", "proxy hosted delete [-url URL] <ecosystem> <name> <version>", "Delete a pushed version", getenv)
positional, err := cmd.parse(args, deleteArgCount)
if err != nil {
return err
}
if len(positional) > deleteArgCount && !handler.HostedFileInPath(positional[0]) {
return fmt.Errorf("only generic files can be deleted one at a time\nusage: %s", cmd.usage)
}
token, err := hostedToken(getenv)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodDelete, cmd.endpoint(positional[0], positional[1], positional[2:]...), nil)
req, err := http.NewRequest(http.MethodDelete, cmd.endpoint(positional[0], positional[1], positional[2]), nil)
if err != nil {
return err
}
@ -210,11 +179,7 @@ func runDelete(args []string, getenv func(string) string, stdout io.Writer, clie
if err := doHosted(client, req, http.StatusNoContent, nil); err != nil {
return err
}
deleted := positional[1] + "@" + positional[2]
if len(positional) > deleteArgCount {
deleted += " " + positional[deleteArgCount]
}
_, err = fmt.Fprintf(stdout, "deleted %s %s\n", positional[0], deleted)
_, err = fmt.Fprintf(stdout, "deleted %s %s@%s\n", positional[0], positional[1], positional[2])
return err
}
@ -245,7 +210,7 @@ func doHosted(client *http.Client, req *http.Request, wantStatus int, out any) e
const hostedUsage = "usage: proxy hosted <push|list|delete> [flags] ...\n\n" +
" push Push a package archive to the proxy\n" +
" list List pushed package archives\n" +
" delete Delete a pushed version, or one generic file of it"
" delete Delete a pushed version"
type hostedSubcommand func([]string, func(string) string, io.Writer, *http.Client) error

View file

@ -12,8 +12,7 @@ import (
)
type recordedRequest struct {
method, path, query, authorization, body, expect string
contentLength int64
method, path, query, authorization, body string
}
func hostedTestServer(t *testing.T, status int, response string) (*httptest.Server, *recordedRequest) {
@ -27,8 +26,6 @@ func hostedTestServer(t *testing.T, status int, response string) (*httptest.Serv
query: r.URL.RawQuery,
authorization: r.Header.Get("Authorization"),
body: string(body),
expect: r.Header.Get("Expect"),
contentLength: r.ContentLength,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
@ -65,34 +62,14 @@ func TestRunPush(t *testing.T) {
if recorded.method != http.MethodPut || recorded.path != "/api/hosted/npm/@types/node/20.1.0" {
t.Errorf("request = %s %s", recorded.method, recorded.path)
}
// sha256 of "tarball", so the proxy can check the upload.
const tarballSHA256 = "db4b4d0d1cb480bf9aeea253771c00febe627f236765fa37d6a5614f079a3aa0"
if recorded.authorization != "Bearer tok" || recorded.body != "tarball" || recorded.query != "sha256="+tarballSHA256 {
if recorded.authorization != "Bearer tok" || recorded.body != "tarball" || recorded.query != "" {
t.Errorf("authorization/body/query = %q/%q/%q", recorded.authorization, recorded.body, recorded.query)
}
if recorded.expect != "100-continue" || recorded.contentLength != int64(len("tarball")) {
t.Errorf("expect/content length = %q/%d, want 100-continue and the file size", recorded.expect, recorded.contentLength)
}
if out := stdout.String(); !strings.Contains(out, "node-20.1.0.tgz") || !strings.Contains(out, "abc123") {
t.Errorf("output = %q, want filename and sha256", out)
}
}
func TestRunPushGenericNamesTheFile(t *testing.T) {
srv, recorded := hostedTestServer(t, http.StatusCreated,
`{"ecosystem":"generic","name":"tools","version":"1.2.0","filename":"tool-linux-arm64.tar.gz","sha256":"abc123","size":7}`)
archive := writeArchive(t, "tool-linux-arm64.tar.gz", "tarball")
err := runPush([]string{"-url", srv.URL, "generic", "tools", "1.2.0", archive},
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, srv.Client())
if err != nil {
t.Fatalf("runPush: %v", err)
}
if recorded.path != "/api/hosted/generic/tools/1.2.0/tool-linux-arm64.tar.gz" {
t.Errorf("path = %q, want the file name appended", recorded.path)
}
}
func TestRunPushErrors(t *testing.T) {
srv, _ := hostedTestServer(t, http.StatusBadRequest, `{"code":"BAD_REQUEST","message":"pushing packages is not supported for this ecosystem: cargo"}`)
archive := writeArchive(t, "serde-1.0.200.crate", "crate")
@ -164,36 +141,6 @@ func TestRunDelete(t *testing.T) {
}
}
func TestRunDeleteOneFile(t *testing.T) {
srv, recorded := hostedTestServer(t, http.StatusNoContent, "")
var stdout bytes.Buffer
err := runDelete([]string{"-url", srv.URL, "generic", "tools", "1.2.0", "tool-linux-arm64.tar.gz"},
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), &stdout, srv.Client())
if err != nil {
t.Fatalf("runDelete: %v", err)
}
if recorded.path != "/api/hosted/generic/tools/1.2.0/tool-linux-arm64.tar.gz" {
t.Errorf("path = %q", recorded.path)
}
if out := stdout.String(); out != "deleted generic tools@1.2.0 tool-linux-arm64.tar.gz\n" {
t.Errorf("output = %q", out)
}
}
func TestRunDeleteOneFileOnlyForGeneric(t *testing.T) {
srv, recorded := hostedTestServer(t, http.StatusNoContent, "")
err := runDelete([]string{"-url", srv.URL, "npm", "@acme", "widget", "1.0.0"},
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, srv.Client())
if err == nil || !strings.Contains(err.Error(), "only generic files") {
t.Errorf("npm delete with a fourth argument error = %v, want a refusal", err)
}
if recorded.method != "" {
t.Errorf("refused delete sent %s %s", recorded.method, recorded.path)
}
}
func TestRunHostedDispatchesSubcommands(t *testing.T) {
srv, recorded := hostedTestServer(t, http.StatusOK, `{"ecosystem":"npm","name":"request","artifacts":[]}`)

View file

@ -8,7 +8,7 @@
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description "Bearer " followed by hosted.token, or projects.token for /api/projects
// @description "Bearer " followed by hosted.token
//
// The proxy caches package artifacts from upstream registries (npm, cargo, etc.)
// providing faster, more reliable access for development teams.
@ -82,7 +82,7 @@
// PROXY_LOG_FORMAT - Log format
// PROXY_ACCESS_LOG_PATH - JSONL access log path
// PROXY_UPSTREAM_* - Upstream URLs and network access controls
// PROXY_GRADLE_BUILD_CACHE_READ_ONLY - Set false to allow Gradle PUT uploads (default true)
// PROXY_GRADLE_BUILD_CACHE_READ_ONLY - Disable Gradle PUT uploads
// PROXY_GRADLE_BUILD_CACHE_MAX_UPLOAD_SIZE - Max Gradle PUT request body size
// PROXY_GRADLE_BUILD_CACHE_MAX_AGE - Gradle cache max age eviction
// PROXY_GRADLE_BUILD_CACHE_MAX_SIZE - Gradle cache max total size
@ -93,9 +93,6 @@
// PROXY_HOSTED_MAX_UPLOAD_SIZE - Max size of one pushed archive (default "100MB")
// PROXY_HOSTED_NPM_SCOPES - npm scopes served from pushed packages only
// PROXY_HOSTED_COMPOSER_VENDORS - Composer vendors served from pushed packages only
// PROXY_PROJECTS_ENABLED - Enable the project inventory (/api/projects, /ui/projects)
// PROXY_PROJECTS_TOKEN - Bearer token for uploading and deleting project SBOMs
// PROXY_PROJECTS_MAX_AGE - Remove projects not uploaded for this long (e.g. "90d")
// PROXY_URL - Proxy URL for proxy hosted
//
// Example:
@ -127,8 +124,7 @@ import (
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/memlimit"
"github.com/git-pkgs/proxy/internal/denylist"
"github.com/git-pkgs/proxy/internal/mirror"
"github.com/git-pkgs/proxy/internal/server"
"github.com/git-pkgs/proxy/internal/storage"
@ -242,7 +238,6 @@ func runServe() {
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_CARGO_DOWNLOAD Cargo download upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_GEM RubyGems upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_GO Go module proxy upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_GO_VULNDB Go vulnerability database URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_HEX Hex repository upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_HEX_API Hex API upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_PUB pub registry upstream URL\n")
@ -255,7 +250,6 @@ func runServe() {
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_COMPOSER Packagist API upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_COMPOSER_REPOSITORY Packagist repository upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_CONAN Conan registry upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_CHEF Chef Supermarket upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_CONDA Conda channel upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_CRAN CRAN mirror upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_JULIA Julia package server upstream URL\n")
@ -263,7 +257,7 @@ func runServe() {
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_OCI_DEFAULT Default OCI registry upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_DEBIAN Debian repository upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_RPM RPM repository upstream URL\n")
fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_READ_ONLY Set false to allow Gradle PUT uploads (default true)\n")
fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_READ_ONLY Disable Gradle PUT uploads\n")
fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_MAX_UPLOAD_SIZE Max Gradle PUT request body size\n")
fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_MAX_AGE Gradle cache max age eviction\n")
fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_MAX_SIZE Gradle cache max total size\n")
@ -328,7 +322,6 @@ func runServe() {
// Setup logger
logger := setupLogger(cfg.Log.Level, cfg.Log.Format)
memlimit.Apply(logger)
// Create and start server
srv, err := server.New(cfg, logger, server.BuildInfo{
@ -444,7 +437,6 @@ func runMirror() {
concurrency := fs.Int("concurrency", 4, "Number of parallel downloads") //nolint:mnd // default concurrency
dryRun := fs.Bool("dry-run", false, "Show what would be mirrored without downloading")
platforms := fs.String("platform", "", "Comma-separated os/arch[/variant] list limiting which platforms of container images are mirrored")
latest := fs.Bool("latest", false, "Mirror only the newest version of PURLs without a version")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "git-pkgs proxy - Pre-populate cache\n\n")
@ -453,7 +445,6 @@ func runMirror() {
fmt.Fprintf(os.Stderr, " proxy mirror pkg:npm/lodash@4.17.21\n")
fmt.Fprintf(os.Stderr, " proxy mirror --sbom sbom.cdx.json\n")
fmt.Fprintf(os.Stderr, " proxy mirror pkg:npm/lodash # all versions\n")
fmt.Fprintf(os.Stderr, " proxy mirror --latest pkg:npm/lodash # newest version\n")
fmt.Fprintf(os.Stderr, " proxy mirror --platform linux/amd64 pkg:docker/library/alpine@3.20\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
fs.PrintDefaults()
@ -479,9 +470,9 @@ func runMirror() {
fmt.Fprintf(os.Stderr, "error reading SBOM %s: %v\n", *sbomPath, err)
os.Exit(1)
}
source = &mirror.SBOMSource{Data: data, Name: *sbomPath, Latest: *latest, Platforms: platformList}
source = &mirror.SBOMSource{Data: data, Name: *sbomPath, Platforms: platformList}
case len(purls) > 0:
source = &mirror.PURLSource{PURLs: purls, Latest: *latest, Platforms: platformList}
source = &mirror.PURLSource{PURLs: purls, Platforms: platformList}
default:
fmt.Fprintf(os.Stderr, "error: provide PURLs or --sbom\n")
fs.Usage()
@ -495,7 +486,6 @@ func runMirror() {
os.Exit(1)
}
cfg.LoadFromEnv()
printEnvWarnings(cfg)
if *storageURL != "" {
cfg.Storage.URL = *storageURL
@ -555,24 +545,22 @@ func runMirror() {
// Build proxy with the same upstream clients and authentication as serve
proxy, images := server.NewMirrorProxy(cfg, db, store, logger)
proxy.Denylist, err = denylist.New(cfg.Denylist.Packages)
if err != nil {
_ = db.Close()
fmt.Fprintf(os.Stderr, "invalid denylist: %v\n", err)
os.Exit(1) //nolint:gocritic // db closed above
}
proxy.CacheMetadata = true // mirror always caches metadata
proxy.MetadataTTL = cfg.ParseMetadataTTL()
proxy.MetadataMaxSize = cfg.ParseMetadataMaxSize()
proxy.MetadataStreamMaxSize = cfg.ParseMetadataStreamMaxSize()
// As serve does, resolve Swift source repositories in the denylist through
// the upstream, and refuse to run when one can't be.
if err := server.ConfigureDenylist(cfg, proxy); err != nil {
_ = db.Close()
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1) //nolint:gocritic // db closed above
}
m := newCommandMirror(cfg, proxy, images, db, store, logger, *concurrency)
switch s := source.(type) {
case *mirror.PURLSource:
s.Versions = m.Versions
case *mirror.SBOMSource:
s.Versions = m.Versions
m := mirror.New(proxy, db, store, logger, *concurrency)
m.Images = images
if cfg.Scanning.Enabled {
// Scanners fetch staged layers from the running server, which this
// command does not start.
m.Images = mirror.UnavailableImages{Reason: "container images cannot be mirrored by this command while scanning is enabled; use the mirror API of the running server"}
}
ctx, cancel := context.WithCancel(context.Background())
@ -596,9 +584,7 @@ func runMirror() {
return
}
closeGit := useCommandGit(cfg, proxy, m, logger)
progress, err := m.Run(ctx, source)
closeGit()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
@ -613,75 +599,6 @@ func runMirror() {
fmt.Fprintf(os.Stderr, " %s/%s@%s: %s\n", e.Ecosystem, e.Name, e.Version, e.Error)
}
}
// A partial mirror has to be visible in the exit status, or a CI step
// that prewarms the cache passes even when every download failed.
if code := mirrorExitCode(progress); code != 0 {
_ = db.Close()
os.Exit(code) //nolint:gocritic // db closed above
}
}
// newCommandMirror builds the mirror proxy mirror runs, which fetches and
// lists versions from the configured upstreams.
func newCommandMirror(cfg *config.Config, proxy *handler.Proxy, images *handler.ContainerHandler,
db *database.DB, store storage.Storage, logger *slog.Logger, workers int) *mirror.Mirror {
m := mirror.New(proxy, db, store, logger, workers)
m.Images = images
m.Bottles = handler.NewHomebrewHandler(proxy, cfg.Upstream.HomebrewAPI)
m.Charts = handler.NewHelmHandler(proxy, cfg.BaseURL, cfg.Upstream.Helm)
server.UseMirrorUpstreams(m, cfg.Upstream, proxy.HTTPClient)
if cfg.Scanning.Enabled {
refuseUnscannedMirroring(m)
}
return m
}
// useCommandGit opens the git cache for the pkg:git items of proxy mirror,
// when upstream.git is configured, and returns the function that closes it.
// Where it can't be opened, such as while the running server holds
// git.cache_dir, those items fail with the reason and the rest are mirrored.
func useCommandGit(cfg *config.Config, proxy *handler.Proxy, m *mirror.Mirror, logger *slog.Logger) func() {
git, closeGit, err := server.NewMirrorGit(cfg, proxy, logger)
if err != nil {
m.Git = mirror.UnavailableGit{Reason: fmt.Sprintf("this command can't open the git cache (%v); while the server runs, use its mirror API", err)}
return func() {}
}
if git == nil {
return func() {}
}
m.Git = git
return func() {
if err := closeGit(); err != nil {
logger.Warn("closing the git cache", "error", err)
}
}
}
// mirrorExitCode returns the exit status for a finished mirror run: 1 when any
// package version failed to mirror, 0 otherwise.
func mirrorExitCode(progress *mirror.Progress) int {
if progress != nil && progress.Failed > 0 {
return 1
}
return 0
}
// Reasons the mirror command gives for refusing items while scanning is
// enabled.
const (
mirrorScanningImagesReason = "container images cannot be mirrored by this command while scanning is enabled; use the mirror API of the running server"
mirrorScanningPackagesReason = "packages cannot be mirrored by this command while scanning is enabled; use the mirror API of the running server"
)
// refuseUnscannedMirroring makes m fail every item instead of caching it.
// Scanners fetch the staged bytes from the running server, which this command
// does not start, so anything it cached would skip the pre-cache scan and
// later be served from the cache unscanned. The mirror API's jobs run inside
// the server with its scanners and are unaffected.
func refuseUnscannedMirroring(m *mirror.Mirror) {
m.Images = mirror.UnavailableImages{Reason: mirrorScanningImagesReason}
m.RefusePackages = mirrorScanningPackagesReason
}
func printStats(db *database.DB, popular, recent int, asJSON bool) error {
@ -826,14 +743,6 @@ func formatSize(bytes int64) string {
}
}
// printEnvWarnings reports environment values LoadFromEnv ignored. serve
// logs them from server.Start; mirror never gets there.
func printEnvWarnings(cfg *config.Config) {
for _, warning := range cfg.EnvWarnings() {
fmt.Fprintf(os.Stderr, "warning: %s\n", warning)
}
}
func loadConfig(path string) (*config.Config, error) {
if path != "" {
return config.Load(path)

View file

@ -35,7 +35,6 @@ func TestServeHelpListsUpstreamEnvironmentVariables(t *testing.T) {
"PROXY_UPSTREAM_COMPOSER",
"PROXY_UPSTREAM_COMPOSER_REPOSITORY",
"PROXY_UPSTREAM_CONAN",
"PROXY_UPSTREAM_CHEF",
"PROXY_UPSTREAM_CONDA",
"PROXY_UPSTREAM_CRAN",
"PROXY_UPSTREAM_JULIA",
@ -57,24 +56,3 @@ func TestServeHelpProcess(*testing.T) {
os.Args = []string{"proxy", "serve", "-help"}
main()
}
func TestMirrorLogsEnvWarnings(t *testing.T) {
cmd := exec.Command(os.Args[0], "-test.run=^TestMirrorEnvWarningsProcess$")
// The negative size stops mirror before it opens any storage.
cmd.Env = append(os.Environ(),
"PROXY_TEST_MIRROR_ENV_WARNINGS=1",
"PROXY_CACHE_METADATA=maybe",
"PROXY_STORAGE_MAX_SIZE=-1")
output, _ := cmd.CombinedOutput()
if !strings.Contains(string(output), "PROXY_CACHE_METADATA") {
t.Errorf("mirror did not warn about PROXY_CACHE_METADATA:\n%s", output)
}
}
func TestMirrorEnvWarningsProcess(*testing.T) {
if os.Getenv("PROXY_TEST_MIRROR_ENV_WARNINGS") != "1" {
return
}
os.Args = []string{"proxy", "mirror", "pkg:npm/lodash@4.17.21"}
main()
}

View file

@ -1,132 +0,0 @@
package main
import (
"net/http"
"net/http/cgi"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/git-pkgs/proxy/internal/gitcache"
)
// gitMirrorUpstream serves team/repo, with one commit on main, from git
// http-backend, and returns the server and the commit.
func gitMirrorUpstream(t *testing.T) (*httptest.Server, string) {
t.Helper()
git := func(dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Env = []string{
"PATH=" + os.Getenv("PATH"), "HOME=" + t.TempDir(), "GIT_CONFIG_NOSYSTEM=1",
"GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example.com",
"GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example.com",
}
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
}
return strings.TrimSpace(string(out))
}
backend := ""
for _, name := range []string{"git-http-backend", "git-http-backend.exe"} {
if path := filepath.Join(git("", "--exec-path"), name); fileExistsForTest(path) {
backend = path
}
}
if backend == "" {
t.Skip("git http-backend is not installed")
}
root := t.TempDir()
bare := filepath.Join(root, "team", "repo.git")
git("", "init", "--quiet", "--bare", "--initial-branch=main", bare)
work := t.TempDir()
git("", "clone", "--quiet", bare, work)
git(work, "commit", "--quiet", "--allow-empty", "-m", "first")
git(work, "push", "--quiet", "origin", "HEAD:main")
head := git(work, "rev-parse", "HEAD")
server := httptest.NewServer(&cgi.Handler{
Path: backend,
Env: []string{"PATH=" + os.Getenv("PATH"), "GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1"},
})
t.Cleanup(server.Close)
return server, head
}
func fileExistsForTest(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// writeGitMirrorConfig writes a config with upstream.git "up" at
// upstreamURL and the git cache, storage and database under dir.
func writeGitMirrorConfig(t *testing.T, dir, upstreamURL string) string {
t.Helper()
cfg := "storage:\n" +
" url: file://" + filepath.Join(dir, "artifacts") + "\n" +
"database:\n" +
" path: " + filepath.Join(dir, "proxy.db") + "\n" +
"upstream:\n" +
" allow_loopback: true\n" +
" git:\n" +
" up: " + upstreamURL + "\n" +
"git:\n" +
" cache_dir: " + filepath.Join(dir, "git") + "\n"
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(cfg), 0o600); err != nil {
t.Fatalf("writing config: %v", err)
}
return path
}
func TestMirrorCommandCachesGitCommit(t *testing.T) {
upstream, head := gitMirrorUpstream(t)
dir := t.TempDir()
configPath := writeGitMirrorConfig(t, dir, upstream.URL)
purl := "pkg:git/" + strings.TrimPrefix(upstream.URL, "http://") + "/team/repo@" + head
output := runMirrorProcess(t, 0, "-config", configPath, purl)
if !strings.Contains(output, "1 downloaded") {
t.Errorf("output:\n%s", output)
}
cached, _ := filepath.Glob(filepath.Join(dir, "git", "up-*", "team", "repo.git"))
if len(cached) != 1 {
t.Errorf("cached repositories %v, want team/repo.git", cached)
}
output = runMirrorProcess(t, 0, "-config", configPath, purl)
if !strings.Contains(output, "1 skipped") {
t.Errorf("second run output:\n%s", output)
}
}
// TestMirrorCommandGitCacheInUse checks pkg:git items fail, pointing at the
// mirror API, while the running server holds git.cache_dir.
func TestMirrorCommandGitCacheInUse(t *testing.T) {
upstream, head := gitMirrorUpstream(t)
dir := t.TempDir()
configPath := writeGitMirrorConfig(t, dir, upstream.URL)
cache, err := gitcache.New(gitcache.Options{
CacheDir: filepath.Join(dir, "git"),
RefreshWait: time.Second,
Client: &http.Client{},
})
if err != nil {
t.Fatal(err)
}
defer func() { _ = cache.Close() }()
purl := "pkg:git/" + strings.TrimPrefix(upstream.URL, "http://") + "/team/repo@" + head
output := runMirrorProcess(t, 1, "-config", configPath, purl)
for _, want := range []string{"in use by another process", "mirror API", "1 failed"} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
}

View file

@ -1,186 +0,0 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"github.com/git-pkgs/proxy/internal/database"
)
const swiftWidgetArchive = "acme widget archive"
// swiftMirrorUpstream serves acme.widget 1.0.0 the way a Swift package
// registry does, with https://github.com/acme/widget as its source
// repository, and counts archive downloads and identifier lookups. The
// repository's identifiers are ids, or only acme.widget.
func swiftMirrorUpstream(t *testing.T, ids ...string) (srv *httptest.Server, downloads, lookups *atomic.Int64) {
t.Helper()
if len(ids) == 0 {
ids = []string{"acme.widget"}
}
identifiers, err := json.Marshal(map[string][]string{"identifiers": ids})
if err != nil {
t.Fatal(err)
}
downloads, lookups = new(atomic.Int64), new(atomic.Int64)
sum := sha256.Sum256([]byte(swiftWidgetArchive))
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Version", "1")
switch r.URL.Path {
case "/identifiers":
lookups.Add(1)
if r.URL.Query().Get("url") != "https://github.com/acme/widget" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(identifiers)
case "/acme/widget":
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"releases":{"1.0.0":{}}}`)
case "/acme/widget/1.0.0/Package.swift":
w.Header().Set("Content-Type", "text/x-swift")
_, _ = io.WriteString(w, "// swift-tools-version:5.9\n")
case "/acme/widget/1.0.0":
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{"id":"acme.widget","version":"1.0.0","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(sum[:]))
case "/acme/widget/1.0.0.zip":
downloads.Add(1)
w.Header().Set("Content-Type", "application/zip")
_, _ = io.WriteString(w, swiftWidgetArchive)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
return srv, downloads, lookups
}
// writeSwiftMirrorConfig writes a config using upstream as the Swift
// registry, denying the denied package PURLs.
func writeSwiftMirrorConfig(t *testing.T, dir, upstream string, denied ...string) string {
t.Helper()
cfg := "storage:\n" +
" url: file://" + filepath.Join(dir, "artifacts") + "\n" +
"database:\n" +
" path: " + filepath.Join(dir, "proxy.db") + "\n" +
"upstream:\n" +
" swift: " + upstream + "\n" +
" allow_loopback: true\n"
if len(denied) > 0 {
cfg += "denylist:\n packages:\n"
for _, p := range denied {
cfg += " - " + p + "\n"
}
}
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(cfg), 0o600); err != nil {
t.Fatalf("writing config: %v", err)
}
return path
}
func cachedArtifacts(t *testing.T, dir string) int64 {
t.Helper()
db, err := database.Open(filepath.Join(dir, "proxy.db"))
if err != nil {
t.Fatalf("opening database: %v", err)
}
defer func() { _ = db.Close() }()
stats, err := db.GetCacheStats()
if err != nil {
t.Fatalf("GetCacheStats: %v", err)
}
return stats.TotalArtifacts
}
// Without a denylist the fake registry mirrors the release, so the tests
// below fail for the denylist and not for the setup.
func TestMirrorSwiftRelease(t *testing.T) {
upstream, downloads, _ := swiftMirrorUpstream(t)
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL)
output := runMirrorProcess(t, 0, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "1 downloaded") {
t.Errorf("output:\n%s", output)
}
if n := downloads.Load(); n != 1 {
t.Errorf("archive downloads = %d, want 1", n)
}
if n := cachedArtifacts(t, dir); n != 1 {
t.Errorf("cached artifacts = %d, want 1", n)
}
}
// A denylist entry naming the package by its source repository blocks the
// mirror download of the registry identity it resolves to.
func TestMirrorDenylistSwiftSourceEntry(t *testing.T) {
upstream, downloads, _ := swiftMirrorUpstream(t)
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL, "pkg:swift/github.com/acme/widget@1.0.0")
output := runMirrorProcess(t, 1, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "0 downloaded") || !strings.Contains(output, "1 failed") {
t.Errorf("output:\n%s", output)
}
if n := downloads.Load(); n != 0 {
t.Errorf("archive downloads = %d, want 0", n)
}
if n := cachedArtifacts(t, dir); n != 0 {
t.Errorf("cached artifacts = %d, want 0", n)
}
}
// The registry may list several identities for one repository. A source
// entry denies all of them, not only the first.
func TestMirrorDenylistSwiftSourceEntryEveryIdentity(t *testing.T) {
upstream, downloads, _ := swiftMirrorUpstream(t, "other.widget", "acme.widget")
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL, "pkg:swift/github.com/acme/widget@1.0.0")
output := runMirrorProcess(t, 1, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "0 downloaded") || !strings.Contains(output, "1 failed") {
t.Errorf("output:\n%s", output)
}
if n := downloads.Load(); n != 0 {
t.Errorf("archive downloads = %d, want 0", n)
}
}
// A source repository entry the registry can't resolve stops the mirror
// before it downloads anything, as it stops serve: left out, it would let
// the version through.
func TestMirrorDenylistUnresolvedSwiftSource(t *testing.T) {
upstream, downloads, lookups := swiftMirrorUpstream(t)
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL, "pkg:swift/github.com/acme/unknown@1.0.0")
output := runMirrorProcess(t, 1, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "configuring denylist") || !strings.Contains(output, "github.com/acme/unknown") {
t.Errorf("output does not report the unresolved entry:\n%s", output)
}
if strings.Contains(output, "Mirror complete") {
t.Errorf("mirror ran:\n%s", output)
}
if lookups.Load() == 0 {
t.Error("the entry was not looked up")
}
if n := downloads.Load(); n != 0 {
t.Errorf("archive downloads = %d, want 0", n)
}
}

View file

@ -1,214 +0,0 @@
package main
import (
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/mirror"
)
// mirrorTestEnv carries the mirror command's arguments into the helper
// process, one per line.
const mirrorTestEnv = "PROXY_TEST_MIRROR_ARGS"
// writeScanningMirrorConfig writes a config with scanning enabled and storage
// and database under dir. The scanner address is never reached.
func writeScanningMirrorConfig(t *testing.T, dir string) string {
t.Helper()
cfg := "storage:\n" +
" url: file://" + filepath.Join(dir, "artifacts") + "\n" +
"database:\n" +
" path: " + filepath.Join(dir, "proxy.db") + "\n" +
"scanning:\n" +
" enabled: true\n" +
" signing_key: test-signing-key\n" +
" scanners:\n" +
" - name: test\n" +
" url: http://127.0.0.1:1/scan\n"
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(cfg), 0o600); err != nil {
t.Fatalf("writing config: %v", err)
}
return path
}
// runMirrorProcess runs proxy mirror with args in a helper process and
// checks that it exits with wantCode.
func runMirrorProcess(t *testing.T, wantCode int, args ...string) string {
t.Helper()
cmd := exec.Command(os.Args[0], "-test.run=^TestMirrorHelperProcess$")
cmd.Env = append(os.Environ(), mirrorTestEnv+"="+strings.Join(args, "\n"))
output, err := cmd.CombinedOutput()
code := 0
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
code = exitErr.ExitCode()
} else if err != nil {
t.Fatalf("proxy mirror %v: %v\n%s", args, err, output)
}
if code != wantCode {
t.Fatalf("proxy mirror %v exited with status %d, want %d\n%s", args, code, wantCode, output)
}
return string(output)
}
// With scanning enabled, proxy mirror refuses packages rather than caching
// them unscanned: scanners fetch staged bytes from the running server, which
// the command does not start.
// A refused package counts as failed, so the command exits 1.
func TestMirrorCommandRefusesPackagesWhileScanning(t *testing.T) {
dir := t.TempDir()
configPath := writeScanningMirrorConfig(t, dir)
output := runMirrorProcess(t, 1, "-config", configPath, "pkg:npm/lodash@4.17.21")
for _, want := range []string{mirrorScanningPackagesReason, "0 downloaded", "1 failed"} {
if !strings.Contains(output, want) {
t.Errorf("output missing %q:\n%s", want, output)
}
}
db, err := database.Open(filepath.Join(dir, "proxy.db"))
if err != nil {
t.Fatalf("opening database: %v", err)
}
defer func() { _ = db.Close() }()
stats, err := db.GetCacheStats()
if err != nil {
t.Fatalf("GetCacheStats: %v", err)
}
if stats.TotalArtifacts != 0 {
t.Errorf("cached artifacts = %d, want 0", stats.TotalArtifacts)
}
var stored []string
_ = filepath.WalkDir(filepath.Join(dir, "artifacts"), func(path string, d os.DirEntry, err error) error {
if err == nil && !d.IsDir() {
stored = append(stored, path)
}
return nil
})
if len(stored) != 0 {
t.Errorf("files written to storage: %v", stored)
}
}
// A dry run caches nothing, so it still lists what would be mirrored.
func TestMirrorCommandDryRunWhileScanning(t *testing.T) {
configPath := writeScanningMirrorConfig(t, t.TempDir())
output := runMirrorProcess(t, 0, "-config", configPath, "-dry-run", "pkg:npm/lodash@4.17.21")
if !strings.Contains(output, "Would mirror 1 package versions") {
t.Errorf("dry run output:\n%s", output)
}
if strings.Contains(output, mirrorScanningPackagesReason) {
t.Errorf("dry run reported the refusal:\n%s", output)
}
}
func TestMirrorCommandSBOMComponentVersion(t *testing.T) {
dir := t.TempDir()
configPath := writeScanningMirrorConfig(t, dir)
sbomPath := filepath.Join(dir, "bom.json")
data := []byte(`{"components":[{"name":"app","version":"1.0","purl":"pkg:docker/ghcr.io/team/app"}]}`)
if err := os.WriteFile(sbomPath, data, 0600); err != nil {
t.Fatal(err)
}
output := runMirrorProcess(t, 0, "-config", configPath, "-dry-run", "-sbom", sbomPath)
for _, want := range []string{"Would mirror 1 package versions", "pkg:docker/ghcr.io/team/app@1.0"} {
if !strings.Contains(output, want) {
t.Errorf("dry run missing %q:\n%s", want, output)
}
}
}
func TestRefuseUnscannedMirroring(t *testing.T) {
m := mirror.New(nil, nil, nil, nil, 1)
refuseUnscannedMirroring(m)
if m.RefusePackages != mirrorScanningPackagesReason {
t.Errorf("RefusePackages = %q, want %q", m.RefusePackages, mirrorScanningPackagesReason)
}
images, ok := m.Images.(mirror.UnavailableImages)
if !ok || images.Reason != mirrorScanningImagesReason {
t.Errorf("Images = %#v, want UnavailableImages with the scanning reason", m.Images)
}
}
func TestMirrorHelperProcess(*testing.T) {
args := os.Getenv(mirrorTestEnv)
if args == "" {
return
}
os.Args = append([]string{"proxy", "mirror"}, strings.Split(args, "\n")...)
main()
}
func TestMirrorExitCode(t *testing.T) {
tests := []struct {
name string
progress *mirror.Progress
want int
}{
{"nil progress", nil, 0},
{"everything mirrored", &mirror.Progress{Total: 2, Completed: 2}, 0},
{"everything cached", &mirror.Progress{Total: 2, Skipped: 2}, 0},
{"one failure", &mirror.Progress{Total: 2, Completed: 1, Failed: 1}, 1},
{"all failed", &mirror.Progress{Total: 3, Failed: 3}, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := mirrorExitCode(tt.progress); got != tt.want {
t.Errorf("mirrorExitCode() = %d, want %d", got, tt.want)
}
})
}
}
// proxy mirror used to exit 0 after printing "N failed", so a CI step that
// prewarms the cache passed even when every download failed (#500). An
// ecosystem the resolver rejects fails without touching the network.
func TestMirrorExitsNonZeroWhenDownloadsFail(t *testing.T) {
dir := t.TempDir()
cmd := exec.Command(os.Args[0], "-test.run=^TestMirrorFailureProcess$")
cmd.Env = append(os.Environ(),
"PROXY_TEST_MIRROR_FAILURE=1",
"PROXY_TEST_MIRROR_DIR="+dir,
)
output, err := cmd.CombinedOutput()
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("mirror with a failed download exited with %v, want status 1\n%s", err, output)
}
if code := exitErr.ExitCode(); code != 1 {
t.Fatalf("exit status = %d, want 1\n%s", code, output)
}
if !strings.Contains(string(output), "1 failed") {
t.Errorf("output does not report the failure:\n%s", output)
}
if !strings.Contains(string(output), "bogus/foo@1.0.0") {
t.Errorf("output does not list the failed package:\n%s", output)
}
}
func TestMirrorFailureProcess(*testing.T) {
if os.Getenv("PROXY_TEST_MIRROR_FAILURE") != "1" {
return
}
dir := os.Getenv("PROXY_TEST_MIRROR_DIR")
os.Args = []string{
"proxy", "mirror",
"--storage-url", "file://" + filepath.Join(dir, "store"),
"--database-path", filepath.Join(dir, "cache.db"),
"pkg:bogus/foo@1.0.0",
}
main()
}

View file

@ -14,19 +14,6 @@ base_url: "http://localhost:8080"
# Set to "0" to disable the timeout. Default: "30s".
# http_timeout: "30s"
# Response write deadline, including download and scan time before sending data.
# Set to "0" to disable. Default: "5m".
# write_timeout: "5m"
# Timeout for each upstream artifact download while serving requests; excludes
# scanning. Set to "0" to disable. Default: "5m". Does not limit proxy mirror.
# artifact_timeout: "5m"
# Register DELETE /api/cache and show Delete buttons on the UI's version page,
# for removing cached artifacts so they are fetched again. Has no
# authentication: restrict /api/cache and /ui at the ingress first.
# cache_delete: false
# Largest upstream artifact to fetch (e.g. "2GB"). Larger ones are refused
# and not cached. Default: unlimited.
# artifact_max_size: ""
@ -104,24 +91,19 @@ storage:
# The default only applies to ecosystems that support retention; naming an
# ecosystem or package that does not is a configuration error. See
# docs/configuration.md for the supported ecosystems.
# Generic downloads require an explicit ecosystem or package rule;
# they never inherit default, since an upstream release may disappear.
retention:
default: ""
# ecosystems:
# npm: "30d"
# generic: "30d"
# packages:
# "pkg:npm/lodash": "0"
# "pkg:generic/owner%2Frepo": "90d"
# How often to look for expired artifacts; at least "1m".
sweep_interval: "10m"
# Store fetched artifacts. Set to false to stream every download from
# upstream without storing it; metadata filtering, cooldown and the
# denylist still apply. Useful when another cache sits in front of the
# proxy. false is incompatible with direct_serve, scanning, mirror_api,
# mirror.schedules and mirror.refresh.
# proxy. false is incompatible with direct_serve, scanning and mirror_api.
cache_artifacts: true
# Redirect cached artifact downloads to presigned storage URLs (HTTP 302)
@ -189,10 +171,6 @@ upstream:
# npm_routes:
# "@example/*": "https://npm.example.com/registry"
# Follow the npm registry's change feed, so a new release is fetched on the
# next request instead of after metadata_ttl. Requires cache_metadata.
# npm_change_feed: true
# Cargo sparse index URL
cargo: "https://index.crates.io"
@ -223,12 +201,6 @@ upstream:
# Maven repository URL (used by /maven endpoint)
maven: "https://repo1.maven.org/maven2"
# Additional Maven repositories, served at /maven/{name}/. A name must not
# be the start of a group ID (such as "org" or "tools"), since it would hide
# those groups in the default repository.
# maven_repositories:
# grails: "https://repo.grails.org/grails/restricted"
# Gradle Plugin Portal Maven URL (fallback for plugin marker artifacts)
gradle_plugin_portal: "https://plugins.gradle.org/m2"
@ -249,18 +221,9 @@ upstream:
# composer_routes:
# "example/*": "https://composer.example.com/packages"
# Follow the change feed of the Packagist at composer, so a new release is
# fetched on the next request instead of after metadata_ttl. Requires
# cache_metadata.
# composer_change_feed: true
# Conan registry URL
conan: "https://center.conan.io"
# Chef Supermarket URL (used by /chef/). Point it at a private
# Supermarket to proxy that instead.
chef: "https://supermarket.chef.io"
# Conda channel base URL
conda: "https://conda.anaconda.org"
@ -324,14 +287,6 @@ upstream:
# archlinux: "https://geo.mirror.pkgbuild.com"
# archlinuxarm: "https://<region>.mirror.archlinuxarm.org"
# Named FreeBSD pkg mirrors (used by /freebsd/{name}/), for ports and
# pkgbase repositories alike.
# Defaults to {"freebsd": "https://pkg.freebsd.org"} when empty;
# configuring any entry replaces that default.
# freebsd:
# freebsd: "https://pkg.freebsd.org"
# poudriere: "https://packages.example.com"
# Named generic HTTP upstreams (used by /generic/{name}/). The remaining
# request path and query are appended to the upstream URL. GitHub release
# assets ({owner}/{repo}/releases/download/{tag}/{asset}) are cached
@ -341,18 +296,6 @@ upstream:
# github-api: "https://api.github.com"
# apache: "https://archive.apache.org"
# Paths a generic upstream may serve, as path.Match patterns for the path
# after /generic/{name}/; a pattern ending in /** also matches everything
# nested below its prefix. Other paths answer 404. Required for an upstream
# that upstream.auth has credentials for, such as a GitHub token for
# api.github.com; use a fine-grained token with read-only access to public
# repositories.
# generic_paths:
# github-api:
# - "repos/*/*/releases"
# - "repos/*/*/releases/**"
# - "repos/*/*/tags"
# Version-pinned paths of generic upstreams, cached immutably like GitHub
# release assets. Each pattern must match the whole path after
# /generic/{name}/ and have `version` and `file` named groups; an optional
@ -361,15 +304,6 @@ upstream:
# apache:
# - 'dist/(?P<name>maven)/maven-3/(?P<version>[^/]+)/binaries/(?P<file>[^/]+)'
# Named Git servers (used by /git/{name}/{repo}), served read-only from
# cached bare repositories. repos limits which repositories may be served
# and is required when upstream.auth has credentials for the server.
# git:
# github: "https://github.com"
# wikimedia:
# url: "https://gerrit.wikimedia.org/r"
# repos: ["mediawiki/**"]
# Authentication for upstream registries
# Keys are absolute URL scopes. Scheme, host, effective port, and path
# segment boundaries must match; the longest matching scope wins.
@ -410,20 +344,11 @@ upstream:
# "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com":
# type: ecr
# Git repository cache, used when upstream.git is set
# git:
# cache_dir: ./cache/git # local disk, used by this process only
# max_size: "" # evict least recently used repositories past this
# max_repo_size: 5GB # refuse a repository that grows past this
# refresh_wait: 5s # then serve the cached copy if the upstream check is slow
# stale_max_age: 24h # never serve a copy not checked for this long
# Gradle HttpBuildCache configuration
gradle:
build_cache:
# PUT uploads are rejected by default; set to false on the instance
# your CI jobs upload to
read_only: true
# Set to true to disable PUT uploads (read-only cache mode)
read_only: false
# Maximum accepted Gradle cache upload body size
# Required and must be > 0
@ -451,33 +376,6 @@ gradle:
# npm_scopes: ["@acme"]
# composer_vendors: ["acme"]
# Project inventory (disabled by default). Repositories upload an SBOM to
# PUT /api/projects/{project}/sbom, and the UI shows which project uses
# which package versions. Project names are visible on the unauthenticated UI.
# projects:
# enabled: true
# token: "${PROXY_PROJECTS_TOKEN}"
# # Drop projects that have not uploaded for this long ("" keeps them)
# max_age: "90d"
# Re-mirror container image tags on a schedule, so a moved tag is picked up
# even when nobody pulls it. An unchanged tag costs one HEAD request.
# mirror:
# schedules:
# - images: ["pkg:docker/library/alpine@3.20", "pkg:docker/library/nginx@stable"]
# interval: "6h"
# platforms: ["linux/amd64"]
#
# Mirror new releases of the most used packages in the background, so the
# first install after a release doesn't wait on the upstream. Cooldown and
# the denylist apply.
# refresh:
# interval: "24h"
# top: 50 # packages per ecosystem, by recent downloads
# since: "14d"
# max_versions: 200 # per run; default no cap
# max_bytes: "2GB" # per run; default no cap
# Health endpoint configuration.
health:
# Minimum time between storage backend probes.
@ -492,13 +390,6 @@ health:
# to spot malicious releases before they're pulled into projects.
# Supported durations: "7d" (days), "48h" (hours), "30m" (minutes), "0" (disabled)
cooldown:
# Independent policies selected through /cooldown/{name}/{registry}/.
# profiles:
# 3d:
# default: "3d"
# 7d:
# default: "7d"
# Global default cooldown for all ecosystems
# default: "3d"

View file

@ -33,10 +33,6 @@ spec:
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.extraInitContainers }}
initContainers:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: {{ include "proxy.image" . | quote }}
@ -74,11 +70,6 @@ spec:
readOnly: true
- name: data
mountPath: {{ .Values.persistence.mountPath }}
- name: tmp
mountPath: /tmp
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: config
configMap:
@ -93,16 +84,6 @@ spec:
{{- else }}
emptyDir: {}
{{- end }}
- name: tmp
emptyDir:
{{- with .Values.tmpDir.sizeLimit }}
sizeLimit: {{ . }}
{{- else }}
{}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

Some files were not shown because too many files have changed in this diff Show more