Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-10 16:19:16 -05:00
pkg-proxy/docs/swagger/docs.go

1140 lines
38 KiB
Go
Raw Permalink Normal View History

2026-03-11 17:18:29 +00:00
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
package swagger
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
Add pre-cache artifact scanning hook (trivy/ClamAV/Wiz/custom) (#298) Runs fetched artifacts through pluggable external scanners after they're staged in storage but before they're committed to the cache DB, so a block verdict deletes the object instead of ever exposing it to a client. Scanners pull the staged bytes themselves via a short-lived HMAC-signed internal route rather than the proxy pushing bytes to them, keeping the mechanism storage-backend-agnostic and avoiding uploading potentially huge artifacts through the proxy's own egress. Hardening baked in from the start: the internal scan-fetch route is gated both at router-mount time and in the handler so it's inert whenever scanning is disabled or unsigned; the signing key is mandatory whenever scanning is enabled, enforced directly in scanner.NewGroup rather than relying on callers to invoke config validation; the scan call and the delete-on-block cleanup both run on a context detached from the client's, so a client disconnecting mid-scan can't be mistaken for a scanner failure, doesn't cause a legitimate artifact to be deleted, and doesn't leave a genuinely blocked artifact's bytes orphaned in storage; and scanner infrastructure errors (connection failures, internal hostnames) are never forwarded verbatim to anonymous clients, only a generic message. The scan-error metric also correctly distinguishes a scanner's own timeout from being cancelled because a sibling scanner already decided the verdict. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 05:06:44 -04:00
"/_internal/scan-fetch": {
"get": {
"description": "Streams the exact bytes staged in storage for a pre-cache security scan.\nRequires a short-lived HMAC-signed token minted by the proxy itself and\ndelivered via the fetch_url field of the scan notify request (see the\nArtifact Scanning section of docs/configuration.md). Not part of the\npublic API; restrict access to the scanner network at the ingress layer.",
"produces": [
"application/octet-stream"
],
"tags": [
"scanning"
],
"summary": "Fetch a staged artifact for scanning",
"parameters": [
{
"type": "string",
"description": "Storage path of the staged artifact",
"name": "path",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "Token expiry, Unix seconds",
"name": "exp",
"in": "query",
"required": true
},
{
"type": "string",
"description": "HMAC-SHA256 signature over the string path|exp",
"name": "sig",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"403": {
"description": "invalid, expired, or tampered token",
"schema": {
"type": "string"
}
},
"404": {
"description": "object not found in storage, or scanning is not configured",
"schema": {
"type": "string"
}
}
}
}
},
"/api/bulk": {
"post": {
"consumes": [
"application/json"
],
2026-03-11 17:18:29 +00:00
"produces": [
"application/json"
],
"tags": [
"api"
2026-03-11 17:18:29 +00:00
],
"summary": "Bulk package lookup by PURL",
2026-03-11 17:18:29 +00:00
"parameters": [
{
"description": "PURLs",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/server.BulkRequest"
}
2026-03-11 17:18:29 +00:00
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.BulkResponse"
2026-03-11 17:18:29 +00:00
}
},
"400": {
"description": "Bad Request",
2026-03-11 17:18:29 +00:00
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/api/hosted/{ecosystem}/{name}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"hosted"
],
"summary": "List pushed package archives",
"parameters": [
{
"type": "string",
"description": "Ecosystem",
"name": "ecosystem",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Package name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.HostedListResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
}
}
}
},
"/api/hosted/{ecosystem}/{name}/{version}": {
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Stores the request body as the archive for name@version. It is served in place of the upstream archive with the same version and is never evicted.",
"consumes": [
"application/octet-stream"
],
"produces": [
"application/json"
],
"tags": [
"hosted"
],
"summary": "Push a package archive",
"parameters": [
{
"type": "string",
"description": "Ecosystem (npm or composer)",
"name": "ecosystem",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Package name, including an npm scope or the Composer vendor",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Version",
"name": "version",
"in": "path",
"required": true
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/server.HostedPushResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
},
"413": {
"description": "Request Entity Too Large",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Removes the archives pushed for name@version. If the upstream has the version, clients get the upstream archive again.",
"tags": [
"hosted"
],
"summary": "Delete a pushed version",
"parameters": [
{
"type": "string",
"description": "Ecosystem",
"name": "ecosystem",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Package name",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Version",
"name": "version",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
}
}
}
},
"/api/outdated": {
"post": {
"consumes": [
"application/json"
],
2026-03-11 17:18:29 +00:00
"produces": [
"application/json"
2026-03-11 17:18:29 +00:00
],
"tags": [
"api"
2026-03-11 17:18:29 +00:00
],
"summary": "Check outdated packages",
2026-03-11 17:18:29 +00:00
"parameters": [
{
"description": "Packages to check",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/server.OutdatedRequest"
}
2026-03-11 17:18:29 +00:00
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.OutdatedResponse"
2026-03-11 17:18:29 +00:00
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/api/packages": {
"get": {
2026-03-11 17:18:29 +00:00
"produces": [
"application/json"
],
"tags": [
"api"
],
"summary": "List cached packages",
2026-03-11 17:18:29 +00:00
"parameters": [
{
"type": "string",
"description": "Ecosystem",
"name": "ecosystem",
"in": "query"
},
{
"enum": [
"hits",
"name",
"size",
"cached_at",
"ecosystem",
"vulns"
],
"type": "string",
"description": "Sort",
"name": "sort",
"in": "query"
2026-03-11 17:18:29 +00:00
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.PackagesListResponse"
2026-03-11 17:18:29 +00:00
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/api/search": {
2026-03-11 17:18:29 +00:00
"get": {
"produces": [
"application/json"
],
"tags": [
"api"
2026-03-11 17:18:29 +00:00
],
"summary": "Search cached packages",
2026-03-11 17:18:29 +00:00
"parameters": [
{
"type": "string",
"description": "Query",
"name": "q",
"in": "query",
2026-03-11 17:18:29 +00:00
"required": true
},
{
"type": "string",
"description": "Ecosystem",
"name": "ecosystem",
"in": "query"
2026-03-11 17:18:29 +00:00
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.SearchResponse"
2026-03-11 17:18:29 +00:00
}
},
"400": {
"description": "Bad Request",
2026-03-11 17:18:29 +00:00
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/health": {
"get": {
2026-03-11 17:18:29 +00:00
"produces": [
"application/json"
],
"tags": [
"meta"
2026-03-11 17:18:29 +00:00
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
2026-03-11 17:18:29 +00:00
"schema": {
"$ref": "#/definitions/server.HealthResponse"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/server.HealthResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/stats": {
"get": {
"produces": [
"application/json"
],
"tags": [
"meta"
2026-03-11 17:18:29 +00:00
],
"summary": "Cache statistics",
2026-03-11 17:18:29 +00:00
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.StatsResponse"
2026-03-11 17:18:29 +00:00
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/ui/api/browse/{ecosystem}/{name}/{version}": {
2026-03-11 17:18:29 +00:00
"get": {
"description": "Lists files from the first cached artifact for a package version.",
2026-03-11 17:18:29 +00:00
"produces": [
"application/json"
],
"tags": [
"browse"
2026-03-11 17:18:29 +00:00
],
"summary": "List files inside a cached artifact",
2026-03-11 17:18:29 +00:00
"parameters": [
{
"type": "string",
"description": "Ecosystem",
"name": "ecosystem",
"in": "path",
"required": true
2026-03-11 17:18:29 +00:00
},
{
"type": "string",
"description": "Package name",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Version",
"name": "version",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Directory path inside the archive",
"name": "path",
2026-03-11 17:18:29 +00:00
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/server.BrowseListResponse"
2026-03-11 17:18:29 +00:00
}
},
"404": {
"description": "Not Found",
2026-03-11 17:18:29 +00:00
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/ui/api/browse/{ecosystem}/{name}/{version}/file/{filepath}": {
2026-03-11 17:18:29 +00:00
"get": {
"description": "Streams a single file from the cached artifact. The file path may contain slashes.",
2026-03-11 17:18:29 +00:00
"produces": [
"application/octet-stream"
2026-03-11 17:18:29 +00:00
],
"tags": [
"browse"
2026-03-11 17:18:29 +00:00
],
"summary": "Fetch a file inside a cached artifact",
2026-03-11 17:18:29 +00:00
"parameters": [
{
"type": "string",
"description": "Ecosystem",
"name": "ecosystem",
"in": "path",
2026-03-11 17:18:29 +00:00
"required": true
},
{
"type": "string",
"description": "Package name",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Version",
"name": "version",
"in": "path",
"required": true
},
{
"type": "string",
"description": "File path inside the archive",
"name": "filepath",
"in": "path",
"required": true
2026-03-11 17:18:29 +00:00
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
2026-03-11 17:18:29 +00:00
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
},
2026-03-11 17:18:29 +00:00
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"/ui/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}": {
2026-03-11 17:18:29 +00:00
"get": {
"description": "Returns a structured diff for two cached versions.",
2026-03-11 17:18:29 +00:00
"produces": [
Add storage backend probe to /health (closes #73) (#119) * config: add Health.StorageProbeInterval * metrics: add proxy_health_probe_failures_total counter * server: add storageProbe with happy-path test * server: add storageProbe failure-mode tests * server: add healthCache with TTL, single-flight, transition logging * server: wire storage probe into /health * server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer Also fix Windows file-locking issue in storageProbe: close the reader explicitly before Delete so the file handle is released prior to os.Remove. * server: clean up stale comment in storageProbe * docs: document storage health probe and new metric * docs: regenerate Swagger for /health JSON response * server: simplify rc.Close error handling in storageProbe * server: defer probe cleanup so size/open/read/verify failures don't leak objects Previously, storageProbe only called Delete on the success path. Any failure between Store and the final Delete (size mismatch, Open error, mid-stream read failure, content mismatch) left the probe object orphaned in the storage backend. With caching disabled and Kubernetes-rate probing, the leak could accumulate noticeably on backends like S3. Use a named return + defer to attempt Delete after every successful Store. The earlier-step failure remains the primary error; Delete failure only surfaces as step="delete" when nothing else went wrong. Add a table-driven test that asserts cleanup runs for each non-delete failure path. Reported by Copilot on #119. * config: validate health.storage_probe_interval in Config.Validate The new duration field was only validated at use time in newHealthCache. The existing codebase already validates other duration fields (MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in Config.Validate() so misconfiguration fails fast at startup with a config-key-specific error. Match that pattern. The parse-at-use code in newHealthCache stays as a safety net, mirroring the MetadataTTL precedent. Reported by Copilot on #119. * docs: lowercase "counter" in metrics table for consistency Other rows in the table use lowercase type names (counter/gauge/histogram). Match that style. Reported by Copilot on #119. * docs: include size-check step in /health probe description The probe is write → size-check → read → verify → delete; the architecture note was missing the size-check step. Reported by Copilot on #119. * server: address andrew's review on #119 - Drop unused callerCtx parameter from healthCache.Check (Check is now parameter-less; the comment-only "accepted for symmetry" justification wasn't carrying its weight). - Emit "storage": {"status": "skipped"} on DB short-circuit instead of omitting the key, so monitors expecting a fixed key set keep working. - Reject negative storage_probe_interval at config validation time (previously parsed and silently behaved like "0"). - Extract HealthConfig.Validate to keep Config.Validate under the gocognit threshold and match the existing GradleBuildCacheConfig pattern. - README Health Check section: note that /health is intended as a readiness probe rather than a liveness probe (Check holds a mutex for up to the 10s probe timeout). - cmd/proxy/main.go godoc: column-align the new env var with the surrounding Gradle entries. Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
"application/json"
2026-03-11 17:18:29 +00:00
],
"tags": [
"browse"
2026-03-11 17:18:29 +00:00
],
"summary": "Compare two cached versions",
"parameters": [
{
"type": "string",
"description": "Ecosystem",
"name": "ecosystem",
"in": "path",
"required": true
2026-03-11 17:18:29 +00:00
},
{
"type": "string",
"description": "Package name",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "From version",
"name": "fromVersion",
"in": "path",
"required": true
},
{
"type": "string",
"description": "To version",
"name": "toVersion",
"in": "path",
"required": true
2026-03-11 17:18:29 +00:00
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/server.ErrorResponse"
}
2026-03-11 17:18:29 +00:00
}
}
}
}
},
"definitions": {
"handler.HostedArtifact": {
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"pushed_at": {
"type": "string"
},
"sha256": {
"type": "string"
},
"size": {
"type": "integer"
},
"version": {
"type": "string"
}
}
},
2026-03-11 17:18:29 +00:00
"server.BrowseFileInfo": {
"type": "object",
"properties": {
"is_dir": {
"type": "boolean"
},
"mod_time": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"server.BrowseListResponse": {
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {
"$ref": "#/definitions/server.BrowseFileInfo"
}
},
"path": {
"type": "string"
}
}
},
"server.BulkRequest": {
"type": "object",
"properties": {
"purls": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"server.BulkResponse": {
"type": "object",
"properties": {
"packages": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/server.PackageResponse"
}
}
}
},
"server.EcosystemStatsEntry": {
"type": "object",
"properties": {
"cache_size_bytes": {
"type": "integer"
},
"cached_artifacts": {
"type": "integer"
},
"downloaded": {
"type": "string"
},
"downloaded_bytes": {
"type": "integer"
},
"downloads": {
"type": "integer"
},
"ecosystem": {
"type": "string"
},
"packages": {
"type": "integer"
},
"versions": {
"type": "integer"
}
}
},
"server.ErrorResponse": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
Add storage backend probe to /health (closes #73) (#119) * config: add Health.StorageProbeInterval * metrics: add proxy_health_probe_failures_total counter * server: add storageProbe with happy-path test * server: add storageProbe failure-mode tests * server: add healthCache with TTL, single-flight, transition logging * server: wire storage probe into /health * server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer Also fix Windows file-locking issue in storageProbe: close the reader explicitly before Delete so the file handle is released prior to os.Remove. * server: clean up stale comment in storageProbe * docs: document storage health probe and new metric * docs: regenerate Swagger for /health JSON response * server: simplify rc.Close error handling in storageProbe * server: defer probe cleanup so size/open/read/verify failures don't leak objects Previously, storageProbe only called Delete on the success path. Any failure between Store and the final Delete (size mismatch, Open error, mid-stream read failure, content mismatch) left the probe object orphaned in the storage backend. With caching disabled and Kubernetes-rate probing, the leak could accumulate noticeably on backends like S3. Use a named return + defer to attempt Delete after every successful Store. The earlier-step failure remains the primary error; Delete failure only surfaces as step="delete" when nothing else went wrong. Add a table-driven test that asserts cleanup runs for each non-delete failure path. Reported by Copilot on #119. * config: validate health.storage_probe_interval in Config.Validate The new duration field was only validated at use time in newHealthCache. The existing codebase already validates other duration fields (MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in Config.Validate() so misconfiguration fails fast at startup with a config-key-specific error. Match that pattern. The parse-at-use code in newHealthCache stays as a safety net, mirroring the MetadataTTL precedent. Reported by Copilot on #119. * docs: lowercase "counter" in metrics table for consistency Other rows in the table use lowercase type names (counter/gauge/histogram). Match that style. Reported by Copilot on #119. * docs: include size-check step in /health probe description The probe is write → size-check → read → verify → delete; the architecture note was missing the size-check step. Reported by Copilot on #119. * server: address andrew's review on #119 - Drop unused callerCtx parameter from healthCache.Check (Check is now parameter-less; the comment-only "accepted for symmetry" justification wasn't carrying its weight). - Emit "storage": {"status": "skipped"} on DB short-circuit instead of omitting the key, so monitors expecting a fixed key set keep working. - Reject negative storage_probe_interval at config validation time (previously parsed and silently behaved like "0"). - Extract HealthConfig.Validate to keep Config.Validate under the gocognit threshold and match the existing GradleBuildCacheConfig pattern. - README Health Check section: note that /health is intended as a readiness probe rather than a liveness probe (Check holds a mutex for up to the 10s probe timeout). - cmd/proxy/main.go godoc: column-align the new env var with the surrounding Gradle entries. Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
"server.HealthCheck": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"status": {
"type": "string"
},
"step": {
"type": "string"
}
}
},
"server.HealthResponse": {
"type": "object",
"properties": {
"checks": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/server.HealthCheck"
}
},
"circuit_breakers": {
"description": "CircuitBreakers reports the state (\"open\" or \"closed\") of each upstream\nregistry's artifact-fetch circuit breaker, keyed by the host fetched from\nor, where the fetch URL has none to read, by an opaque placeholder\nstanding in for it. It is omitted when no breaker has been created yet.\nAn open breaker fails every artifact fetch it covers without contacting\nthe upstream, but says nothing about this proxy's own health, so it does\nnot change Status.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
Add storage backend probe to /health (closes #73) (#119) * config: add Health.StorageProbeInterval * metrics: add proxy_health_probe_failures_total counter * server: add storageProbe with happy-path test * server: add storageProbe failure-mode tests * server: add healthCache with TTL, single-flight, transition logging * server: wire storage probe into /health * server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer Also fix Windows file-locking issue in storageProbe: close the reader explicitly before Delete so the file handle is released prior to os.Remove. * server: clean up stale comment in storageProbe * docs: document storage health probe and new metric * docs: regenerate Swagger for /health JSON response * server: simplify rc.Close error handling in storageProbe * server: defer probe cleanup so size/open/read/verify failures don't leak objects Previously, storageProbe only called Delete on the success path. Any failure between Store and the final Delete (size mismatch, Open error, mid-stream read failure, content mismatch) left the probe object orphaned in the storage backend. With caching disabled and Kubernetes-rate probing, the leak could accumulate noticeably on backends like S3. Use a named return + defer to attempt Delete after every successful Store. The earlier-step failure remains the primary error; Delete failure only surfaces as step="delete" when nothing else went wrong. Add a table-driven test that asserts cleanup runs for each non-delete failure path. Reported by Copilot on #119. * config: validate health.storage_probe_interval in Config.Validate The new duration field was only validated at use time in newHealthCache. The existing codebase already validates other duration fields (MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in Config.Validate() so misconfiguration fails fast at startup with a config-key-specific error. Match that pattern. The parse-at-use code in newHealthCache stays as a safety net, mirroring the MetadataTTL precedent. Reported by Copilot on #119. * docs: lowercase "counter" in metrics table for consistency Other rows in the table use lowercase type names (counter/gauge/histogram). Match that style. Reported by Copilot on #119. * docs: include size-check step in /health probe description The probe is write → size-check → read → verify → delete; the architecture note was missing the size-check step. Reported by Copilot on #119. * server: address andrew's review on #119 - Drop unused callerCtx parameter from healthCache.Check (Check is now parameter-less; the comment-only "accepted for symmetry" justification wasn't carrying its weight). - Emit "storage": {"status": "skipped"} on DB short-circuit instead of omitting the key, so monitors expecting a fixed key set keep working. - Reject negative storage_probe_interval at config validation time (previously parsed and silently behaved like "0"). - Extract HealthConfig.Validate to keep Config.Validate under the gocognit threshold and match the existing GradleBuildCacheConfig pattern. - README Health Check section: note that /health is intended as a readiness probe rather than a liveness probe (Check holds a mutex for up to the 10s probe timeout). - cmd/proxy/main.go godoc: column-align the new env var with the surrounding Gradle entries. Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
"status": {
"type": "string"
},
"upstream_holds": {
"description": "UpstreamHolds lists upstream hosts that rate limited the proxy and\nthe RFC 3339 time until which it sends them nothing. A held host\nfails cache misses without contacting it and does not change Status.",
"type": "object",
"additionalProperties": {
"type": "string"
}
Add storage backend probe to /health (closes #73) (#119) * config: add Health.StorageProbeInterval * metrics: add proxy_health_probe_failures_total counter * server: add storageProbe with happy-path test * server: add storageProbe failure-mode tests * server: add healthCache with TTL, single-flight, transition logging * server: wire storage probe into /health * server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer Also fix Windows file-locking issue in storageProbe: close the reader explicitly before Delete so the file handle is released prior to os.Remove. * server: clean up stale comment in storageProbe * docs: document storage health probe and new metric * docs: regenerate Swagger for /health JSON response * server: simplify rc.Close error handling in storageProbe * server: defer probe cleanup so size/open/read/verify failures don't leak objects Previously, storageProbe only called Delete on the success path. Any failure between Store and the final Delete (size mismatch, Open error, mid-stream read failure, content mismatch) left the probe object orphaned in the storage backend. With caching disabled and Kubernetes-rate probing, the leak could accumulate noticeably on backends like S3. Use a named return + defer to attempt Delete after every successful Store. The earlier-step failure remains the primary error; Delete failure only surfaces as step="delete" when nothing else went wrong. Add a table-driven test that asserts cleanup runs for each non-delete failure path. Reported by Copilot on #119. * config: validate health.storage_probe_interval in Config.Validate The new duration field was only validated at use time in newHealthCache. The existing codebase already validates other duration fields (MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in Config.Validate() so misconfiguration fails fast at startup with a config-key-specific error. Match that pattern. The parse-at-use code in newHealthCache stays as a safety net, mirroring the MetadataTTL precedent. Reported by Copilot on #119. * docs: lowercase "counter" in metrics table for consistency Other rows in the table use lowercase type names (counter/gauge/histogram). Match that style. Reported by Copilot on #119. * docs: include size-check step in /health probe description The probe is write → size-check → read → verify → delete; the architecture note was missing the size-check step. Reported by Copilot on #119. * server: address andrew's review on #119 - Drop unused callerCtx parameter from healthCache.Check (Check is now parameter-less; the comment-only "accepted for symmetry" justification wasn't carrying its weight). - Emit "storage": {"status": "skipped"} on DB short-circuit instead of omitting the key, so monitors expecting a fixed key set keep working. - Reject negative storage_probe_interval at config validation time (previously parsed and silently behaved like "0"). - Extract HealthConfig.Validate to keep Config.Validate under the gocognit threshold and match the existing GradleBuildCacheConfig pattern. - README Health Check section: note that /health is intended as a readiness probe rather than a liveness probe (Check holds a mutex for up to the 10s probe timeout). - cmd/proxy/main.go godoc: column-align the new env var with the surrounding Gradle entries. Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
}
}
},
"server.HostedListResponse": {
"type": "object",
"properties": {
"artifacts": {
"type": "array",
"items": {
"$ref": "#/definitions/handler.HostedArtifact"
}
},
"ecosystem": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"server.HostedPushResponse": {
"type": "object",
"properties": {
"ecosystem": {
"type": "string"
},
"filename": {
"type": "string"
},
"name": {
"type": "string"
},
"pushed_at": {
"type": "string"
},
"sha256": {
"type": "string"
},
"size": {
"type": "integer"
},
"version": {
"type": "string"
}
}
},
2026-03-11 17:18:29 +00:00
"server.OutdatedPackage": {
"type": "object",
"properties": {
"ecosystem": {
"type": "string"
},
"name": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"server.OutdatedRequest": {
"type": "object",
"properties": {
"packages": {
"type": "array",
"items": {
"$ref": "#/definitions/server.OutdatedPackage"
}
}
}
},
"server.OutdatedResponse": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/server.OutdatedResult"
}
}
}
},
"server.OutdatedResult": {
"type": "object",
"properties": {
"ecosystem": {
"type": "string"
},
"is_outdated": {
"type": "boolean"
},
"latest_version": {
"type": "string"
},
"name": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"server.PackageListResult": {
"type": "object",
"properties": {
"cached_at": {
"type": "string"
},
"ecosystem": {
"type": "string"
},
"hits": {
"type": "integer"
},
"latest_version": {
"type": "string"
},
"license": {
"type": "string"
},
"license_category": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer"
},
"vuln_count": {
"type": "integer"
}
}
},
"server.PackageResponse": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"ecosystem": {
"type": "string"
},
"homepage": {
"type": "string"
},
"latest_version": {
"type": "string"
},
"license": {
"type": "string"
},
"license_category": {
"type": "string"
},
"name": {
"type": "string"
},
"registry_url": {
"type": "string"
},
"repository": {
"type": "string"
}
}
},
"server.PackagesListResponse": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"ecosystem": {
"type": "string"
},
"page": {
"type": "integer"
},
"per_page": {
"type": "integer"
},
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/server.PackageListResult"
}
},
"sort_by": {
"type": "string"
},
"total": {
"type": "integer"
}
}
},
"server.SearchPackageResult": {
"type": "object",
"properties": {
"cached_at": {
"type": "string"
},
"ecosystem": {
"type": "string"
},
"hits": {
"type": "integer"
},
"latest_version": {
"type": "string"
},
"license": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"server.SearchResponse": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"query": {
"type": "string"
},
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/server.SearchPackageResult"
}
}
}
},
"server.StatsResponse": {
"type": "object",
"properties": {
"cached_artifacts": {
"type": "integer"
},
"database_path": {
"type": "string"
},
"downloaded": {
"type": "string"
},
"downloaded_bytes": {
"description": "DownloadedBytes is the accumulated download volume across every\necosystem: cache hits multiplied by the artifact size they served.",
"type": "integer"
},
"downloads": {
"type": "integer"
},
"ecosystems": {
"type": "array",
"items": {
"$ref": "#/definitions/server.EcosystemStatsEntry"
}
},
"stats_unavailable": {
"description": "StatsUnavailable distinguishes a proxy that has served nothing from one\nwhose aggregation failed with no snapshot to fall back on. Without it\nboth report zeros and an empty array.",
"type": "boolean"
},
"storage_url": {
2026-03-11 17:18:29 +00:00
"type": "string"
},
"total_size": {
"type": "string"
},
"total_size_bytes": {
"type": "integer"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "\"Bearer \" followed by hosted.token",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
2026-03-11 17:18:29 +00:00
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "0.1.0",
Host: "",
BasePath: "/",
Schemes: []string{},
Title: "git-pkgs proxy API",
Description: "HTTP API for package enrichment, vulnerability lookup, cache stats, and source browsing.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}