{"openapi":"3.1.0","info":{"title":"Perfectscale API - OpenAPI 3.1","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"UnevictableReportRow":{"type":"object","description":"One row of the per-pod issues view — a single unevictable pod with all its reasons combined.","required":["name","id","workload","namespace","reasons","mute"],"properties":{"name":{"type":"string"},"id":{"type":"string"},"workload":{"$ref":"#/components/schemas/UnevictableWorkloadRef"},"namespace":{"type":"string"},"labels":{"type":"object","additionalProperties":{"type":"string"}},"node":{"type":["string","null"],"description":"Node the pod is scheduled on; null for Pending pods."},"nodeGroup":{"type":"string","description":"Node group the pod's node belongs to; empty if the node's node group is unresolved."},"reasons":{"type":"array","description":"All unevictable reasons for this pod.","items":{"$ref":"#/components/schemas/UnevictableReason"}},"mute":{"type":"boolean","description":"True when the pod's workload is muted by a dismissal rule, muting all of its reasons."},"priority":{"type":"integer","description":"Kubernetes pod scheduling priority (from the pod's PriorityClass); same value as UnevictablePod.spec.priority."},"blockedCostHourly":{"description":"Hourly cost of the blocked nodes. Filtering (`blockedCostHourly:gte:<amount>`) and sorting compare numerically on the underlying cost value, not the serialized `Money` object.\n","allOf":[{"$ref":"#/components/schemas/Money"}]}}},"UnevictableWorkloadRef":{"type":"object","description":"Identity of the pod's top-level owning workload.","required":["id","type"],"properties":{"id":{"type":"string","description":"Canonical workload id (namespace-kind-name) of the top-level controller."},"name":{"type":"string","description":"Denormalized workload name for display."},"type":{"type":"string","description":"Kubernetes workload kind (e.g. Deployment, StatefulSet, DaemonSet)."}}},"UnevictableReason":{"type":"object","required":["reason","details"],"properties":{"reason":{"type":"string","description":"Short reason code (e.g. \"Topology-Locked\", \"PDB Violation\")."},"reasonCode":{"oneOf":[{"$ref":"#/components/schemas/UnevictableReasonCode"},{"type":"null"}],"description":"Stable slug derived from the display reason. Null for unrecognised reasons."},"details":{"type":"string","description":"Human-readable explanation."},"remediation":{"$ref":"#/components/schemas/UnevictableRemediation"},"mute":{"type":"boolean","description":"True when this reason is covered by a dismissal rule and therefore muted."},"mutedByRule":{"oneOf":[{"$ref":"#/components/schemas/UnevictableMutedByRule"},{"type":"null"}]}}},"UnevictableReasonCode":{"type":"string","description":"Stable canonical identifier for an unevictable reason. Decoupled from the mutable display string.","enum":["opt_out_annotation","pod_disruption_budget","strict_affinity","strict_anti_affinity","topology_locked","taints_not_tolerated","local_node_storage","static_mirror_pod"]},"UnevictableRemediation":{"type":"object","description":"Reason-specific fix. current/recommended spec and the yaml diff are null for partial/informational cases.\n","required":["fixSummary","risk","confidence"],"properties":{"fixSummary":{"type":"string","description":"One-line plain-language fix."},"risk":{"type":"string","enum":["none","low","medium","high"]},"confidence":{"type":"string","enum":["low","medium","high"]},"currentSpec":{"type":["string","null"],"description":"Rendered YAML of the current spec; null for partial/informational cases."},"recommendedSpec":{"type":["string","null"],"description":"Rendered YAML of the recommended spec; null for partial/informational cases."},"yamlDiff":{"type":["string","null"],"description":"Unified diff string; null when there is no clean diff."}}},"UnevictableMutedByRule":{"type":"object","description":"Summary of the dismissal rule that suppressed this finding.","required":["createdBy","createTime"],"properties":{"note":{"type":["string","null"]},"createdBy":{"type":"string","description":"Email of the user who created the rule."},"createTime":{"type":"string","format":"date-time","description":"When the rule was created (RFC 3339 UTC)"}}},"Money":{"type":"object","description":"Monetary amount with currency","properties":{"amount":{"type":"string","description":"Decimal string representation of the amount (e.g. \"100.50\")"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. \"USD\")"}},"required":["amount","currency"]},"PaginationMeta":{"type":"object","description":"Cursor-based pagination metadata for list responses.","properties":{"next":{"type":["string","null"],"description":"Opaque cursor token for the next page, or null if this is the last page"},"prev":{"type":["string","null"],"description":"Opaque cursor token for the previous page, or null if this is the first page"},"pageSize":{"type":"integer","description":"Number of items per page used for this response"}},"required":["next","prev","pageSize"]},"UnevictableSummary":{"type":"object","description":"Aggregate pod/node counts for the snapshot backing the listing response.","required":["totalPods","unevictablePods","mute","totalNodes"],"properties":{"totalPods":{"type":"integer","description":"Total number of pods observed in the snapshot."},"unevictablePods":{"type":"integer","description":"Number of pods detected as unevictable in the snapshot."},"mute":{"type":"integer","description":"Number of unevictable findings muted by a dismissal rule in this snapshot."},"totalNodes":{"type":"integer","description":"Total number of nodes observed in the snapshot."},"autoscalerType":{"type":"string","description":"Detected autoscaler, rolled up from per-node detection (canonical\nvalues, e.g. \"cluster_autoscaler\", \"karpenter\", \"castai\",\n\"eks_auto_mode\"). Omitted if not detected. Results written before\nPSD-9016 may carry the legacy hyphenated \"cluster-autoscaler\"."}}},"UnevictablePodsStatus":{"type":"object","description":"Snapshot processing status, returned on 202 (still processing) and 422 (processing failed) instead of pod/report data.\n","properties":{"status":{"type":"string","enum":["processing","failed"]}},"required":["status"]},"Problem":{"type":"object","description":"RFC 9457 Problem Details error body, served as `application/problem+json`. `code` is a stable machine-readable identifier (e.g. `filter_key_not_supported`, `not_found`, `internal_error`); `retryable` indicates whether retrying the same request may succeed without client-side changes.\n","properties":{"type":{"type":"string","description":"URI reference identifying the problem type."},"title":{"type":"string","description":"Short, human-readable summary of the problem type."},"status":{"type":"integer","description":"HTTP status code generated by the origin server for this occurrence of the problem."},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence of the problem."},"instance":{"type":"string","description":"URI reference identifying the specific occurrence of the problem."},"code":{"type":"string","description":"Stable machine-readable error code."},"retryable":{"type":"boolean","description":"Whether retrying the same request may succeed without client-side changes."}},"required":["type","title","status","code","retryable"]}}},"paths":{"/clusters/{clusterUid}/unevictable-report":{"get":{"operationId":"GetUnevictableReport","tags":["Unevictable"],"summary":"Per-pod unevictable report — one row per pod, with all its reasons combined","description":"Issues view — one row per pod, carrying all of that pod's unevictable reasons. Served from the latest pre-computed snapshot (no request-time recompute). Tenant resolved from the auth token; a cluster belonging to another tenant returns 404.\n","parameters":[{"name":"clusterUid","in":"path","required":true,"schema":{"type":"string"},"description":"Cluster unique identifier"},{"name":"filter","in":"query","required":false,"schema":{"type":"string"},"description":"Composite filter expression: clauses of the form `key[:op]:value` joined by `|` (AND); `op` is optional and defaults to `eq`; comma-separate values within a clause for an IN match. Supported keys for this endpoint: `namespace` (eq), `blockedCostHourly` (eq/gte/lte, compares numerically on the underlying cost value, not the serialized `Money` object), `nodeGroup` (eq), `labels.<key>` (eq). `labels.<key>` matches against the pod's Kubernetes labels — for example `filter=labels.team:payments` matches pods labeled `team=payments`. An unsupported key returns 400 with `code: \"filter_key_not_supported\"`. Mute-status filtering is handled by the separate `mute` parameter, not this filter.\n"},{"name":"mute","in":"query","required":false,"schema":{"type":"string","enum":["exclude","include","only"],"default":"exclude"},"description":"Muted-finding handling: `exclude` (default) hides rows whose workload has a dismissal rule; `include` returns all rows, flagged via `mute`; `only` returns exclusively muted rows, for auditing what's been silenced.\n"},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","enum":["blockedCostHourly"]},"description":"Field to sort results by. `blockedCostHourly` sorts numerically on the underlying cost value, not the serialized `Money` object. Unset falls back to a stable but unspecified order (currently pod UID). Ties are always broken by UID for stable pagination.\n"},{"name":"sortOrder","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"Sort direction, applied only when sortBy is set."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":500},"description":"Number of items per page (default 50, max 500)"},{"name":"pageToken","in":"query","schema":{"type":"string"},"description":"Opaque cursor token for pagination"}],"responses":{"200":{"description":"List of unevictable pod issue rows","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UnevictableReportRow"}},"meta":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/PaginationMeta"},"snapshotTime":{"type":"string","format":"date-time","description":"Timestamp (RFC 3339, UTC) of the snapshot the data was read from"},"algorithmVersion":{"type":"string","description":"Version of the unevictable-detection algorithm/library used to produce the snapshot"},"summary":{"$ref":"#/components/schemas/UnevictableSummary"}},"required":["pagination","snapshotTime","algorithmVersion","summary"]}},"required":["data","meta"]}}}},"202":{"description":"Raw snapshot exists but is still being processed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UnevictablePodsStatus"}},"required":["data"]}}}},"400":{"description":"Invalid parameters","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Cluster or snapshot not found","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Raw snapshot exists but processing failed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal server error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}