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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Resolution order: package override, then ecosystem override, then global default
| Conda | Python/R | Yes | ✓ |
| CRAN | R | | ✓ |
| Julia | Julia | | ✓ |
| Swift | Swift | | ✓ |
| Container | Docker/OCI | | ✓ |
| Debian | Debian/Ubuntu | | ✓ |
| RPM | RHEL/Fedora | | ✓ |
Expand All @@ -47,7 +48,6 @@ Resolution order: package override, then ecosystem override, then global default
| Chef | Chef | | ✗ |
| Generic | Any | | ✗ |
| Helm | Kubernetes | | ✗ |
| Swift | Swift | | ✗ |
| 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.
Expand Down Expand Up @@ -340,6 +340,25 @@ ENV["JULIA_PKG_SERVER"] = "http://localhost:8080/julia"
using Pkg; Pkg.update()
```

### Swift

Configure the proxy as the default registry for the current Swift package:

```bash
swift package-registry set --allow-insecure-http http://localhost:8080/swift
```

Registry dependencies use their scoped package identifier in `Package.swift`:

```swift
dependencies: [
.package(id: "apple.swift-argument-parser", from: "1.2.0")
]
```

The proxy supports dependency resolution and source downloads. Publishing with
`swift package-registry publish` is not supported.

### Docker / Container Registry

Configure Docker to use the proxy as a registry mirror in `/etc/docker/daemon.json`:
Expand Down Expand Up @@ -513,6 +532,7 @@ PROXY_DATABASE_URL=postgres://user:pass@localhost/proxy?sslmode=disable
PROXY_LOG_LEVEL=info
PROXY_LOG_FORMAT=text
PROXY_ACCESS_LOG_PATH=/var/log/proxy/access.jsonl
PROXY_UPSTREAM_SWIFT=https://tuist.dev/api/registry/swift
```

### Configuration File
Expand Down Expand Up @@ -540,6 +560,7 @@ access_log:
upstream:
npm: "https://registry.npmjs.org"
cargo: "https://index.crates.io"
swift: "https://tuist.dev/api/registry/swift"

