From c077d11fd863b56cf1d128a93dde568cce025d43 Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:13:17 +1000 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20swagger=20bearer=20auth=20=E2=80=94?= =?UTF-8?q?=20drop=20duplicate=20Authorization=20params?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swagger was also generating an Authorization parameter defaulting to "Bearer " which overwrote a good Authorize value. Rely on BearerAuth only; document exact UI steps (Bearer dev, not just dev). --- docs/docs.go | 30 ++---------------------------- docs/swagger.json | 30 ++---------------------------- docs/swagger.yaml | 29 ++++++----------------------- handlers/handlers.go | 3 +-- handlers/proxy.go | 1 - main.go | 2 +- 6 files changed, 12 insertions(+), 83 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index b0efce2..664b0f8 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -107,22 +107,12 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Env dump so you can check secrets/configmaps/task params actually landed. Behind auth under /a/", + "description": "Env dump so you can check secrets/configmaps/task params actually landed. Behind auth under /a/. Use the Authorize button (value: Bearer \u0026lt;token\u0026gt;) — do not leave a separate Authorization param empty.", "produces": [ "application/json" ], "summary": "Get environment variables", "operationId": "env", - "parameters": [ - { - "type": "string", - "default": "Bearer", - "description": "Bearer token from app logs", - "name": "Authorization", - "in": "header", - "required": true - } - ], "responses": { "200": { "description": "OK", @@ -182,14 +172,6 @@ const docTemplate = `{ "description": "HTTP method for GET form (default GET)", "name": "method", "in": "query" - }, - { - "type": "string", - "default": "Bearer", - "description": "Bearer token", - "name": "Authorization", - "in": "header", - "required": true } ], "responses": { @@ -263,14 +245,6 @@ const docTemplate = `{ "description": "HTTP method for GET form (default GET)", "name": "method", "in": "query" - }, - { - "type": "string", - "default": "Bearer", - "description": "Bearer token", - "name": "Authorization", - "in": "header", - "required": true } ], "responses": { @@ -793,7 +767,7 @@ const docTemplate = `{ }, "securityDefinitions": { "BearerAuth": { - "description": "Paste: Bearer \u003ctoken\u003e (token from container logs, or AUTH_TOKEN env). Example: Bearer dev", + "description": "Click Authorize (lock icon). Value MUST be: Bearer \u003ctoken\u003e including the word Bearer and a space. Example for local podman: Bearer dev. Token only (without Bearer) will 401.", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.json b/docs/swagger.json index 047f92f..b2bce05 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -100,22 +100,12 @@ "BearerAuth": [] } ], - "description": "Env dump so you can check secrets/configmaps/task params actually landed. Behind auth under /a/", + "description": "Env dump so you can check secrets/configmaps/task params actually landed. Behind auth under /a/. Use the Authorize button (value: Bearer \u0026lt;token\u0026gt;) — do not leave a separate Authorization param empty.", "produces": [ "application/json" ], "summary": "Get environment variables", "operationId": "env", - "parameters": [ - { - "type": "string", - "default": "Bearer", - "description": "Bearer token from app logs", - "name": "Authorization", - "in": "header", - "required": true - } - ], "responses": { "200": { "description": "OK", @@ -175,14 +165,6 @@ "description": "HTTP method for GET form (default GET)", "name": "method", "in": "query" - }, - { - "type": "string", - "default": "Bearer", - "description": "Bearer token", - "name": "Authorization", - "in": "header", - "required": true } ], "responses": { @@ -256,14 +238,6 @@ "description": "HTTP method for GET form (default GET)", "name": "method", "in": "query" - }, - { - "type": "string", - "default": "Bearer", - "description": "Bearer token", - "name": "Authorization", - "in": "header", - "required": true } ], "responses": { @@ -786,7 +760,7 @@ }, "securityDefinitions": { "BearerAuth": { - "description": "Paste: Bearer \u003ctoken\u003e (token from container logs, or AUTH_TOKEN env). Example: Bearer dev", + "description": "Click Authorize (lock icon). Value MUST be: Bearer \u003ctoken\u003e including the word Bearer and a space. Example for local podman: Bearer dev. Token only (without Bearer) will 401.", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 58e80d0..9b6ad66 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -158,16 +158,10 @@ paths: summary: Update probe control state /a/env: get: - description: Env dump so you can check secrets/configmaps/task params actually - landed. Behind auth under /a/ + description: 'Env dump so you can check secrets/configmaps/task params actually + landed. Behind auth under /a/. Use the Authorize button (value: Bearer <token>) + — do not leave a separate Authorization param empty.' operationId: env - parameters: - - default: Bearer - description: Bearer token from app logs - in: header - name: Authorization - required: true - type: string produces: - application/json responses: @@ -209,12 +203,6 @@ paths: in: query name: method type: string - - default: Bearer - description: Bearer token - in: header - name: Authorization - required: true - type: string produces: - application/json responses: @@ -265,12 +253,6 @@ paths: in: query name: method type: string - - default: Bearer - description: Bearer token - in: header - name: Authorization - required: true - type: string produces: - application/json responses: @@ -552,8 +534,9 @@ paths: summary: Version / build info securityDefinitions: BearerAuth: - description: 'Paste: Bearer (token from container logs, or AUTH_TOKEN - env). Example: Bearer dev' + description: 'Click Authorize (lock icon). Value MUST be: Bearer including + the word Bearer and a space. Example for local podman: Bearer dev. Token only + (without Bearer) will 401.' in: header name: Authorization type: apiKey diff --git a/handlers/handlers.go b/handlers/handlers.go index 1f7153f..c8ed7d6 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -96,14 +96,13 @@ func HeadersHandler(c *gin.Context) { } // @Summary Get environment variables -// @Description Env dump so you can check secrets/configmaps/task params actually landed. Behind auth under /a/ +// @Description Env dump so you can check secrets/configmaps/task params actually landed. Behind auth under /a/. Use the Authorize button (value: Bearer <token>) — do not leave a separate Authorization param empty. // @ID env // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /a/env [get] -// @Param Authorization header string true "Bearer token from app logs" default(Bearer ) func EnvHandler(c *gin.Context) { c.JSON(http.StatusOK, GetEnvironmentVariables()) } diff --git a/handlers/proxy.go b/handlers/proxy.go index c144749..28be97d 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -74,7 +74,6 @@ type ProxyRequest struct { // @Failure 502 {object} map[string]interface{} // @Router /a/proxy [post] // @Router /a/proxy [get] -// @Param Authorization header string true "Bearer token" default(Bearer ) func ProxyHandler(c *gin.Context) { var req ProxyRequest diff --git a/main.go b/main.go index d542613..7d5faef 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization -// @description Paste: Bearer (token from container logs, or AUTH_TOKEN env). Example: Bearer dev +// @description Click Authorize (lock icon). Value MUST be: Bearer including the word Bearer and a space. Example for local podman: Bearer dev. Token only (without Bearer) will 401. package main From 1ce5acdecd7904147e400ae8eb833ed0b961544a Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:13:36 +1000 Subject: [PATCH 2/5] docs: clearer swagger Bearer auth steps for local testing --- README.md | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 081dcb8..4721d43 100644 --- a/README.md +++ b/README.md @@ -93,29 +93,33 @@ curl -sS -H "Authorization: Bearer $TOKEN" localhost:8080/a/control/probes | jq ### Swagger UI -1. Open the UI **on the same host/port you want to call** (that way Try it out just works): - - local: http://localhost:8080/ or `/api-docs/index.html` - - port-forward: http://localhost:8080/api-docs/index.html -2. Click **Authorize** (lock icon) -3. Value: `Bearer ` — word **Bearer**, a space, then the token - Example: `Bearer dev` - Header name is `Authorization`. The UI remembers it in this browser (`PersistAuthorization`). -4. **Try it out** on any route — protected ones under `/a/` need Authorize first. +Yes — for `/a/*` you **Authorize before Execute** (same value as curl). -**Host / port for Try it out** +**Local podman (`AUTH_TOKEN=dev` on :18080):** -Swagger uses the host you opened the page on (so docker `:8080`, port-forward, or in-cluster ingress all line up without editing the spec). +1. Open http://127.0.0.1:18080/ (same host/port as the API) +2. Click the green **Authorize** lock (top right) — not a random field on the op +3. Paste exactly: -If you ever need to point Try it out somewhere else (UI on A, API on B): + ```text + Bearer dev + ``` -```text -http://localhost:8080/api-docs/index.html?host=cluster-utils-api-svc:8080&scheme=http -http://localhost:8080/api-docs/index.html?host=my-alb.example.com&scheme=https -``` + | Value | Result | + |-------|--------| + | `Bearer dev` | **200** | + | `dev` only | **401** | + | `bearer dev` (lowercase) | **401** (we require exact `Bearer `) | + +4. **Authorize** → **Close** +5. Operation → **Try it out** → **Execute** -`host` = `hostname` or `hostname:port` (no `http://`). `scheme` = `http` or `https`. +If still 401: hard-refresh swagger (stale token), confirm +`curl -sS -o /dev/null -w '%{http_code}\n' -H 'Authorization: Bearer dev' http://127.0.0.1:18080/a/env` +is `200`, or check `podman logs cu-api | grep '"token"'`. -Without a token, `/a/*` returns **401**. +**Host / port:** UI uses the host you opened. Override with +`?host=127.0.0.1:18080&scheme=http` if needed. --- From 72ab038eabfc100137dacd8fd42bee18abd58069 Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:21:22 +1000 Subject: [PATCH 3/5] fix: swagger examples for proxy, probes, status, delay, echo Default proxy GET url to https://httpbin.org/get (echoes headers/method). Status 418, delay 2s, probe mode examples, clearer Authorize-only auth text. --- docs/docs.go | 194 +++++++++++++++++++++++++++++++++---------- docs/swagger.json | 194 +++++++++++++++++++++++++++++++++---------- docs/swagger.yaml | 194 +++++++++++++++++++++++++++++++++---------- handlers/handlers.go | 11 +-- handlers/probes.go | 29 ++++--- handlers/proxy.go | 35 +++++--- main.go | 2 +- 7 files changed, 496 insertions(+), 163 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 664b0f8..77e64b1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -52,7 +52,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Partial update of live/ready/startup. resetStartupLatch re-runs cold start without restarting the process.", + "description": "Partial update of live/ready/startup. Example: fail readiness so the pod drops from Service endpoints (no restart). resetStartupLatch re-runs cold start without restarting the process. Auth: Authorize lock with \"Bearer dev\".", "consumes": [ "application/json" ], @@ -63,7 +63,7 @@ const docTemplate = `{ "operationId": "putProbes", "parameters": [ { - "description": "probe update", + "description": "Partial update — schema examples show fail-readiness shape", "name": "body", "in": "body", "required": true, @@ -142,7 +142,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Auth required. North→south hits this pod; this pod calls url east-west. Default JSON wrap includes upstream status, headers, and body. Open proxy would be SSRF — keep behind bearer.", + "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", "consumes": [ "application/json" ], @@ -153,25 +153,36 @@ const docTemplate = `{ "operationId": "proxy", "parameters": [ { - "description": "proxy request", + "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", "name": "body", "in": "body", - "required": true, "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } }, { "type": "string", - "description": "absolute url (GET form)", + "default": "https://httpbin.org/get", + "example": "https://httpbin.org/get", + "description": "GET form: absolute URL to fetch", "name": "url", "in": "query" }, { "type": "string", - "description": "HTTP method for GET form (default GET)", + "default": "GET", + "example": "GET", + "description": "GET form: HTTP method", "name": "method", "in": "query" + }, + { + "type": "number", + "default": 15, + "example": 15, + "description": "GET form: outbound timeout", + "name": "timeoutSeconds", + "in": "query" } ], "responses": { @@ -215,7 +226,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Auth required. North→south hits this pod; this pod calls url east-west. Default JSON wrap includes upstream status, headers, and body. Open proxy would be SSRF — keep behind bearer.", + "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", "consumes": [ "application/json" ], @@ -226,25 +237,36 @@ const docTemplate = `{ "operationId": "proxy", "parameters": [ { - "description": "proxy request", + "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", "name": "body", "in": "body", - "required": true, "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } }, { "type": "string", - "description": "absolute url (GET form)", + "default": "https://httpbin.org/get", + "example": "https://httpbin.org/get", + "description": "GET form: absolute URL to fetch", "name": "url", "in": "query" }, { "type": "string", - "description": "HTTP method for GET form (default GET)", + "default": "GET", + "example": "GET", + "description": "GET form: HTTP method", "name": "method", "in": "query" + }, + { + "type": "number", + "default": 15, + "example": 15, + "description": "GET form: outbound timeout", + "name": "timeoutSeconds", + "in": "query" } ], "responses": { @@ -304,7 +326,7 @@ const docTemplate = `{ }, "/delay/{seconds}": { "get": { - "description": "Sleep N seconds then return 200. Cap is MAX_DELAY_SECONDS env (default 120, hard max 600). For probe timeouts prefer LIVE/READY/STARTUP delaySeconds instead", + "description": "Sleep N seconds then return 200. Cap is MAX_DELAY_SECONDS env (default 120, hard max 600). Try 2 for a slow upstream demo; use probe delaySeconds to trip kube timeoutSeconds.", "produces": [ "text/plain" ], @@ -313,6 +335,8 @@ const docTemplate = `{ "parameters": [ { "type": "number", + "default": 2, + "example": 2, "description": "seconds to sleep", "name": "seconds", "in": "path", @@ -331,7 +355,7 @@ const docTemplate = `{ }, "/echo": { "get": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -340,6 +364,18 @@ const docTemplate = `{ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -351,7 +387,7 @@ const docTemplate = `{ } }, "put": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -360,6 +396,18 @@ const docTemplate = `{ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -371,7 +419,7 @@ const docTemplate = `{ } }, "post": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -380,6 +428,18 @@ const docTemplate = `{ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -391,7 +451,7 @@ const docTemplate = `{ } }, "delete": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -400,6 +460,18 @@ const docTemplate = `{ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -411,7 +483,7 @@ const docTemplate = `{ } }, "patch": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -420,6 +492,18 @@ const docTemplate = `{ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -599,7 +683,7 @@ const docTemplate = `{ }, "/status/{code}": { "get": { - "description": "Respond with whatever http status you pass (100-599). Great for ingress/retry testing", + "description": "Respond with whatever http status you pass (100-599). Try 418 (teapot), 503, or 502 for ingress/retry tests.", "produces": [ "text/plain" ], @@ -608,6 +692,8 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "default": 418, + "example": 418, "description": "HTTP status code", "name": "code", "in": "path", @@ -651,23 +737,29 @@ const docTemplate = `{ "type": "object", "properties": { "bootDelaySeconds": { - "description": "Startup only: wall-clock seconds from process start before a success is allowed.", - "type": "number" + "description": "Startup only: wall-clock seconds from process start before a success is allowed.\nexample: 10", + "type": "number", + "example": 10 }, "delaySeconds": { - "description": "sleep before answering (0–30)", - "type": "number" + "description": "sleep before answering (seconds)\nexample: 0", + "type": "number", + "example": 0 }, "flapEvery": { - "type": "integer" + "description": "Flap: every Nth request fails (0 = use time-based flapSeconds).\nexample: 2", + "type": "integer", + "example": 2 }, "flapSeconds": { - "description": "Flap (mode=flap):\n flapSeconds — wall-clock half-period: ok for N sec, fail for N sec, repeat (default 5).\n flapEvery — if \u003e 0, every Nth request fails instead of using the clock (handy for tests).", - "type": "number" + "description": "Flap (mode=flap): half-period seconds (default 5).\nexample: 5", + "type": "number", + "example": 5 }, "mode": { - "description": "ok | fail | delay | flap", - "type": "string" + "description": "ok | fail | delay | flap\nexample: fail", + "type": "string", + "example": "fail" } } }, @@ -714,7 +806,9 @@ const docTemplate = `{ "$ref": "#/definitions/handlers.ProbeConfig" }, "resetStartupLatch": { - "type": "boolean" + "description": "example: true", + "type": "boolean", + "example": true }, "startup": { "$ref": "#/definitions/handlers.ProbeConfig" @@ -728,46 +822,56 @@ const docTemplate = `{ ], "properties": { "body": { - "description": "Optional body (string; use for JSON text, form, etc.)", - "type": "string" + "description": "Optional body (string; use for JSON text, form, etc.)\nexample: {\"hello\":\"cluster\"}", + "type": "string", + "example": "{\"hello\":\"cluster\"}" }, "forwardIncomingHeaders": { - "description": "When true (default), copy inbound request headers onto the outbound call\n(minus hop-by-hop). Tracing headers like X-Request-Id ride along.", - "type": "boolean" + "description": "When true (default), copy inbound request headers onto the outbound call\n(minus hop-by-hop). Tracing headers like X-Request-Id ride along.\nexample: true", + "type": "boolean", + "example": true }, "forwardSensitiveHeaders": { - "description": "When true, also forward Authorization / Cookie from the inbound request.\nDefault false so your bearer token for *this* api is not sent east-west by accident.", - "type": "boolean" + "description": "When true, also forward Authorization / Cookie from the inbound request.\nDefault false so your bearer token for *this* api is not sent east-west by accident.\nexample: false", + "type": "boolean", + "example": false }, "headers": { - "description": "Extra headers to set/override on the outbound request", + "description": "Extra headers to set/override on the outbound request\nexample: {\"X-Demo\":\"from-swagger\"}", "type": "object", "additionalProperties": { "type": "string" + }, + "example": { + "X-Demo": "from-swagger" } }, "method": { - "description": "HTTP method (default GET)", - "type": "string" + "description": "HTTP method (default GET)\nexample: GET", + "type": "string", + "example": "GET" }, "raw": { - "description": "When true, return the upstream body as the raw HTTP response (status + headers from upstream).\nDefault false → JSON wrap with request/response/meta (includes upstream headers + body).", - "type": "boolean" + "description": "When true, return the upstream body as the raw HTTP response (status + headers from upstream).\nDefault false → JSON wrap with request/response/meta (includes upstream headers + body).\nexample: false", + "type": "boolean", + "example": false }, "timeoutSeconds": { - "description": "Timeout for the outbound call (default 10; capped by MAX_DELAY_SECONDS)", - "type": "number" + "description": "Timeout for the outbound call (default 10; capped by MAX_DELAY_SECONDS)\nexample: 15", + "type": "number", + "example": 15 }, "url": { - "description": "Absolute URL to call, e.g. http://other-api:8080/debug", - "type": "string" + "description": "Absolute URL to call (demo: httpbin echoes method/headers back as JSON)\nexample: https://httpbin.org/get", + "type": "string", + "example": "https://httpbin.org/get" } } } }, "securityDefinitions": { "BearerAuth": { - "description": "Click Authorize (lock icon). Value MUST be: Bearer \u003ctoken\u003e including the word Bearer and a space. Example for local podman: Bearer dev. Token only (without Bearer) will 401.", + "description": "Use the top-right Authorize lock only (not a per-operation Authorization field). Value MUST be exactly: Bearer \u003ctoken\u003e e.g. local: Bearer dev", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.json b/docs/swagger.json index b2bce05..e6481b6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -45,7 +45,7 @@ "BearerAuth": [] } ], - "description": "Partial update of live/ready/startup. resetStartupLatch re-runs cold start without restarting the process.", + "description": "Partial update of live/ready/startup. Example: fail readiness so the pod drops from Service endpoints (no restart). resetStartupLatch re-runs cold start without restarting the process. Auth: Authorize lock with \"Bearer dev\".", "consumes": [ "application/json" ], @@ -56,7 +56,7 @@ "operationId": "putProbes", "parameters": [ { - "description": "probe update", + "description": "Partial update — schema examples show fail-readiness shape", "name": "body", "in": "body", "required": true, @@ -135,7 +135,7 @@ "BearerAuth": [] } ], - "description": "Auth required. North→south hits this pod; this pod calls url east-west. Default JSON wrap includes upstream status, headers, and body. Open proxy would be SSRF — keep behind bearer.", + "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", "consumes": [ "application/json" ], @@ -146,25 +146,36 @@ "operationId": "proxy", "parameters": [ { - "description": "proxy request", + "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", "name": "body", "in": "body", - "required": true, "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } }, { "type": "string", - "description": "absolute url (GET form)", + "default": "https://httpbin.org/get", + "example": "https://httpbin.org/get", + "description": "GET form: absolute URL to fetch", "name": "url", "in": "query" }, { "type": "string", - "description": "HTTP method for GET form (default GET)", + "default": "GET", + "example": "GET", + "description": "GET form: HTTP method", "name": "method", "in": "query" + }, + { + "type": "number", + "default": 15, + "example": 15, + "description": "GET form: outbound timeout", + "name": "timeoutSeconds", + "in": "query" } ], "responses": { @@ -208,7 +219,7 @@ "BearerAuth": [] } ], - "description": "Auth required. North→south hits this pod; this pod calls url east-west. Default JSON wrap includes upstream status, headers, and body. Open proxy would be SSRF — keep behind bearer.", + "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", "consumes": [ "application/json" ], @@ -219,25 +230,36 @@ "operationId": "proxy", "parameters": [ { - "description": "proxy request", + "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", "name": "body", "in": "body", - "required": true, "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } }, { "type": "string", - "description": "absolute url (GET form)", + "default": "https://httpbin.org/get", + "example": "https://httpbin.org/get", + "description": "GET form: absolute URL to fetch", "name": "url", "in": "query" }, { "type": "string", - "description": "HTTP method for GET form (default GET)", + "default": "GET", + "example": "GET", + "description": "GET form: HTTP method", "name": "method", "in": "query" + }, + { + "type": "number", + "default": 15, + "example": 15, + "description": "GET form: outbound timeout", + "name": "timeoutSeconds", + "in": "query" } ], "responses": { @@ -297,7 +319,7 @@ }, "/delay/{seconds}": { "get": { - "description": "Sleep N seconds then return 200. Cap is MAX_DELAY_SECONDS env (default 120, hard max 600). For probe timeouts prefer LIVE/READY/STARTUP delaySeconds instead", + "description": "Sleep N seconds then return 200. Cap is MAX_DELAY_SECONDS env (default 120, hard max 600). Try 2 for a slow upstream demo; use probe delaySeconds to trip kube timeoutSeconds.", "produces": [ "text/plain" ], @@ -306,6 +328,8 @@ "parameters": [ { "type": "number", + "default": 2, + "example": 2, "description": "seconds to sleep", "name": "seconds", "in": "path", @@ -324,7 +348,7 @@ }, "/echo": { "get": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -333,6 +357,18 @@ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -344,7 +380,7 @@ } }, "put": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -353,6 +389,18 @@ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -364,7 +412,7 @@ } }, "post": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -373,6 +421,18 @@ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -384,7 +444,7 @@ } }, "delete": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -393,6 +453,18 @@ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -404,7 +476,7 @@ } }, "patch": { - "description": "Bounce method, path, query, headers and body back as json", + "description": "Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived.", "consumes": [ "text/plain" ], @@ -413,6 +485,18 @@ ], "summary": "Echo request", "operationId": "echo", + "parameters": [ + { + "default": "{\"hello\":\"from-swagger\"}", + "example": "{\"hello\":\"from-swagger\"}", + "description": "optional body to echo", + "name": "body", + "in": "body", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -592,7 +676,7 @@ }, "/status/{code}": { "get": { - "description": "Respond with whatever http status you pass (100-599). Great for ingress/retry testing", + "description": "Respond with whatever http status you pass (100-599). Try 418 (teapot), 503, or 502 for ingress/retry tests.", "produces": [ "text/plain" ], @@ -601,6 +685,8 @@ "parameters": [ { "type": "integer", + "default": 418, + "example": 418, "description": "HTTP status code", "name": "code", "in": "path", @@ -644,23 +730,29 @@ "type": "object", "properties": { "bootDelaySeconds": { - "description": "Startup only: wall-clock seconds from process start before a success is allowed.", - "type": "number" + "description": "Startup only: wall-clock seconds from process start before a success is allowed.\nexample: 10", + "type": "number", + "example": 10 }, "delaySeconds": { - "description": "sleep before answering (0–30)", - "type": "number" + "description": "sleep before answering (seconds)\nexample: 0", + "type": "number", + "example": 0 }, "flapEvery": { - "type": "integer" + "description": "Flap: every Nth request fails (0 = use time-based flapSeconds).\nexample: 2", + "type": "integer", + "example": 2 }, "flapSeconds": { - "description": "Flap (mode=flap):\n flapSeconds — wall-clock half-period: ok for N sec, fail for N sec, repeat (default 5).\n flapEvery — if \u003e 0, every Nth request fails instead of using the clock (handy for tests).", - "type": "number" + "description": "Flap (mode=flap): half-period seconds (default 5).\nexample: 5", + "type": "number", + "example": 5 }, "mode": { - "description": "ok | fail | delay | flap", - "type": "string" + "description": "ok | fail | delay | flap\nexample: fail", + "type": "string", + "example": "fail" } } }, @@ -707,7 +799,9 @@ "$ref": "#/definitions/handlers.ProbeConfig" }, "resetStartupLatch": { - "type": "boolean" + "description": "example: true", + "type": "boolean", + "example": true }, "startup": { "$ref": "#/definitions/handlers.ProbeConfig" @@ -721,46 +815,56 @@ ], "properties": { "body": { - "description": "Optional body (string; use for JSON text, form, etc.)", - "type": "string" + "description": "Optional body (string; use for JSON text, form, etc.)\nexample: {\"hello\":\"cluster\"}", + "type": "string", + "example": "{\"hello\":\"cluster\"}" }, "forwardIncomingHeaders": { - "description": "When true (default), copy inbound request headers onto the outbound call\n(minus hop-by-hop). Tracing headers like X-Request-Id ride along.", - "type": "boolean" + "description": "When true (default), copy inbound request headers onto the outbound call\n(minus hop-by-hop). Tracing headers like X-Request-Id ride along.\nexample: true", + "type": "boolean", + "example": true }, "forwardSensitiveHeaders": { - "description": "When true, also forward Authorization / Cookie from the inbound request.\nDefault false so your bearer token for *this* api is not sent east-west by accident.", - "type": "boolean" + "description": "When true, also forward Authorization / Cookie from the inbound request.\nDefault false so your bearer token for *this* api is not sent east-west by accident.\nexample: false", + "type": "boolean", + "example": false }, "headers": { - "description": "Extra headers to set/override on the outbound request", + "description": "Extra headers to set/override on the outbound request\nexample: {\"X-Demo\":\"from-swagger\"}", "type": "object", "additionalProperties": { "type": "string" + }, + "example": { + "X-Demo": "from-swagger" } }, "method": { - "description": "HTTP method (default GET)", - "type": "string" + "description": "HTTP method (default GET)\nexample: GET", + "type": "string", + "example": "GET" }, "raw": { - "description": "When true, return the upstream body as the raw HTTP response (status + headers from upstream).\nDefault false → JSON wrap with request/response/meta (includes upstream headers + body).", - "type": "boolean" + "description": "When true, return the upstream body as the raw HTTP response (status + headers from upstream).\nDefault false → JSON wrap with request/response/meta (includes upstream headers + body).\nexample: false", + "type": "boolean", + "example": false }, "timeoutSeconds": { - "description": "Timeout for the outbound call (default 10; capped by MAX_DELAY_SECONDS)", - "type": "number" + "description": "Timeout for the outbound call (default 10; capped by MAX_DELAY_SECONDS)\nexample: 15", + "type": "number", + "example": 15 }, "url": { - "description": "Absolute URL to call, e.g. http://other-api:8080/debug", - "type": "string" + "description": "Absolute URL to call (demo: httpbin echoes method/headers back as JSON)\nexample: https://httpbin.org/get", + "type": "string", + "example": "https://httpbin.org/get" } } } }, "securityDefinitions": { "BearerAuth": { - "description": "Click Authorize (lock icon). Value MUST be: Bearer \u003ctoken\u003e including the word Bearer and a space. Example for local podman: Bearer dev. Token only (without Bearer) will 401.", + "description": "Use the top-right Authorize lock only (not a per-operation Authorization field). Value MUST be exactly: Bearer \u003ctoken\u003e e.g. local: Bearer dev", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9b6ad66..4024b34 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3,22 +3,34 @@ definitions: handlers.ProbeConfig: properties: bootDelaySeconds: - description: 'Startup only: wall-clock seconds from process start before a - success is allowed.' + description: |- + Startup only: wall-clock seconds from process start before a success is allowed. + example: 10 + example: 10 type: number delaySeconds: - description: sleep before answering (0–30) + description: |- + sleep before answering (seconds) + example: 0 + example: 0 type: number flapEvery: + description: |- + Flap: every Nth request fails (0 = use time-based flapSeconds). + example: 2 + example: 2 type: integer flapSeconds: description: |- - Flap (mode=flap): - flapSeconds — wall-clock half-period: ok for N sec, fail for N sec, repeat (default 5). - flapEvery — if > 0, every Nth request fails instead of using the clock (handy for tests). + Flap (mode=flap): half-period seconds (default 5). + example: 5 + example: 5 type: number mode: - description: ok | fail | delay | flap + description: |- + ok | fail | delay | flap + example: fail + example: fail type: string type: object handlers.ProbeSnapshot: @@ -50,6 +62,8 @@ definitions: ready: $ref: '#/definitions/handlers.ProbeConfig' resetStartupLatch: + description: 'example: true' + example: true type: boolean startup: $ref: '#/definitions/handlers.ProbeConfig' @@ -57,36 +71,58 @@ definitions: handlers.ProxyRequest: properties: body: - description: Optional body (string; use for JSON text, form, etc.) + description: |- + Optional body (string; use for JSON text, form, etc.) + example: {"hello":"cluster"} + example: '{"hello":"cluster"}' type: string forwardIncomingHeaders: description: |- When true (default), copy inbound request headers onto the outbound call (minus hop-by-hop). Tracing headers like X-Request-Id ride along. + example: true + example: true type: boolean forwardSensitiveHeaders: description: |- When true, also forward Authorization / Cookie from the inbound request. Default false so your bearer token for *this* api is not sent east-west by accident. + example: false + example: false type: boolean headers: additionalProperties: type: string - description: Extra headers to set/override on the outbound request + description: |- + Extra headers to set/override on the outbound request + example: {"X-Demo":"from-swagger"} + example: + X-Demo: from-swagger type: object method: - description: HTTP method (default GET) + description: |- + HTTP method (default GET) + example: GET + example: GET type: string raw: description: |- When true, return the upstream body as the raw HTTP response (status + headers from upstream). Default false → JSON wrap with request/response/meta (includes upstream headers + body). + example: false + example: false type: boolean timeoutSeconds: - description: Timeout for the outbound call (default 10; capped by MAX_DELAY_SECONDS) + description: |- + Timeout for the outbound call (default 10; capped by MAX_DELAY_SECONDS) + example: 15 + example: 15 type: number url: - description: Absolute URL to call, e.g. http://other-api:8080/debug + description: |- + Absolute URL to call (demo: httpbin echoes method/headers back as JSON) + example: https://httpbin.org/get + example: https://httpbin.org/get type: string required: - url @@ -124,11 +160,13 @@ paths: put: consumes: - application/json - description: Partial update of live/ready/startup. resetStartupLatch re-runs - cold start without restarting the process. + description: 'Partial update of live/ready/startup. Example: fail readiness + so the pod drops from Service endpoints (no restart). resetStartupLatch re-runs + cold start without restarting the process. Auth: Authorize lock with "Bearer + dev".' operationId: putProbes parameters: - - description: probe update + - description: Partial update — schema examples show fail-readiness shape in: body name: body required: true @@ -184,25 +222,36 @@ paths: get: consumes: - application/json - description: Auth required. North→south hits this pod; this pod calls url east-west. - Default JSON wrap includes upstream status, headers, and body. Open proxy - would be SSRF — keep behind bearer. + description: 'Auth required (use Authorize lock: "Bearer dev"). North→south + hits this pod; this pod calls url east-west. Response wrap includes upstream + status, headers, and body. Try GET with url=https://httpbin.org/get or POST + body example. Open proxy is SSRF — keep behind bearer.' operationId: proxy parameters: - - description: proxy request + - description: POST JSON body (preferred for full control). Example hits httpbin + so you see headers/method echoed. in: body name: body - required: true schema: $ref: '#/definitions/handlers.ProxyRequest' - - description: absolute url (GET form) + - default: https://httpbin.org/get + description: 'GET form: absolute URL to fetch' + example: https://httpbin.org/get in: query name: url type: string - - description: HTTP method for GET form (default GET) + - default: GET + description: 'GET form: HTTP method' + example: GET in: query name: method type: string + - default: 15 + description: 'GET form: outbound timeout' + example: 15 + in: query + name: timeoutSeconds + type: number produces: - application/json responses: @@ -234,25 +283,36 @@ paths: post: consumes: - application/json - description: Auth required. North→south hits this pod; this pod calls url east-west. - Default JSON wrap includes upstream status, headers, and body. Open proxy - would be SSRF — keep behind bearer. + description: 'Auth required (use Authorize lock: "Bearer dev"). North→south + hits this pod; this pod calls url east-west. Response wrap includes upstream + status, headers, and body. Try GET with url=https://httpbin.org/get or POST + body example. Open proxy is SSRF — keep behind bearer.' operationId: proxy parameters: - - description: proxy request + - description: POST JSON body (preferred for full control). Example hits httpbin + so you see headers/method echoed. in: body name: body - required: true schema: $ref: '#/definitions/handlers.ProxyRequest' - - description: absolute url (GET form) + - default: https://httpbin.org/get + description: 'GET form: absolute URL to fetch' + example: https://httpbin.org/get in: query name: url type: string - - description: HTTP method for GET form (default GET) + - default: GET + description: 'GET form: HTTP method' + example: GET in: query name: method type: string + - default: 15 + description: 'GET form: outbound timeout' + example: 15 + in: query + name: timeoutSeconds + type: number produces: - application/json responses: @@ -297,11 +357,13 @@ paths: /delay/{seconds}: get: description: Sleep N seconds then return 200. Cap is MAX_DELAY_SECONDS env (default - 120, hard max 600). For probe timeouts prefer LIVE/READY/STARTUP delaySeconds - instead + 120, hard max 600). Try 2 for a slow upstream demo; use probe delaySeconds + to trip kube timeoutSeconds. operationId: delay parameters: - - description: seconds to sleep + - default: 2 + description: seconds to sleep + example: 2 in: path name: seconds required: true @@ -318,8 +380,17 @@ paths: delete: consumes: - text/plain - description: Bounce method, path, query, headers and body back as json + description: Bounce method, path, query, headers and body back as json. Try + POST with any body and custom headers (e.g. X-Request-Id) to see what arrived. operationId: echo + parameters: + - default: '{"hello":"from-swagger"}' + description: optional body to echo + example: '{"hello":"from-swagger"}' + in: body + name: body + schema: + type: string produces: - application/json responses: @@ -332,8 +403,17 @@ paths: get: consumes: - text/plain - description: Bounce method, path, query, headers and body back as json + description: Bounce method, path, query, headers and body back as json. Try + POST with any body and custom headers (e.g. X-Request-Id) to see what arrived. operationId: echo + parameters: + - default: '{"hello":"from-swagger"}' + description: optional body to echo + example: '{"hello":"from-swagger"}' + in: body + name: body + schema: + type: string produces: - application/json responses: @@ -346,8 +426,17 @@ paths: patch: consumes: - text/plain - description: Bounce method, path, query, headers and body back as json + description: Bounce method, path, query, headers and body back as json. Try + POST with any body and custom headers (e.g. X-Request-Id) to see what arrived. operationId: echo + parameters: + - default: '{"hello":"from-swagger"}' + description: optional body to echo + example: '{"hello":"from-swagger"}' + in: body + name: body + schema: + type: string produces: - application/json responses: @@ -360,8 +449,17 @@ paths: post: consumes: - text/plain - description: Bounce method, path, query, headers and body back as json + description: Bounce method, path, query, headers and body back as json. Try + POST with any body and custom headers (e.g. X-Request-Id) to see what arrived. operationId: echo + parameters: + - default: '{"hello":"from-swagger"}' + description: optional body to echo + example: '{"hello":"from-swagger"}' + in: body + name: body + schema: + type: string produces: - application/json responses: @@ -374,8 +472,17 @@ paths: put: consumes: - text/plain - description: Bounce method, path, query, headers and body back as json + description: Bounce method, path, query, headers and body back as json. Try + POST with any body and custom headers (e.g. X-Request-Id) to see what arrived. operationId: echo + parameters: + - default: '{"hello":"from-swagger"}' + description: optional body to echo + example: '{"hello":"from-swagger"}' + in: body + name: body + schema: + type: string produces: - application/json responses: @@ -501,11 +608,13 @@ paths: summary: Startup (startupz) /status/{code}: get: - description: Respond with whatever http status you pass (100-599). Great for - ingress/retry testing + description: Respond with whatever http status you pass (100-599). Try 418 (teapot), + 503, or 502 for ingress/retry tests. operationId: status parameters: - - description: HTTP status code + - default: 418 + description: HTTP status code + example: 418 in: path name: code required: true @@ -534,9 +643,8 @@ paths: summary: Version / build info securityDefinitions: BearerAuth: - description: 'Click Authorize (lock icon). Value MUST be: Bearer including - the word Bearer and a space. Example for local podman: Bearer dev. Token only - (without Bearer) will 401.' + description: 'Use the top-right Authorize lock only (not a per-operation Authorization + field). Value MUST be exactly: Bearer e.g. local: Bearer dev' in: header name: Authorization type: apiKey diff --git a/handlers/handlers.go b/handlers/handlers.go index c8ed7d6..541b7f0 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -126,10 +126,10 @@ func DebugHandler(c *gin.Context) { } // @Summary Fixed status code -// @Description Respond with whatever http status you pass (100-599). Great for ingress/retry testing +// @Description Respond with whatever http status you pass (100-599). Try 418 (teapot), 503, or 502 for ingress/retry tests. // @ID status // @Produce plain -// @Param code path int true "HTTP status code" +// @Param code path int true "HTTP status code" default(418) example(418) // @Success 200 {string} string "status body" // @Router /status/{code} [get] func StatusHandler(c *gin.Context) { @@ -142,10 +142,10 @@ func StatusHandler(c *gin.Context) { } // @Summary Delay then OK -// @Description Sleep N seconds then return 200. Cap is MAX_DELAY_SECONDS env (default 120, hard max 600). For probe timeouts prefer LIVE/READY/STARTUP delaySeconds instead +// @Description Sleep N seconds then return 200. Cap is MAX_DELAY_SECONDS env (default 120, hard max 600). Try 2 for a slow upstream demo; use probe delaySeconds to trip kube timeoutSeconds. // @ID delay // @Produce plain -// @Param seconds path number true "seconds to sleep" +// @Param seconds path number true "seconds to sleep" default(2) example(2) // @Success 200 {string} string "delayed" // @Router /delay/{seconds} [get] func DelayHandler(c *gin.Context) { @@ -162,10 +162,11 @@ func DelayHandler(c *gin.Context) { } // @Summary Echo request -// @Description Bounce method, path, query, headers and body back as json +// @Description Bounce method, path, query, headers and body back as json. Try POST with any body and custom headers (e.g. X-Request-Id) to see what arrived. // @ID echo // @Accept plain // @Produce json +// @Param body body string false "optional body to echo" default({"hello":"from-swagger"}) example({"hello":"from-swagger"}) // @Success 200 {object} map[string]interface{} // @Router /echo [get] // @Router /echo [post] diff --git a/handlers/probes.go b/handlers/probes.go index 6dbe54f..dbfd8ab 100644 --- a/handlers/probes.go +++ b/handlers/probes.go @@ -41,15 +41,21 @@ func maxDelaySeconds() float64 { // ProbeConfig is the knobs for one probe type (live / ready / startup). type ProbeConfig struct { - Mode string `json:"mode"` // ok | fail | delay | flap - DelaySeconds float64 `json:"delaySeconds"` // sleep before answering (0–30) + // ok | fail | delay | flap + // example: fail + Mode string `json:"mode" example:"fail"` + // sleep before answering (seconds) + // example: 0 + DelaySeconds float64 `json:"delaySeconds" example:"0"` // Startup only: wall-clock seconds from process start before a success is allowed. - BootDelaySeconds float64 `json:"bootDelaySeconds,omitempty"` - // Flap (mode=flap): - // flapSeconds — wall-clock half-period: ok for N sec, fail for N sec, repeat (default 5). - // flapEvery — if > 0, every Nth request fails instead of using the clock (handy for tests). - FlapSeconds float64 `json:"flapSeconds,omitempty"` - FlapEvery int `json:"flapEvery,omitempty"` + // example: 10 + BootDelaySeconds float64 `json:"bootDelaySeconds,omitempty" example:"10"` + // Flap (mode=flap): half-period seconds (default 5). + // example: 5 + FlapSeconds float64 `json:"flapSeconds,omitempty" example:"5"` + // Flap: every Nth request fails (0 = use time-based flapSeconds). + // example: 2 + FlapEvery int `json:"flapEvery,omitempty" example:"2"` } // ProbeSnapshot is what /a/control/probes returns (includes runtime bits). @@ -71,7 +77,8 @@ type ProbeUpdate struct { Live *ProbeConfig `json:"live,omitempty"` Ready *ProbeConfig `json:"ready,omitempty"` Startup *ProbeConfig `json:"startup,omitempty"` - ResetStartupLatch *bool `json:"resetStartupLatch,omitempty"` + // example: true + ResetStartupLatch *bool `json:"resetStartupLatch,omitempty" example:"true"` } type probeState struct { @@ -397,12 +404,12 @@ func GetProbesHandler(c *gin.Context) { } // @Summary Update probe control state -// @Description Partial update of live/ready/startup. resetStartupLatch re-runs cold start without restarting the process. +// @Description Partial update of live/ready/startup. Example: fail readiness so the pod drops from Service endpoints (no restart). resetStartupLatch re-runs cold start without restarting the process. Auth: Authorize lock with "Bearer dev". // @ID putProbes // @Accept json // @Produce json // @Security BearerAuth -// @Param body body ProbeUpdate true "probe update" +// @Param body body ProbeUpdate true "Partial update — schema examples show fail-readiness shape" // @Success 200 {object} ProbeSnapshot // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string diff --git a/handlers/proxy.go b/handlers/proxy.go index 28be97d..c5d4624 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -38,36 +38,45 @@ var sensitiveForwardHeaders = map[string]bool{ // ProxyRequest is the JSON body for POST /a/proxy. // North-south hits this pod; we call another URL east-west (auth required — open proxy is SSRF). type ProxyRequest struct { - // Absolute URL to call, e.g. http://other-api:8080/debug - URL string `json:"url" binding:"required"` + // Absolute URL to call (demo: httpbin echoes method/headers back as JSON) + // example: https://httpbin.org/get + URL string `json:"url" example:"https://httpbin.org/get" binding:"required"` // HTTP method (default GET) - Method string `json:"method,omitempty"` + // example: GET + Method string `json:"method,omitempty" example:"GET"` // Extra headers to set/override on the outbound request - Headers map[string]string `json:"headers,omitempty"` + // example: {"X-Demo":"from-swagger"} + Headers map[string]string `json:"headers,omitempty" example:"X-Demo:from-swagger"` // Optional body (string; use for JSON text, form, etc.) - Body string `json:"body,omitempty"` + // example: {"hello":"cluster"} + Body string `json:"body,omitempty" example:"{\"hello\":\"cluster\"}"` // Timeout for the outbound call (default 10; capped by MAX_DELAY_SECONDS) - TimeoutSeconds float64 `json:"timeoutSeconds,omitempty"` + // example: 15 + TimeoutSeconds float64 `json:"timeoutSeconds,omitempty" example:"15"` // When true (default), copy inbound request headers onto the outbound call // (minus hop-by-hop). Tracing headers like X-Request-Id ride along. - ForwardIncomingHeaders *bool `json:"forwardIncomingHeaders,omitempty"` + // example: true + ForwardIncomingHeaders *bool `json:"forwardIncomingHeaders,omitempty" example:"true"` // When true, also forward Authorization / Cookie from the inbound request. // Default false so your bearer token for *this* api is not sent east-west by accident. - ForwardSensitiveHeaders bool `json:"forwardSensitiveHeaders,omitempty"` + // example: false + ForwardSensitiveHeaders bool `json:"forwardSensitiveHeaders,omitempty" example:"false"` // When true, return the upstream body as the raw HTTP response (status + headers from upstream). // Default false → JSON wrap with request/response/meta (includes upstream headers + body). - Raw bool `json:"raw,omitempty"` + // example: false + Raw bool `json:"raw,omitempty" example:"false"` } // @Summary Proxy / hop to another service (auth) -// @Description Auth required. North→south hits this pod; this pod calls url east-west. Default JSON wrap includes upstream status, headers, and body. Open proxy would be SSRF — keep behind bearer. +// @Description Auth required (use Authorize lock: "Bearer dev"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer. // @ID proxy // @Security BearerAuth // @Accept json // @Produce json -// @Param body body ProxyRequest true "proxy request" -// @Param url query string false "absolute url (GET form)" -// @Param method query string false "HTTP method for GET form (default GET)" +// @Param body body ProxyRequest false "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed." +// @Param url query string false "GET form: absolute URL to fetch" default(https://httpbin.org/get) example(https://httpbin.org/get) +// @Param method query string false "GET form: HTTP method" default(GET) example(GET) +// @Param timeoutSeconds query number false "GET form: outbound timeout" default(15) example(15) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string diff --git a/main.go b/main.go index 7d5faef..5d865b5 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization -// @description Click Authorize (lock icon). Value MUST be: Bearer including the word Bearer and a space. Example for local podman: Bearer dev. Token only (without Bearer) will 401. +// @description Use the top-right Authorize lock only (not a per-operation Authorization field). Value MUST be exactly: Bearer e.g. local: Bearer dev package main From 48fcd33bcdd8930e9f273ea55b826e4aaf02ce51 Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:26:19 +1000 Subject: [PATCH 4/5] fix: swagger GET proxy no body + better API description Split proxy GET/POST handlers so Try it out does not send a body on GET (browser fetch error). Refresh top swagger title/description for the cluster-debug story. Default GET url remains httpbin.org/get. --- docs/docs.go | 64 ++++++++----------------------- docs/swagger.json | 64 ++++++++----------------------- docs/swagger.yaml | 77 +++++++++++++------------------------ handlers/proxy.go | 98 +++++++++++++++++++++++++++++------------------ main.go | 13 +++++-- routes/routes.go | 5 ++- 6 files changed, 128 insertions(+), 193 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 77e64b1..e2c6a18 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -142,29 +142,18 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", - "consumes": [ - "application/json" - ], + "description": "Auth: top-right Authorize with \"Bearer dev\". Simple query-only hop (no body — browsers forbid GET+body). Default url hits httpbin so you see method/headers echoed. Prefer POST /a/proxy for full JSON control.", "produces": [ "application/json" ], - "summary": "Proxy / hop to another service (auth)", - "operationId": "proxy", + "summary": "Proxy GET hop (auth)", + "operationId": "proxyGet", "parameters": [ - { - "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", - "name": "body", - "in": "body", - "schema": { - "$ref": "#/definitions/handlers.ProxyRequest" - } - }, { "type": "string", "default": "https://httpbin.org/get", "example": "https://httpbin.org/get", - "description": "GET form: absolute URL to fetch", + "description": "absolute URL to fetch", "name": "url", "in": "query" }, @@ -172,7 +161,7 @@ const docTemplate = `{ "type": "string", "default": "GET", "example": "GET", - "description": "GET form: HTTP method", + "description": "HTTP method for the outbound call", "name": "method", "in": "query" }, @@ -180,7 +169,7 @@ const docTemplate = `{ "type": "number", "default": 15, "example": 15, - "description": "GET form: outbound timeout", + "description": "outbound timeout seconds", "name": "timeoutSeconds", "in": "query" } @@ -226,47 +215,24 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", + "description": "Auth: top-right Authorize with \"Bearer dev\". Full JSON control — example body calls https://httpbin.org/get so the wrap shows upstream status/headers/body. North→south then east-west; SSRF if left unauthenticated.", "consumes": [ "application/json" ], "produces": [ "application/json" ], - "summary": "Proxy / hop to another service (auth)", - "operationId": "proxy", + "summary": "Proxy POST hop (auth)", + "operationId": "proxyPost", "parameters": [ { - "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", + "description": "proxy request", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } - }, - { - "type": "string", - "default": "https://httpbin.org/get", - "example": "https://httpbin.org/get", - "description": "GET form: absolute URL to fetch", - "name": "url", - "in": "query" - }, - { - "type": "string", - "default": "GET", - "example": "GET", - "description": "GET form: HTTP method", - "name": "method", - "in": "query" - }, - { - "type": "number", - "default": 15, - "example": 15, - "description": "GET form: outbound timeout", - "name": "timeoutSeconds", - "in": "query" } ], "responses": { @@ -871,7 +837,7 @@ const docTemplate = `{ }, "securityDefinitions": { "BearerAuth": { - "description": "Use the top-right Authorize lock only (not a per-operation Authorization field). Value MUST be exactly: Bearer \u003ctoken\u003e e.g. local: Bearer dev", + "description": "Top-right Authorize lock only. Value MUST be: Bearer \u003ctoken\u003e e.g. Bearer dev", "type": "apiKey", "name": "Authorization", "in": "header" @@ -881,12 +847,12 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "2.0", + Version: "2.5.0", Host: "", BasePath: "/", Schemes: []string{}, - Title: "Cluster Util API", - Description: "Drop-in HTTP util for testing probes, routing, headers, env/params and more in a cluster. Swagger \"Try it out\" uses the host you opened the UI on (or override with ?host=host:port&scheme=http on /api-docs/index.html). Authorize with Bearer token from logs or AUTH_TOKEN.", + Title: "Cluster Utils API", + Description: "HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops.\n\n• Authorize (top-right lock): Bearer <token> e.g. local podman → Bearer dev\n• Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes\n• Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body)\n• Pair: https://github.com/donkeyx/cluster-utils", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index e6481b6..d7209c9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,10 +1,10 @@ { "swagger": "2.0", "info": { - "description": "Drop-in HTTP util for testing probes, routing, headers, env/params and more in a cluster. Swagger \"Try it out\" uses the host you opened the UI on (or override with ?host=host:port\u0026scheme=http on /api-docs/index.html). Authorize with Bearer token from logs or AUTH_TOKEN.", - "title": "Cluster Util API", + "description": "HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops.\n\n• Authorize (top-right lock): Bearer \u0026lt;token\u0026gt; e.g. local podman → Bearer dev\n• Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes\n• Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body)\n• Pair: https://github.com/donkeyx/cluster-utils", + "title": "Cluster Utils API", "contact": {}, - "version": "2.0" + "version": "2.5.0" }, "basePath": "/", "paths": { @@ -135,29 +135,18 @@ "BearerAuth": [] } ], - "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", - "consumes": [ - "application/json" - ], + "description": "Auth: top-right Authorize with \"Bearer dev\". Simple query-only hop (no body — browsers forbid GET+body). Default url hits httpbin so you see method/headers echoed. Prefer POST /a/proxy for full JSON control.", "produces": [ "application/json" ], - "summary": "Proxy / hop to another service (auth)", - "operationId": "proxy", + "summary": "Proxy GET hop (auth)", + "operationId": "proxyGet", "parameters": [ - { - "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", - "name": "body", - "in": "body", - "schema": { - "$ref": "#/definitions/handlers.ProxyRequest" - } - }, { "type": "string", "default": "https://httpbin.org/get", "example": "https://httpbin.org/get", - "description": "GET form: absolute URL to fetch", + "description": "absolute URL to fetch", "name": "url", "in": "query" }, @@ -165,7 +154,7 @@ "type": "string", "default": "GET", "example": "GET", - "description": "GET form: HTTP method", + "description": "HTTP method for the outbound call", "name": "method", "in": "query" }, @@ -173,7 +162,7 @@ "type": "number", "default": 15, "example": 15, - "description": "GET form: outbound timeout", + "description": "outbound timeout seconds", "name": "timeoutSeconds", "in": "query" } @@ -219,47 +208,24 @@ "BearerAuth": [] } ], - "description": "Auth required (use Authorize lock: \"Bearer dev\"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer.", + "description": "Auth: top-right Authorize with \"Bearer dev\". Full JSON control — example body calls https://httpbin.org/get so the wrap shows upstream status/headers/body. North→south then east-west; SSRF if left unauthenticated.", "consumes": [ "application/json" ], "produces": [ "application/json" ], - "summary": "Proxy / hop to another service (auth)", - "operationId": "proxy", + "summary": "Proxy POST hop (auth)", + "operationId": "proxyPost", "parameters": [ { - "description": "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed.", + "description": "proxy request", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } - }, - { - "type": "string", - "default": "https://httpbin.org/get", - "example": "https://httpbin.org/get", - "description": "GET form: absolute URL to fetch", - "name": "url", - "in": "query" - }, - { - "type": "string", - "default": "GET", - "example": "GET", - "description": "GET form: HTTP method", - "name": "method", - "in": "query" - }, - { - "type": "number", - "default": 15, - "example": 15, - "description": "GET form: outbound timeout", - "name": "timeoutSeconds", - "in": "query" } ], "responses": { @@ -864,7 +830,7 @@ }, "securityDefinitions": { "BearerAuth": { - "description": "Use the top-right Authorize lock only (not a per-operation Authorization field). Value MUST be exactly: Bearer \u003ctoken\u003e e.g. local: Bearer dev", + "description": "Top-right Authorize lock only. Value MUST be: Bearer \u003ctoken\u003e e.g. Bearer dev", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4024b34..1f6c9ce 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -129,12 +129,15 @@ definitions: type: object info: contact: {} - description: Drop-in HTTP util for testing probes, routing, headers, env/params - and more in a cluster. Swagger "Try it out" uses the host you opened the UI on - (or override with ?host=host:port&scheme=http on /api-docs/index.html). Authorize - with Bearer token from logs or AUTH_TOKEN. - title: Cluster Util API - version: "2.0" + description: |- + HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops. + + • Authorize (top-right lock): Bearer <token> e.g. local podman → Bearer dev + • Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes + • Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body) + • Pair: https://github.com/donkeyx/cluster-utils + title: Cluster Utils API + version: 2.5.0 paths: /a/control/probes: get: @@ -220,34 +223,25 @@ paths: summary: Get environment variables /a/proxy: get: - consumes: - - application/json - description: 'Auth required (use Authorize lock: "Bearer dev"). North→south - hits this pod; this pod calls url east-west. Response wrap includes upstream - status, headers, and body. Try GET with url=https://httpbin.org/get or POST - body example. Open proxy is SSRF — keep behind bearer.' - operationId: proxy + description: 'Auth: top-right Authorize with "Bearer dev". Simple query-only + hop (no body — browsers forbid GET+body). Default url hits httpbin so you + see method/headers echoed. Prefer POST /a/proxy for full JSON control.' + operationId: proxyGet parameters: - - description: POST JSON body (preferred for full control). Example hits httpbin - so you see headers/method echoed. - in: body - name: body - schema: - $ref: '#/definitions/handlers.ProxyRequest' - default: https://httpbin.org/get - description: 'GET form: absolute URL to fetch' + description: absolute URL to fetch example: https://httpbin.org/get in: query name: url type: string - default: GET - description: 'GET form: HTTP method' + description: HTTP method for the outbound call example: GET in: query name: method type: string - default: 15 - description: 'GET form: outbound timeout' + description: outbound timeout seconds example: 15 in: query name: timeoutSeconds @@ -279,40 +273,21 @@ paths: type: object security: - BearerAuth: [] - summary: Proxy / hop to another service (auth) + summary: Proxy GET hop (auth) post: consumes: - application/json - description: 'Auth required (use Authorize lock: "Bearer dev"). North→south - hits this pod; this pod calls url east-west. Response wrap includes upstream - status, headers, and body. Try GET with url=https://httpbin.org/get or POST - body example. Open proxy is SSRF — keep behind bearer.' - operationId: proxy + description: 'Auth: top-right Authorize with "Bearer dev". Full JSON control + — example body calls https://httpbin.org/get so the wrap shows upstream status/headers/body. + North→south then east-west; SSRF if left unauthenticated.' + operationId: proxyPost parameters: - - description: POST JSON body (preferred for full control). Example hits httpbin - so you see headers/method echoed. + - description: proxy request in: body name: body + required: true schema: $ref: '#/definitions/handlers.ProxyRequest' - - default: https://httpbin.org/get - description: 'GET form: absolute URL to fetch' - example: https://httpbin.org/get - in: query - name: url - type: string - - default: GET - description: 'GET form: HTTP method' - example: GET - in: query - name: method - type: string - - default: 15 - description: 'GET form: outbound timeout' - example: 15 - in: query - name: timeoutSeconds - type: number produces: - application/json responses: @@ -340,7 +315,7 @@ paths: type: object security: - BearerAuth: [] - summary: Proxy / hop to another service (auth) + summary: Proxy POST hop (auth) /debug: get: description: Hostname, client ip, headers, uri — good for routing tests @@ -643,8 +618,8 @@ paths: summary: Version / build info securityDefinitions: BearerAuth: - description: 'Use the top-right Authorize lock only (not a per-operation Authorization - field). Value MUST be exactly: Bearer e.g. local: Bearer dev' + description: 'Top-right Authorize lock only. Value MUST be: Bearer e.g. + Bearer dev' in: header name: Authorization type: apiKey diff --git a/handlers/proxy.go b/handlers/proxy.go index c5d4624..68bda16 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -67,58 +67,84 @@ type ProxyRequest struct { Raw bool `json:"raw,omitempty" example:"false"` } -// @Summary Proxy / hop to another service (auth) -// @Description Auth required (use Authorize lock: "Bearer dev"). North→south hits this pod; this pod calls url east-west. Response wrap includes upstream status, headers, and body. Try GET with url=https://httpbin.org/get or POST body example. Open proxy is SSRF — keep behind bearer. -// @ID proxy +// @Summary Proxy GET hop (auth) +// @Description Auth: top-right Authorize with "Bearer dev". Simple query-only hop (no body — browsers forbid GET+body). Default url hits httpbin so you see method/headers echoed. Prefer POST /a/proxy for full JSON control. +// @ID proxyGet +// @Security BearerAuth +// @Produce json +// @Param url query string false "absolute URL to fetch" default(https://httpbin.org/get) example(https://httpbin.org/get) +// @Param method query string false "HTTP method for the outbound call" default(GET) example(GET) +// @Param timeoutSeconds query number false "outbound timeout seconds" default(15) example(15) +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Failure 502 {object} map[string]interface{} +// @Router /a/proxy [get] +func ProxyGetHandler(c *gin.Context) { + req := ProxyRequest{ + URL: c.Query("url"), + Method: c.DefaultQuery("method", http.MethodGet), + } + if t := c.Query("timeoutSeconds"); t != "" { + if f, err := strconv.ParseFloat(t, 64); err == nil { + req.TimeoutSeconds = f + } + } + if c.Query("forwardIncomingHeaders") == "0" || c.Query("forwardIncomingHeaders") == "false" { + f := false + req.ForwardIncomingHeaders = &f + } + if c.Query("forwardSensitiveHeaders") == "1" || c.Query("forwardSensitiveHeaders") == "true" { + req.ForwardSensitiveHeaders = true + } + if c.Query("raw") == "1" || c.Query("raw") == "true" { + req.Raw = true + } + // swagger default url if empty + if strings.TrimSpace(req.URL) == "" { + req.URL = "https://httpbin.org/get" + } + doProxy(c, req) +} + +// @Summary Proxy POST hop (auth) +// @Description Auth: top-right Authorize with "Bearer dev". Full JSON control — example body calls https://httpbin.org/get so the wrap shows upstream status/headers/body. North→south then east-west; SSRF if left unauthenticated. +// @ID proxyPost // @Security BearerAuth // @Accept json // @Produce json -// @Param body body ProxyRequest false "POST JSON body (preferred for full control). Example hits httpbin so you see headers/method echoed." -// @Param url query string false "GET form: absolute URL to fetch" default(https://httpbin.org/get) example(https://httpbin.org/get) -// @Param method query string false "GET form: HTTP method" default(GET) example(GET) -// @Param timeoutSeconds query number false "GET form: outbound timeout" default(15) example(15) +// @Param body body ProxyRequest true "proxy request" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 502 {object} map[string]interface{} // @Router /a/proxy [post] -// @Router /a/proxy [get] -func ProxyHandler(c *gin.Context) { +func ProxyPostHandler(c *gin.Context) { var req ProxyRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json: " + err.Error()}) + return + } + doProxy(c, req) +} - // GET convenience: /a/proxy?url=http://svc:8080/debug&method=GET +// ProxyHandler kept for tests / any external refs — dispatches by method. +func ProxyHandler(c *gin.Context) { if c.Request.Method == http.MethodGet { - req.URL = c.Query("url") - req.Method = c.DefaultQuery("method", http.MethodGet) - if t := c.Query("timeoutSeconds"); t != "" { - if f, err := strconv.ParseFloat(t, 64); err == nil { - req.TimeoutSeconds = f - } - } - if c.Query("forwardIncomingHeaders") == "0" || c.Query("forwardIncomingHeaders") == "false" { - f := false - req.ForwardIncomingHeaders = &f - } - if c.Query("forwardSensitiveHeaders") == "1" || c.Query("forwardSensitiveHeaders") == "true" { - req.ForwardSensitiveHeaders = true - } - if c.Query("raw") == "1" || c.Query("raw") == "true" { - req.Raw = true - } - } else { - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json: " + err.Error()}) - return - } + ProxyGetHandler(c) + return } + ProxyPostHandler(c) +} +func doProxy(c *gin.Context, req ProxyRequest) { if strings.TrimSpace(req.URL) == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"}) return } u, err := url.Parse(req.URL) if err != nil || u.Scheme == "" || u.Host == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "url must be absolute, e.g. http://other-svc:8080/debug"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "url must be absolute, e.g. https://httpbin.org/get or http://other-svc:8080/debug"}) return } if u.Scheme != "http" && u.Scheme != "https" { @@ -153,7 +179,6 @@ func ProxyHandler(c *gin.Context) { return } - // 1) optional: copy north-south inbound headers → east-west outbound if forward { for k, vals := range c.Request.Header { lk := strings.ToLower(k) @@ -169,7 +194,6 @@ func ProxyHandler(c *gin.Context) { } } - // 2) explicit headers win (including Authorization if you set it here on purpose) for k, v := range req.Headers { outReq.Header.Set(k, v) } @@ -180,7 +204,6 @@ func ProxyHandler(c *gin.Context) { hostname, _ := os.Hostname() outReq.Header.Add("X-Cu-Proxy-Hop", hostname) - // otelhttp propagates W3C trace context east-west and creates a client span client := &http.Client{ Timeout: time.Duration(timeout * float64(time.Second)), Transport: otelhttp.NewTransport(http.DefaultTransport), @@ -200,7 +223,7 @@ func ProxyHandler(c *gin.Context) { } defer resp.Body.Close() - respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) // 2MB cap in debug wrap + respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) respHeaders := map[string][]string{} for k, v := range resp.Header { respHeaders[k] = v @@ -221,7 +244,6 @@ func ProxyHandler(c *gin.Context) { sentHeaders[k] = v } - // Default wrap: full upstream response (status + headers + body) for mesh debugging c.JSON(http.StatusOK, gin.H{ "request": gin.H{ "url": req.URL, diff --git a/main.go b/main.go index 5d865b5..aa08650 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,16 @@ -// @title Cluster Util API -// @version 2.0 -// @description Drop-in HTTP util for testing probes, routing, headers, env/params and more in a cluster. Swagger "Try it out" uses the host you opened the UI on (or override with ?host=host:port&scheme=http on /api-docs/index.html). Authorize with Bearer token from logs or AUTH_TOKEN. +// @title Cluster Utils API +// @version 2.5.0 +// @description HTTP sidekick for cluster-utils — drop into a namespace and poke probes, ingress headers, env, and east-west hops. +// @description +// @description • Authorize (top-right lock): Bearer <token> e.g. local podman → Bearer dev +// @description • Probes: /startupz /livez /readyz · Control: GET|PUT /a/control/probes +// @description • Proxy: GET /a/proxy?url=https://httpbin.org/get (or POST JSON body) +// @description • Pair: https://github.com/donkeyx/cluster-utils // @BasePath / // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization -// @description Use the top-right Authorize lock only (not a per-operation Authorization field). Value MUST be exactly: Bearer e.g. local: Bearer dev +// @description Top-right Authorize lock only. Value MUST be: Bearer e.g. Bearer dev package main diff --git a/routes/routes.go b/routes/routes.go index b83ac6d..93f984f 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -64,8 +64,9 @@ func SetupRouter(logger *zap.Logger, st string, r *gin.Engine) { authGroup.GET("/control/probes", handlers.GetProbesHandler) authGroup.PUT("/control/probes", handlers.PutProbesHandler) // open /proxy would be SSRF (scan cluster, hit metadata, etc.) - authGroup.GET("/proxy", handlers.ProxyHandler) - authGroup.POST("/proxy", handlers.ProxyHandler) + // Separate handlers so Swagger GET has no body (browsers reject GET+body). + authGroup.GET("/proxy", handlers.ProxyGetHandler) + authGroup.POST("/proxy", handlers.ProxyPostHandler) } func swaggerHandler() gin.HandlerFunc { From 5a021d1be1ee1fdccf536d888f40c971def3f27b Mon Sep 17 00:00:00 2001 From: David Binney Date: Mon, 10 Aug 2026 23:27:14 +1000 Subject: [PATCH 5/5] fix: install ca-certificates for HTTPS proxy and OTEL --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 01a1a22..8acbfec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,10 @@ RUN make build # no longer using musl dns moved to debian FROM debian:stable-slim WORKDIR /app +# ca-certificates required for HTTPS (proxy, OTEL OTLP, etc.) +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/bin . RUN ln -s /app/cu-api /usr/local/bin/cu-api; ln -s /app/cu-api /usr/local/bin/node; ln -s /app/cu-api /usr/local/bin/npm; EXPOSE 8080