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 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. --- diff --git a/docs/docs.go b/docs/docs.go index b0efce2..e2c6a18 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, @@ -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", @@ -152,44 +142,36 @@ 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.", - "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": "proxy request", - "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": "absolute URL to fetch", "name": "url", "in": "query" }, { "type": "string", - "description": "HTTP method for GET form (default GET)", + "default": "GET", + "example": "GET", + "description": "HTTP method for the outbound call", "name": "method", "in": "query" }, { - "type": "string", - "default": "Bearer", - "description": "Bearer token", - "name": "Authorization", - "in": "header", - "required": true + "type": "number", + "default": 15, + "example": 15, + "description": "outbound timeout seconds", + "name": "timeoutSeconds", + "in": "query" } ], "responses": { @@ -233,15 +215,15 @@ 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: 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": "proxy request", @@ -251,26 +233,6 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } - }, - { - "type": "string", - "description": "absolute url (GET form)", - "name": "url", - "in": "query" - }, - { - "type": "string", - "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": { @@ -330,7 +292,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" ], @@ -339,6 +301,8 @@ const docTemplate = `{ "parameters": [ { "type": "number", + "default": 2, + "example": 2, "description": "seconds to sleep", "name": "seconds", "in": "path", @@ -357,7 +321,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" ], @@ -366,6 +330,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", @@ -377,7 +353,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" ], @@ -386,6 +362,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", @@ -397,7 +385,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" ], @@ -406,6 +394,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", @@ -417,7 +417,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" ], @@ -426,6 +426,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", @@ -437,7 +449,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" ], @@ -446,6 +458,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", @@ -625,7 +649,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" ], @@ -634,6 +658,8 @@ const docTemplate = `{ "parameters": [ { "type": "integer", + "default": 418, + "example": 418, "description": "HTTP status code", "name": "code", "in": "path", @@ -677,23 +703,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" } } }, @@ -740,7 +772,9 @@ const docTemplate = `{ "$ref": "#/definitions/handlers.ProbeConfig" }, "resetStartupLatch": { - "type": "boolean" + "description": "example: true", + "type": "boolean", + "example": true }, "startup": { "$ref": "#/definitions/handlers.ProbeConfig" @@ -754,46 +788,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": "Paste: Bearer \u003ctoken\u003e (token from container logs, or AUTH_TOKEN env). Example: Bearer dev", + "description": "Top-right Authorize lock only. Value MUST be: Bearer \u003ctoken\u003e e.g. Bearer dev", "type": "apiKey", "name": "Authorization", "in": "header" @@ -803,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 047f92f..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": { @@ -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, @@ -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", @@ -145,44 +135,36 @@ "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.", - "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": "proxy request", - "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": "absolute URL to fetch", "name": "url", "in": "query" }, { "type": "string", - "description": "HTTP method for GET form (default GET)", + "default": "GET", + "example": "GET", + "description": "HTTP method for the outbound call", "name": "method", "in": "query" }, { - "type": "string", - "default": "Bearer", - "description": "Bearer token", - "name": "Authorization", - "in": "header", - "required": true + "type": "number", + "default": 15, + "example": 15, + "description": "outbound timeout seconds", + "name": "timeoutSeconds", + "in": "query" } ], "responses": { @@ -226,15 +208,15 @@ "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: 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": "proxy request", @@ -244,26 +226,6 @@ "schema": { "$ref": "#/definitions/handlers.ProxyRequest" } - }, - { - "type": "string", - "description": "absolute url (GET form)", - "name": "url", - "in": "query" - }, - { - "type": "string", - "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": { @@ -323,7 +285,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" ], @@ -332,6 +294,8 @@ "parameters": [ { "type": "number", + "default": 2, + "example": 2, "description": "seconds to sleep", "name": "seconds", "in": "path", @@ -350,7 +314,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" ], @@ -359,6 +323,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", @@ -370,7 +346,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" ], @@ -379,6 +355,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", @@ -390,7 +378,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" ], @@ -399,6 +387,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", @@ -410,7 +410,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" ], @@ -419,6 +419,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", @@ -430,7 +442,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" ], @@ -439,6 +451,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", @@ -618,7 +642,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" ], @@ -627,6 +651,8 @@ "parameters": [ { "type": "integer", + "default": 418, + "example": 418, "description": "HTTP status code", "name": "code", "in": "path", @@ -670,23 +696,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" } } }, @@ -733,7 +765,9 @@ "$ref": "#/definitions/handlers.ProbeConfig" }, "resetStartupLatch": { - "type": "boolean" + "description": "example: true", + "type": "boolean", + "example": true }, "startup": { "$ref": "#/definitions/handlers.ProbeConfig" @@ -747,46 +781,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": "Paste: Bearer \u003ctoken\u003e (token from container logs, or AUTH_TOKEN env). Example: 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 58e80d0..1f6c9ce 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,48 +71,73 @@ 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 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: @@ -124,11 +163,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 @@ -158,16 +199,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: @@ -188,33 +223,29 @@ paths: summary: Get environment variables /a/proxy: 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. - 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: proxy request - in: body - name: body - required: true - schema: - $ref: '#/definitions/handlers.ProxyRequest' - - description: absolute url (GET form) + - default: https://httpbin.org/get + description: 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: HTTP method for the outbound call + example: GET in: query name: method type: string - - default: Bearer - description: Bearer token - in: header - name: Authorization - required: true - type: string + - default: 15 + description: outbound timeout seconds + example: 15 + in: query + name: timeoutSeconds + type: number produces: - application/json responses: @@ -242,14 +273,14 @@ paths: type: object security: - BearerAuth: [] - summary: Proxy / hop to another service (auth) + summary: Proxy GET hop (auth) 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. - 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: proxy request in: body @@ -257,20 +288,6 @@ paths: required: true schema: $ref: '#/definitions/handlers.ProxyRequest' - - description: absolute url (GET form) - in: query - name: url - type: string - - description: HTTP method for GET form (default GET) - in: query - name: method - type: string - - default: Bearer - description: Bearer token - in: header - name: Authorization - required: true - type: string produces: - application/json responses: @@ -298,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 @@ -315,11 +332,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 @@ -336,8 +355,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: @@ -350,8 +378,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: @@ -364,8 +401,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: @@ -378,8 +424,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: @@ -392,8 +447,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: @@ -519,11 +583,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 @@ -552,8 +618,8 @@ paths: summary: Version / build info securityDefinitions: BearerAuth: - description: 'Paste: Bearer (token from container logs, or AUTH_TOKEN - env). Example: 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/handlers.go b/handlers/handlers.go index 1f7153f..541b7f0 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()) } @@ -127,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) { @@ -143,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) { @@ -163,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 c144749..68bda16 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -38,79 +38,113 @@ 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. -// @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 true "proxy request" -// @Param url query string false "absolute url (GET form)" -// @Param method query string false "HTTP method for GET form (default GET)" // @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] -// @Param Authorization header string true "Bearer token" default(Bearer ) -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" { @@ -145,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) @@ -161,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) } @@ -172,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), @@ -192,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 @@ -213,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 d542613..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 Paste: Bearer (token from container logs, or AUTH_TOKEN env). Example: 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 {