# Optional: version cooldown (see above)
cooldown:
Expand Down Expand Up @@ -767,6 +788,7 @@ Recently cached:
| `GET /conda/*` | Conda/Anaconda protocol |
| `GET /cran/*` | CRAN (R) protocol |
| `GET /julia/*` | Julia Pkg server protocol |
| `GET /swift/*` | Swift Package Registry v1 protocol |
| `GET /helm/{repository}/*` | HTTP Helm chart repository protocol |
| `GET /v2/*` | OCI/Docker registry protocol |
| `GET /apk/{repository}/*` | Alpine APK repository protocol |
Expand Down
1 change: 1 addition & 0 deletions cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func runServe() {
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")
fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_SWIFT Swift Package Registry upstream URL\n")
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")
Expand Down
3 changes: 3 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ upstream:
# Julia package server URL
julia: "https://pkg.julialang.org"

# Swift Package Registry URL (used by /swift endpoint)
swift: "https://tuist.dev/api/registry/swift"

# Default OCI registry URL for unprefixed /v2 requests
oci_default: "https://registry-1.docker.io"

Expand Down
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ HTTP protocol handlers for each registry type.
- `handleIndex()` - Proxy sparse index
- `handleDownload()` - Serve cached crate

**SwiftHandler:**
- Proxies the Swift Package Registry v1 read endpoints
- Rewrites release URLs and caches source archives

### `internal/server`

HTTP server setup, web UI, and API handlers.
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ Each upstream used by a built-in package route can be set in YAML or JSON under
| `upstream.conda` | `PROXY_UPSTREAM_CONDA` | `https://conda.anaconda.org` |
| `upstream.cran` | `PROXY_UPSTREAM_CRAN` | `https://cloud.r-project.org` |
| `upstream.julia` | `PROXY_UPSTREAM_JULIA` | `https://pkg.julialang.org` |
| `upstream.swift` | `PROXY_UPSTREAM_SWIFT` | `https://tuist.dev/api/registry/swift` |
| `upstream.oci_default` | `PROXY_UPSTREAM_OCI_DEFAULT` | `https://registry-1.docker.io` |
| `upstream.debian` | `PROXY_UPSTREAM_DEBIAN` | `http://deb.debian.org/debian` |
| `upstream.rpm` | `PROXY_UPSTREAM_RPM` | `https://dl.fedoraproject.org/pub/fedora/linux` |
Expand Down
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ import (
"gopkg.in/yaml.v3"
)

// DefaultSwiftUpstream is the Swift Package Registry used when none is configured.
const DefaultSwiftUpstream = "https://tuist.dev/api/registry/swift"

// Config holds all configuration for the proxy server.
type Config struct {
// Listen is the address to listen on (e.g., ":8080", "127.0.0.1:8080").
Expand Down Expand Up @@ -404,6 +407,10 @@ type UpstreamConfig struct {
// Default: https://registry-1.docker.io
OCIDefault string `json:"oci_default" yaml:"oci_default"`

// Swift is the upstream Swift Package Registry URL.
// Default: https://tuist.dev/api/registry/swift
Swift string `json:"swift" yaml:"swift"`

// Debian is the upstream APT repository base URL.
// Example: http://archive.ubuntu.com/ubuntu would get Ubuntu.
// Default: http://deb.debian.org/debian
Expand Down Expand Up @@ -599,6 +606,7 @@ func Default() *Config {
Conda: "https://conda.anaconda.org",
CRAN: "https://cloud.r-project.org",
Julia: "https://pkg.julialang.org",
Swift: DefaultSwiftUpstream,
OCIDefault: "https://registry-1.docker.io",
Debian: "http://deb.debian.org/debian",
RPM: "https://dl.fedoraproject.org/pub/fedora/linux",
Expand Down Expand Up @@ -688,6 +696,7 @@ func setEnvStringSlice(dst *[]string, key string) {
// - PROXY_LOG_LEVEL
// - PROXY_LOG_FORMAT
// - PROXY_ACCESS_LOG_PATH
// - PROXY_UPSTREAM_SWIFT
// - PROXY_HEALTH_STORAGE_PROBE_INTERVAL
func (c *Config) LoadFromEnv() {
setEnvString(&c.Listen, "PROXY_LISTEN")
Expand Down Expand Up @@ -728,6 +737,7 @@ func (c *Config) LoadFromEnv() {
setEnvString(&c.Upstream.Conda, "PROXY_UPSTREAM_CONDA")
setEnvString(&c.Upstream.CRAN, "PROXY_UPSTREAM_CRAN")
setEnvString(&c.Upstream.Julia, "PROXY_UPSTREAM_JULIA")
setEnvString(&c.Upstream.Swift, "PROXY_UPSTREAM_SWIFT")
setEnvString(&c.Upstream.OCIDefault, "PROXY_UPSTREAM_OCI_DEFAULT")
setEnvString(&c.Upstream.Debian, "PROXY_UPSTREAM_DEBIAN")
setEnvString(&c.Upstream.RPM, "PROXY_UPSTREAM_RPM")
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func upstreamConfigValues(upstream UpstreamConfig) map[string]string {
"conda": upstream.Conda,
"cran": upstream.CRAN,
"julia": upstream.Julia,
"swift": upstream.Swift,
"oci_default": upstream.OCIDefault,
"debian": upstream.Debian,
"rpm": upstream.RPM,
Expand Down Expand Up @@ -64,6 +65,7 @@ func defaultUpstreamValues() map[string]string {
"conda": "https://conda.anaconda.org",
"cran": "https://cloud.r-project.org",
"julia": "https://pkg.julialang.org",
"swift": "https://tuist.dev/api/registry/swift",
"oci_default": "https://registry-1.docker.io",
"debian": "http://deb.debian.org/debian",
"rpm": "https://dl.fedoraproject.org/pub/fedora/linux",
Expand Down Expand Up @@ -92,6 +94,7 @@ func upstreamEnvironmentVariables() map[string]string {
"conda": "PROXY_UPSTREAM_CONDA",
"cran": "PROXY_UPSTREAM_CRAN",
"julia": "PROXY_UPSTREAM_JULIA",
"swift": "PROXY_UPSTREAM_SWIFT",
"oci_default": "PROXY_UPSTREAM_OCI_DEFAULT",
"debian": "PROXY_UPSTREAM_DEBIAN",
"rpm": "PROXY_UPSTREAM_RPM",
Expand Down Expand Up @@ -370,6 +373,7 @@ upstream:
conda: "https://upstream.example.com/conda"
cran: "https://upstream.example.com/cran"
julia: "https://upstream.example.com/julia"
swift: "https://upstream.example.com/swift"
oci_default: "https://upstream.example.com/oci_default"
debian: "https://upstream.example.com/debian"
rpm: "https://upstream.example.com/rpm"
Expand Down
37 changes: 29 additions & 8 deletions internal/enrichment/enrichment.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"time"

"github.com/git-pkgs/proxy/internal/packageurl"
"github.com/git-pkgs/purl"
"github.com/git-pkgs/registries"
_ "github.com/git-pkgs/registries/all" // Import all registry implementations
Expand Down Expand Up @@ -67,7 +68,10 @@ type VulnInfo struct {

// EnrichPackage fetches metadata for a package from registry APIs.
func (s *Service) EnrichPackage(ctx context.Context, ecosystem, name string) (*PackageInfo, error) {
purlStr := purl.MakePURLString(ecosystem, name, "")
purlStr := packageurl.MakeString(ecosystem, name, "")
if purlStr == "" {
return nil, nil
}

pkg, err := registries.FetchPackageFromPURL(ctx, purlStr, s.regClient)
if err != nil {
Expand Down Expand Up @@ -102,7 +106,10 @@ func (s *Service) EnrichPackage(ctx context.Context, ecosystem, name string) (*P

// EnrichVersion fetches metadata for a specific package version.
func (s *Service) EnrichVersion(ctx context.Context, ecosystem, name, version string) (*VersionInfo, error) {
purlStr := purl.MakePURLString(ecosystem, name, version)
purlStr := packageurl.MakeString(ecosystem, name, version)
if purlStr == "" {
return nil, nil
}

ver, err := registries.FetchVersionFromPURL(ctx, purlStr, s.regClient)
if err != nil {
Expand Down Expand Up @@ -134,9 +141,14 @@ func (s *Service) EnrichVersion(ctx context.Context, ecosystem, name, version st

// BulkEnrichPackages fetches metadata for multiple packages in parallel.
func (s *Service) BulkEnrichPackages(ctx context.Context, packages []struct{ Ecosystem, Name string }) map[string]*PackageInfo {
purls := make([]string, len(packages))
for i, pkg := range packages {
purls[i] = purl.MakePURLString(pkg.Ecosystem, pkg.Name, "")
purls := make([]string, 0, len(packages))
for _, pkg := range packages {
if purlStr := packageurl.MakeString(pkg.Ecosystem, pkg.Name, ""); purlStr != "" {
purls = append(purls, purlStr)
}
}
if len(purls) == 0 {
return map[string]*PackageInfo{}
}

pkgData := registries.BulkFetchPackages(ctx, purls, s.regClient)
Expand All @@ -147,7 +159,10 @@ func (s *Service) BulkEnrichPackages(ctx context.Context, packages []struct{ Eco
continue
}

p, _ := purl.Parse(purlStr)
p, err := purl.Parse(purlStr)
if err != nil {
continue
}
info := &PackageInfo{
Ecosystem: p.Type,
Name: pkg.Name,
Expand All @@ -174,7 +189,10 @@ func (s *Service) BulkEnrichPackages(ctx context.Context, packages []struct{ Eco

// CheckVulnerabilities queries for vulnerabilities affecting a package version.
func (s *Service) CheckVulnerabilities(ctx context.Context, ecosystem, name, version string) ([]VulnInfo, error) {
p := purl.MakePURL(ecosystem, name, version)
p := packageurl.Make(ecosystem, name, version)
if p == nil {
return nil, nil
}

vulnList, err := s.vulnSource.Query(ctx, p)
if err != nil {
Expand Down Expand Up @@ -211,7 +229,10 @@ func (s *Service) IsOutdated(currentVersion, latestVersion string) bool {

// GetLatestVersion fetches the latest version for a package.
func (s *Service) GetLatestVersion(ctx context.Context, ecosystem, name string) (string, error) {
purlStr := purl.MakePURLString(ecosystem, name, "")
purlStr := packageurl.MakeString(ecosystem, name, "")
if purlStr == "" {
return "", nil
}

latest, err := registries.FetchLatestVersionFromPURL(ctx, purlStr, s.regClient)
if err != nil {
Expand Down
31 changes: 31 additions & 0 deletions internal/enrichment/enrichment_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package enrichment

import (
"context"
"log/slog"
"os"
"testing"
Expand All @@ -23,6 +24,36 @@ func TestNew(t *testing.T) {
}
}

func TestSwiftRegistryIdentitySkipsPURLDependentLookups(t *testing.T) {
svc := New(slog.New(slog.NewTextHandler(os.Stdout, nil)))
ctx := context.Background()

packageInfo, err := svc.EnrichPackage(ctx, "swift", "apple/example")
if err != nil || packageInfo != nil {
t.Errorf("EnrichPackage() = %#v, %v; want nil, nil", packageInfo, err)
}

versionInfo, err := svc.EnrichVersion(ctx, "swift", "apple/example", "1.2.3")
if err != nil || versionInfo != nil {
t.Errorf("EnrichVersion() = %#v, %v; want nil, nil", versionInfo, err)
}

vulnerabilities, err := svc.CheckVulnerabilities(ctx, "swift", "apple/example", "1.2.3")
if err != nil || vulnerabilities != nil {
t.Errorf("CheckVulnerabilities() = %#v, %v; want nil, nil", vulnerabilities, err)
}

latest, err := svc.GetLatestVersion(ctx, "swift", "apple/example")
if err != nil || latest != "" {
t.Errorf("GetLatestVersion() = %q, %v; want empty string, nil", latest, err)
}

packages := []struct{ Ecosystem, Name string }{{Ecosystem: "swift", Name: "apple/example"}}
if got := svc.BulkEnrichPackages(ctx, packages); len(got) != 0 {
t.Errorf("BulkEnrichPackages() = %#v, want empty result", got)
}
}

func TestIsOutdated(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := New(logger)
Expand Down
Loading
Loading