{
  "openapi": "3.0.3",
  "info": {
    "title": "drydock API",
    "version": "1.0.0",
    "description": "The JSON contract between the drydock server and its dashboard.\n\nThe contract between the Gleam server (`apps/server`) and the dashboard (`apps/dashboard`). This document is the contract: the dashboard's TypeScript types are generated from it, and CI validates responses captured from the real server against it.\n\nTwo conventions are fixed and expensive to retrofit. The path is versioned, because the two tiers do not deploy together and a dashboard may lag the cluster it points at. And raw Kubernetes objects are not passed through — they are large, they carry fields users have no business seeing, and shipping them would make the dashboard depend on the Kubernetes schema.\n\nField names are snake_case unless the value *is* a Kubernetes field, in which case it keeps the spelling Kubernetes gives it, so that nobody has to learn a second name for something `kubectl` also prints.\n\nA request to a path this document does not list is answered 404 with the code `unknown_endpoint`, which is distinct from the `not_found` a missing resource gets.",
    "license": {
      "name": "Apache-2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0"
    }
  },
  "paths": {
    "/api/v1/access/grants": {
      "get": {
        "operationId": "listGrants",
        "summary": "Every AccessGrant, read as the calling user",
        "description": "Read as the caller, so this returns what that user is allowed to see. Not licence-gated: no read is.",
        "tags": [
          "access"
        ],
        "responses": {
          "200": {
            "description": "The grants.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessGrantList"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      },
      "post": {
        "operationId": "createGrant",
        "summary": "Grant a role to subjects, cluster-wide or in named namespaces",
        "description": "Sent with the caller's own token, so Kubernetes' escalation prevention applies: a 403 here is the apiserver refusing to let this user bind a role they could not bind themselves. That is an answer about the user, not a fault. The role must be one of `AccessGrantList.roles`, granted cluster-wide; anything else answers 422 before the apiserver is asked.",
        "tags": [
          "access"
        ],
        "requestBody": {
          "required": true,
          "description": "The grant to create.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GrantRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created grant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessGrant"
                }
              }
            },
            "headers": {
              "location": {
                "description": "Where the created object can now be read, as a path under this API.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/access/grants/{name}": {
      "patch": {
        "operationId": "patchGrant",
        "summary": "Change a grant's subjects, role or scope, or suspend it",
        "description": "Sent with the caller's own token, with the same escalation rules as creation. A body carrying only `suspended` suspends or resumes without touching the rest.",
        "tags": [
          "access"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GrantPatch"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Applied. No body, as with every other write here.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      },
      "delete": {
        "operationId": "deleteGrant",
        "summary": "Revoke a grant",
        "description": "Deletes the AccessGrant; the controller removes the bindings it created.",
        "tags": [
          "access"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/backups": {
      "get": {
        "operationId": "getBackups",
        "summary": "The backups drydock has taken",
        "description": "The Backup objects in drydock's namespace, read as the caller, with each one attributed to the node — and the upgrade, for one taken before an upgrade — it came from where the name allows it. The orchestrator keeps these objects matching the bucket; this endpoint does not open the bucket.",
        "tags": [
          "backups"
        ],
        "responses": {
          "200": {
            "description": "The backups. `target` is null when none is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Backups"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    },
    "/api/v1/backups/config": {
      "get": {
        "operationId": "getBackupConfiguration",
        "summary": "How backups are configured",
        "description": "The BackupConfig drydock backs up with, read as the caller, and what it last did with it. Read-only: the configuration is edited on the BackupConfig itself, with kubectl or GitOps, never here.",
        "tags": [
          "backups"
        ],
        "responses": {
          "200": {
            "description": "The configuration. `config` is null when none is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackupConfiguration"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "summary": "This document",
        "description": "The OpenAPI document the server was built against, byte for byte. Read it from the server rather than from a copy: a dashboard may lag the cluster it points at, and this is the API that cluster actually speaks. Unauthenticated: it describes the API, not the caller.",
        "tags": [
          "contract"
        ],
        "responses": {
          "200": {
            "description": "This document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An OpenAPI 3.0.3 document: this one."
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          }
        },
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    },
    "/api/v1/auth/providers": {
      "get": {
        "operationId": "getAuthProviders",
        "summary": "Which sign-in methods this install offers",
        "description": "Unauthenticated, because the sign-in page needs it before there is a token. Only providers that are usable are listed.",
        "tags": [
          "auth"
        ],
        "responses": {
          "200": {
            "description": "The offered methods.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthProviders"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    },
    "/api/v1/auth/oidc/token": {
      "post": {
        "operationId": "exchangeOidcToken",
        "summary": "Redeem a code or a refresh token",
        "description": "Performed by the server, not the browser, so that the client secret never leaves the cluster.",
        "tags": [
          "auth"
        ],
        "requestBody": {
          "required": true,
          "description": "Which provider, and which grant.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The issuer's tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenAnswer"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ExchangeRefused"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          }
        },
        "security": []
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "operationId": "login",
        "summary": "Sign in as a local user",
        "description": "One answer for every reason a sign-in fails, and a password derivation is burned even for a user that does not exist, so neither the body nor the response time says whether the account is real.",
        "tags": [
          "auth"
        ],
        "requestBody": {
          "required": true,
          "description": "The credentials.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A minted session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginAnswer"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          }
        },
        "security": []
      }
    },
    "/api/v1/auth/logout": {
      "post": {
        "operationId": "logout",
        "summary": "End the session",
        "description": "Answers 204 and holds no server-side state: a session token is self-contained and expires on its own, so this exists for the client to have something to call.",
        "tags": [
          "auth"
        ],
        "responses": {
          "204": {
            "description": "Signed out.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/identity/users": {
      "get": {
        "operationId": "listLocalUsers",
        "summary": "Every local user",
        "description": "Reads the LocalUser objects. No verifier or secret contents are ever included.",
        "tags": [
          "identity"
        ],
        "responses": {
          "200": {
            "description": "The users.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalUserList"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      },
      "post": {
        "operationId": "createLocalUser",
        "summary": "Create a local user",
        "description": "Needs somewhere to put a password verifier, which is a deployment fact — hence the 501 when local sign-in is not configured.",
        "tags": [
          "identity"
        ],
        "requestBody": {
          "required": true,
          "description": "The user to create.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LocalUserRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalUser"
                }
              }
            },
            "headers": {
              "location": {
                "description": "Where the created object can now be read, as a path under this API.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/identity/users/{name}": {
      "patch": {
        "operationId": "patchLocalUser",
        "summary": "Change a local user, or just switch it off, or just set a password",
        "description": "The path name wins over any `name` in the body.",
        "tags": [
          "identity"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "One of three shapes; see the schema.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LocalUserPatch"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Updated. No body.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      },
      "delete": {
        "operationId": "deleteLocalUser",
        "summary": "Delete a local user",
        "description": "Removes the object and the Secret holding its verifier.",
        "tags": [
          "identity"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/identity/users/{name}/tokens": {
      "get": {
        "operationId": "listApiTokens",
        "summary": "A user's API tokens",
        "description": "The API tokens that act as this user, read as the caller: identity-admins may read them. Never the secret — that is shown once, when the token is created.",
        "tags": [
          "identity"
        ],
        "responses": {
          "200": {
            "description": "The tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiTokenList"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The LocalUser's metadata.name: the user the tokens act as.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "createApiToken",
        "summary": "Create an API token",
        "description": "A token a script sends as its bearer token, acting as this user: it can do exactly what the user can, and stops when the user is disabled or deleted. The caller must be allowed to create apitokens and to assign every group the user holds — the same rule as creating the user.",
        "tags": [
          "identity"
        ],
        "requestBody": {
          "required": true,
          "description": "The token to create.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiTokenRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created token, with its secret. The secret is never shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiTokenCreated"
                }
              }
            },
            "headers": {
              "location": {
                "description": "Where the created object can now be read, as a path under this API.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The LocalUser's metadata.name: the user the tokens act as.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/identity/users/{name}/tokens/{id}": {
      "delete": {
        "operationId": "deleteApiToken",
        "summary": "Revoke an API token",
        "description": "Deletes the token. A request already holding it is refused from the next one on.",
        "tags": [
          "identity"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The LocalUser's metadata.name: the user the tokens act as.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The token's id, the part of the token before the dot after `ddk_`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/identity/providers": {
      "get": {
        "operationId": "listIdentityProviders",
        "summary": "Every configured identity provider",
        "description": "The authenticated view, which carries more than the sign-in page's: the claim names, and whether a client secret is configured.",
        "tags": [
          "identity"
        ],
        "responses": {
          "200": {
            "description": "The providers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdentityProviderList"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      },
      "post": {
        "operationId": "createIdentityProvider",
        "summary": "Configure an identity provider",
        "description": "Writes the client secret into a Secret first and then the object referencing it, so a failure part-way leaves a secret nothing points at rather than an object pointing at nothing.",
        "tags": [
          "identity"
        ],
        "requestBody": {
          "required": true,
          "description": "The provider to create.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdentityProviderRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdentityProvider"
                }
              }
            },
            "headers": {
              "location": {
                "description": "Where the created object can now be read, as a path under this API.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/identity/providers/{name}": {
      "patch": {
        "operationId": "patchIdentityProvider",
        "summary": "Change a provider, or just switch it off",
        "description": "A blank `clientSecret` keeps the existing reference rather than clearing it; `publicClient` is how a reference is removed.",
        "tags": [
          "identity"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "One of two shapes; see the schema.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdentityProviderPatch"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Updated. No body.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      },
      "delete": {
        "operationId": "deleteIdentityProvider",
        "summary": "Remove a provider",
        "description": "Deletes the object and any Secret it owned.",
        "tags": [
          "identity"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/inventory": {
      "get": {
        "operationId": "getInventory",
        "summary": "Every node, as the dashboard's table needs it",
        "description": "One row per node: identity, OS, readiness, what drydock can do to it, and what would block draining it.",
        "tags": [
          "inventory"
        ],
        "responses": {
          "200": {
            "description": "The cluster's nodes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Inventory"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    },
    "/api/v1/license": {
      "get": {
        "operationId": "getLicense",
        "summary": "Licence state, limits and current usage",
        "description": "Signed-in callers only. A drydock session or an identity-provider token is verified here; any other bearer token is answered once the apiserver has authenticated it with a SelfSubjectReview, because no other request on this route would test it. Authentication only: no RBAC is needed to read the licence. Answered from the server's own state otherwise. Every field is present in every state, with the claim-derived ones null when there is no licence.",
        "tags": [
          "license"
        ],
        "responses": {
          "200": {
            "description": "The licence state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/License"
                },
                "examples": {
                  "licensed": {
                    "summary": "Active, and within its limits",
                    "value": {
                      "status": "active",
                      "allows_writes": true,
                      "detail": null,
                      "id": "lic_0WM0ik5Db6cZYoYk",
                      "expires_at": 1793315320,
                      "grace_ends_at": 1794524920,
                      "days_left": 90,
                      "cluster_id": null,
                      "data": {
                        "customer": "asergo",
                        "tier": "enterprise"
                      },
                      "limits": {
                        "max_worker_nodes": 10,
                        "max_control_plane_nodes": 3
                      },
                      "usage": {
                        "worker_nodes": 6,
                        "control_plane_nodes": 3,
                        "observed_at": 1793314000
                      },
                      "compliant": true
                    }
                  },
                  "over_limit": {
                    "summary": "Active, but the cluster exceeds a licensed limit",
                    "description": "`compliant` is false while `status` is still active. The two are separate questions: the key is valid, and the cluster is too big for it.",
                    "value": {
                      "status": "active",
                      "allows_writes": true,
                      "detail": null,
                      "id": "lic_0WM0ik5Db6cZYoYk",
                      "expires_at": 1793315320,
                      "grace_ends_at": 1794524920,
                      "days_left": 90,
                      "cluster_id": "8f3c1d2e-1a4b-4c9d-9e77-2b1f0a6c5d31",
                      "data": {
                        "customer": "asergo"
                      },
                      "limits": {
                        "max_worker_nodes": 10,
                        "max_control_plane_nodes": 3
                      },
                      "usage": {
                        "worker_nodes": 14,
                        "control_plane_nodes": 3,
                        "observed_at": 1793314000
                      },
                      "compliant": false
                    }
                  },
                  "unlicensed": {
                    "summary": "No key configured — every claim-derived field is null",
                    "value": {
                      "status": "unlicensed",
                      "allows_writes": false,
                      "detail": "no license key is configured",
                      "id": null,
                      "expires_at": null,
                      "grace_ends_at": null,
                      "days_left": null,
                      "cluster_id": null,
                      "data": {},
                      "limits": {
                        "max_worker_nodes": null,
                        "max_control_plane_nodes": null
                      },
                      "usage": null,
                      "compliant": null
                    }
                  }
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    },
    "/api/v1/risk": {
      "get": {
        "operationId": "getRisk",
        "summary": "Whether losing a node would displace workloads",
        "description": "A per-node assessment of what a drain would move, what it could not move, and whether the remaining nodes have room for it.",
        "tags": [
          "risk"
        ],
        "responses": {
          "200": {
            "description": "The risk assessment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RiskAssessment"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    },
    "/api/v1/upgrades": {
      "get": {
        "operationId": "listUpgrades",
        "summary": "Every ClusterUpgrade, and whether one is executing",
        "description": "Supports conditional requests: the response carries an `etag`, and a matching `if-none-match` is answered 304 with no body. `poll_after_seconds` is the reconcile interval, so a client need not guess a polling rate.",
        "tags": [
          "upgrades"
        ],
        "responses": {
          "200": {
            "description": "The upgrades.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpgradeList"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      },
      "post": {
        "operationId": "createUpgrade",
        "summary": "Start an OS upgrade across named nodes",
        "description": "Creates a ClusterUpgrade. The nodes are named explicitly; there is no selector.",
        "tags": [
          "upgrades"
        ],
        "requestBody": {
          "required": true,
          "description": "The upgrade to create.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpgradeRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created upgrade.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClusterUpgrade"
                }
              }
            },
            "headers": {
              "location": {
                "description": "Where the created object can now be read, as a path under this API.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/upgrades/{name}": {
      "patch": {
        "operationId": "patchUpgrade",
        "summary": "Pause or resume an upgrade",
        "description": "The only mutable field is `paused`. Pausing stops the reconciler before the next step; it does not interrupt a step already running.",
        "tags": [
          "upgrades"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpgradePatch"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Applied. No body, as with every other write here.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MalformedBody"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      },
      "delete": {
        "operationId": "deleteUpgrade",
        "summary": "Cancel an upgrade",
        "description": "Deletes the ClusterUpgrade. Work already done to a node is not reversed.",
        "tags": [
          "upgrades"
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The object's metadata.name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/uptime": {
      "get": {
        "operationId": "getUptime",
        "summary": "Per-node availability over the retained window",
        "description": "Daily rollups and incidents, attributed to a drydock upgrade where one explains the downtime. With `period`, one month; without it, the whole retained record. Incidents are built from the outages each month keeps for its whole life, so they last as long as the record does.",
        "tags": [
          "uptime"
        ],
        "responses": {
          "200": {
            "description": "The uptime record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Uptime"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "A UTC month, `YYYY-MM`. The answer then covers that month only: its days, and the incidents from its outages. Absent, the answer covers the whole retained record, as before.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
            }
          }
        ]
      }
    },
    "/api/v1/uptime/months": {
      "get": {
        "operationId": "getUptimeMonths",
        "summary": "Availability per month over the retained record",
        "description": "One entry per UTC month the record holds: observed and unobserved time, downtime split by cause, and the count of unplanned outages and flap episodes. Small enough to draw thirteen months at once, which is what a month selector needs before it fetches one.",
        "tags": [
          "uptime"
        ],
        "responses": {
          "200": {
            "description": "Availability per month.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UptimeMonths"
                }
              }
            },
            "headers": {
              "cache-control": {
                "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "vary": {
                "description": "`authorization`. What the representation is selected by.",
                "schema": {
                  "type": "string"
                },
                "required": true
              },
              "etag": {
                "description": "An opaque strong validator for this representation. Send it back as `if-none-match`. Derived from the bytes, except on the upgrades list, where the collection's `resourceVersion` lets the 304 be decided before the body is built.",
                "schema": {
                  "type": "string"
                },
                "required": true
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "500": {
            "$ref": "#/components/responses/Crashed"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/IdentityUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "AccessGrant": {
        "type": "object",
        "required": [
          "name",
          "suspended",
          "role",
          "subjects",
          "scope",
          "reconciled",
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "The object name. Lowercase letters, digits, '-' and '.' — note this is NOT the group name, which may contain characters an object name cannot."
          },
          "suspended": {
            "type": "boolean",
            "description": "Reconciliation is stopped. Break-glass, and NOT revocation: existing bindings are left exactly where they are and simply stop being repaired or pruned. A UI that renders this as 'revoked' or 'disabled' is telling the operator the access is gone when it is not. Revoking is deleting the grant. Grants seeded at startup all begin suspended, so this is the normal state of a fresh install rather than an exceptional one."
          },
          "role": {
            "$ref": "#/components/schemas/GrantRoleRef"
          },
          "subjects": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/GrantSubject"
            },
            "description": "Never empty. The minimum is enforced by the CustomResourceDefinition, so the apiserver refuses an object that would violate it; the server does not re-check it on the way out."
          },
          "scope": {
            "$ref": "#/components/schemas/GrantScope"
          },
          "reconciled": {
            "type": "boolean",
            "description": "Whether the controller has fully reconciled THIS version of the spec (status.observedGeneration == metadata.generation). It is what tells 'not acted on yet' apart from 'acted on, and the result is what status says'. A grant with an empty status and reconciled=false has not been reached; the same grant with reconciled=true has been reached and produced nothing, which is a different and much more interesting fact."
          },
          "status": {
            "$ref": "#/components/schemas/GrantStatus"
          }
        }
      },
      "AccessGrantList": {
        "description": "AccessGrant objects, read as the calling user. A grant is one subject list, one role and one scope; the server reconciles it into the Cluster/RoleBindings that implement it. Writes to this collection are the permission to GRANT ACCESS: every write is sent with the caller's own token, so Kubernetes' escalation prevention applies and a 403 here is the apiserver refusing to let this user bind a role they could not bind themselves. That is a real answer about the signed-in user, not a failure. There is no rule authoring anywhere in this contract — a grant names a role that must already exist.",
        "type": "object",
        "required": [
          "grants",
          "roles"
        ],
        "additionalProperties": false,
        "properties": {
          "grants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccessGrant"
            }
          },
          "roles": {
            "type": "array",
            "description": "The ClusterRoles a grant may name, cluster-wide: drydock's viewer, upgrade-requester, identity-admin and drydock-admin, as this install names them. Any other role, and any namespace scope, is refused at creation and reported Denied on a grant that already exists.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ApiToken": {
        "type": "object",
        "description": "An API token, without its secret.",
        "required": [
          "id",
          "user",
          "description",
          "createdAt",
          "expiresAt"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "description": "Names the token; not secret. The part of the token between `ddk_` and the dot."
          },
          "user": {
            "type": "string",
            "description": "The LocalUser it acts as."
          },
          "description": {
            "type": "string",
            "description": "What it is for, as its creator wrote it."
          },
          "createdAt": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "expiresAt": {
            "type": "integer",
            "description": "Unix seconds. Refused from then on."
          }
        }
      },
      "ApiTokenCreated": {
        "type": "object",
        "required": [
          "token",
          "apiToken"
        ],
        "additionalProperties": false,
        "properties": {
          "token": {
            "type": "string",
            "description": "The bearer token, `ddk_<id>.<secret>`. Shown once: only a hash of it is kept."
          },
          "apiToken": {
            "$ref": "#/components/schemas/ApiToken"
          }
        }
      },
      "ApiTokenList": {
        "type": "object",
        "required": [
          "tokens"
        ],
        "additionalProperties": false,
        "properties": {
          "tokens": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiToken"
            }
          }
        }
      },
      "ApiTokenRequest": {
        "type": "object",
        "description": "A token to create.",
        "required": [
          "description",
          "expiresInDays"
        ],
        "additionalProperties": false,
        "properties": {
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "What it is for: the script or job that holds it."
          },
          "expiresInDays": {
            "type": "integer",
            "minimum": 1,
            "maximum": 365,
            "description": "How long it lasts. The install's maximum (identity.apiTokens.maxLifetimeDays, 90 by default) is enforced too, with a 422 above it."
          }
        }
      },
      "AuthProviders": {
        "type": "object",
        "description": "What sign-in methods this install offers. Unauthenticated: the page needs it before there is a token.",
        "required": [
          "providers",
          "localUsers"
        ],
        "additionalProperties": false,
        "properties": {
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicProvider"
            }
          },
          "localUsers": {
            "type": "boolean",
            "description": "Whether this install has local user sign-in configured at all."
          }
        }
      },
      "AuthorizationCodeGrant": {
        "type": "object",
        "description": "Redeem an authorization code. PKCE is not optional here.",
        "required": [
          "provider",
          "grantType",
          "code",
          "codeVerifier",
          "redirectUri"
        ],
        "additionalProperties": false,
        "properties": {
          "provider": {
            "type": "string",
            "description": "Which configured provider to redeem against."
          },
          "grantType": {
            "type": "string",
            "enum": [
              "authorization_code"
            ]
          },
          "code": {
            "type": "string"
          },
          "codeVerifier": {
            "type": "string"
          },
          "redirectUri": {
            "type": "string"
          }
        }
      },
      "BackupConfig": {
        "type": "object",
        "required": [
          "name",
          "namespace",
          "endpoint",
          "bucket",
          "region",
          "scheme",
          "port",
          "private_ca",
          "secret_name",
          "folder",
          "type",
          "interval",
          "ttl",
          "upload",
          "restore_test",
          "last_upload",
          "last_restore_test",
          "last_passed_restore_test_at"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "description": "The BackupConfig's name. drydock reads the one called default.",
            "type": "string",
            "example": "default"
          },
          "namespace": {
            "type": "string",
            "example": "drydock-system"
          },
          "endpoint": {
            "description": "The store's host.",
            "type": "string",
            "example": "s3.example.com"
          },
          "bucket": {
            "type": "string",
            "example": "cluster-backups"
          },
          "region": {
            "type": "string",
            "example": "us-east-1"
          },
          "scheme": {
            "type": "string",
            "enum": [
              "http",
              "https"
            ]
          },
          "port": {
            "description": "Absent on the BackupConfig means the scheme's default.",
            "type": "integer",
            "nullable": true
          },
          "private_ca": {
            "description": "True when the BackupConfig carries a caBundle to verify the endpoint against, rather than the system trust store.",
            "type": "boolean"
          },
          "secret_name": {
            "description": "The Secret holding the upload credential. Named, never read for this answer.",
            "type": "string",
            "example": "drydock-backup-credentials"
          },
          "folder": {
            "description": "Where this cluster's backups live in the bucket: drydock/backup/<cluster-id>/. Not configurable — the cluster ID is the kube-system namespace's UID.",
            "type": "string",
            "example": "drydock/backup/9f2c1e4a-5b7d-4c3e-8a1f-2d6b0e9c7a53/"
          },
          "type": {
            "description": "What is backed up.",
            "type": "string",
            "enum": [
              "etcd"
            ]
          },
          "interval": {
            "description": "How often a backup is taken, as the BackupConfig spells it.",
            "type": "string",
            "example": "1d"
          },
          "ttl": {
            "description": "How long a backup is kept. The newest backup is never deleted, whatever its age.",
            "type": "string",
            "example": "10d"
          },
          "upload": {
            "$ref": "#/components/schemas/BackupUploadPolicy"
          },
          "restore_test": {
            "$ref": "#/components/schemas/BackupRestoreTestPolicy"
          },
          "last_upload": {
            "description": "The last attempt to put a backup in the bucket, successful or not. Null when none has been made.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/BackupLastUpload"
              }
            ]
          },
          "last_restore_test": {
            "description": "The newest restore test, passed or failed. Null when none has run.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/BackupLastRestoreTest"
              }
            ]
          },
          "last_passed_restore_test_at": {
            "description": "Unix seconds when the newest PASSED restore test started. Null when none has passed — which is the state an auditor asks about.",
            "type": "integer",
            "nullable": true
          }
        }
      },
      "BackupConfiguration": {
        "type": "object",
        "description": "The BackupConfig drydock backs up with, as the caller may read it. Nothing secret crosses: the Secret is named, never read for this answer.",
        "required": [
          "config"
        ],
        "additionalProperties": false,
        "properties": {
          "config": {
            "description": "The configuration, or null if drydock has nowhere to back up to — backups switched off in the chart. Null is an ANSWER and must render as 'backups are not configured', never as an error.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/BackupConfig"
              }
            ]
          }
        }
      },
      "BackupLastRestoreTest": {
        "type": "object",
        "required": [
          "snapshot_key",
          "at",
          "outcome",
          "rung",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "snapshot_key": {
            "description": "The backup that was tested.",
            "type": "string"
          },
          "at": {
            "description": "Unix seconds when the test started.",
            "type": "integer"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "passed",
              "failed"
            ]
          },
          "rung": {
            "description": "How far the test got: fetched and its ETag checked, status read by etcdutl, restored, then served by a throwaway etcd. none when it failed before the download finished.",
            "type": "string",
            "enum": [
              "none",
              "fetched",
              "status",
              "restored",
              "served"
            ]
          },
          "reason": {
            "description": "Why it failed. Empty otherwise.",
            "type": "string"
          }
        }
      },
      "BackupLastUpload": {
        "type": "object",
        "required": [
          "key",
          "at",
          "outcome",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "key": {
            "type": "string"
          },
          "at": {
            "description": "Unix seconds.",
            "type": "integer"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "succeeded",
              "failed"
            ]
          },
          "reason": {
            "description": "Why it failed. Empty otherwise.",
            "type": "string"
          }
        }
      },
      "BackupObject": {
        "type": "object",
        "required": [
          "name",
          "key",
          "etag",
          "phase",
          "reason",
          "started_at",
          "backed_up_at",
          "ttl",
          "expires_at",
          "last_restore",
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "description": "The Backup object's name.",
            "type": "string",
            "example": "etcd-cp-1-20260928t120000z"
          },
          "key": {
            "description": "The object key in the bucket: drydock/backup/<cluster-id>/<type>/<name>-<time>.",
            "type": "string",
            "example": "drydock/backup/9f2c1e4a-5b7d-4c3e-8a1f-2d6b0e9c7a53/etcd/up-1-cp-1-20260928T120000Z"
          },
          "etag": {
            "description": "As the store returned it when the backup was verified. Empty while the backup is in progress.",
            "type": "string"
          },
          "phase": {
            "description": "InProgress from the moment the backup begins; Completed once it is in the bucket with its ETag verified; Failed with a reason otherwise. A failed backup is kept, as a record of the failure, until its ttl.",
            "type": "string",
            "enum": [
              "InProgress",
              "Completed",
              "Failed"
            ]
          },
          "reason": {
            "description": "Why it failed. Empty otherwise.",
            "type": "string"
          },
          "started_at": {
            "description": "Unix seconds when the backup began. Null when not recorded.",
            "type": "integer",
            "nullable": true
          },
          "backed_up_at": {
            "description": "Unix seconds when it was taken, for a completed backup. Null otherwise.",
            "type": "integer",
            "nullable": true,
            "example": 1756300000
          },
          "ttl": {
            "description": "The BackupConfig's ttl this backup is kept for.",
            "type": "string",
            "example": "10d"
          },
          "expires_at": {
            "description": "Unix seconds when the ttl deletes it — unless it is the newest backup, which is never deleted. Null when not known.",
            "type": "integer",
            "nullable": true
          },
          "last_restore": {
            "description": "The newest restore test of this backup. Null when it has never been tested.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/BackupRestore"
              }
            ]
          },
          "status": {
            "description": "How far the server got in working out what this object is. THREE values, never two. Collapsing 'I cannot tell' into either confident case is the failure this field exists to prevent — the same argument uptime.v1 makes for its 'unknown' attribution.",
            "type": "string",
            "enum": [
              "attributed",
              "recognised",
              "unrecognised"
            ]
          },
          "cluster_upgrade": {
            "description": "Present only when status is 'attributed' and the backup was taken before a control-plane upgrade; a scheduled backup names only its node and has none. The last segment of the key is <name>-<time>, where the name is either a node or <upgrade>-<node>, and BOTH names may contain hyphens, so the encoding is genuinely ambiguous and cannot be parsed — it is resolved by looking for the one reading that matches what the cluster has: the whole name as a node, or a split into a ClusterUpgrade and a node. Where more than one reading matches, or none does, the status is 'recognised' and this field is absent rather than guessed.",
            "type": "string",
            "example": "up-1"
          },
          "node": {
            "description": "Present only when status is 'attributed'. See cluster_upgrade for why it can be missing on an object that is unmistakably drydock's.",
            "type": "string",
            "example": "cp-1"
          }
        }
      },
      "BackupRestore": {
        "type": "object",
        "required": [
          "at",
          "result",
          "rung",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "at": {
            "description": "Unix seconds when the test started.",
            "type": "integer"
          },
          "result": {
            "description": "Proven: restored into a throwaway etcd, which started and served this cluster's data. Failed otherwise.",
            "type": "string",
            "enum": [
              "Proven",
              "Failed"
            ]
          },
          "rung": {
            "description": "How far the test got.",
            "type": "string",
            "enum": [
              "none",
              "fetched",
              "status",
              "restored",
              "served"
            ]
          },
          "reason": {
            "description": "Why it failed. Empty otherwise.",
            "type": "string"
          }
        }
      },
      "BackupRestoreTestPolicy": {
        "type": "object",
        "description": "spec.restoreTest, with the CRD's defaults filled in.",
        "required": [
          "enabled",
          "interval",
          "on_backup",
          "deadline_seconds",
          "requested"
        ],
        "additionalProperties": false,
        "properties": {
          "enabled": {
            "description": "Off, the orchestrator only plans and logs what it would test.",
            "type": "boolean"
          },
          "interval": {
            "description": "A passed restore test is kept at most this old; one is due at once when none has passed.",
            "type": "string",
            "example": "90d"
          },
          "on_backup": {
            "description": "Every new backup is tested once, within minutes of landing.",
            "type": "boolean"
          },
          "deadline_seconds": {
            "type": "integer",
            "example": 1800
          },
          "requested": {
            "description": "The drydock.asergo.com/restore-test annotation's value — 'latest' or a key — while a requested test has not yet run. Null otherwise.",
            "type": "string",
            "nullable": true
          }
        }
      },
      "BackupTarget": {
        "type": "object",
        "required": [
          "endpoint",
          "bucket",
          "key_prefix"
        ],
        "additionalProperties": false,
        "properties": {
          "endpoint": {
            "description": "The store's host, as the BackupTarget spells it. Shown so an operator reading an unexpected list can tell which bucket they are looking at before concluding anything about it.",
            "type": "string",
            "example": "s3.example.com"
          },
          "bucket": {
            "type": "string",
            "example": "cluster-backups"
          },
          "key_prefix": {
            "description": "The folder under drydock/ this cluster's backups live in: backup/<cluster-id>, the cluster ID being the kube-system namespace's UID. Not configurable.",
            "type": "string",
            "example": "backup/9f2c1e4a-5b7d-4c3e-8a1f-2d6b0e9c7a53"
          }
        }
      },
      "BackupUploadPolicy": {
        "type": "object",
        "description": "spec.upload, with the CRD's defaults filled in.",
        "required": [
          "sweep_interval_seconds",
          "max_bytes",
          "attempts",
          "ship_timeout_seconds"
        ],
        "additionalProperties": false,
        "properties": {
          "sweep_interval_seconds": {
            "type": "integer",
            "example": 300
          },
          "max_bytes": {
            "description": "Above this a file is refused and kept, never uploaded.",
            "type": "integer",
            "example": 5368709120
          },
          "attempts": {
            "description": "Upload attempts per file, including the first.",
            "type": "integer",
            "example": 5
          },
          "ship_timeout_seconds": {
            "type": "integer",
            "example": 1800
          }
        }
      },
      "Backups": {
        "description": "The backups drydock has taken, as the Backup objects in its namespace say. Those are written by the orchestrator and reconciled against the bucket's own listing every few minutes — a backup deleted from the bucket loses its object, and one there with no object gets one — so this is the bucket's contents as of that pass, with each backup's outcome and restore test beside it. It can lag the bucket by one pass. It shows what is PRESENT and never what is ABSENT: a backup that was never taken leaves nothing to list.",
        "type": "object",
        "required": [
          "observed_at",
          "target",
          "objects"
        ],
        "additionalProperties": false,
        "properties": {
          "observed_at": {
            "description": "Unix seconds when the server listed the store. Every age on this page is meant to be computed against this rather than the reader's clock.",
            "type": "integer"
          },
          "target": {
            "description": "Where backups go, or null if nothing is configured — no BackupConfig named default in drydock's namespace. Null is an ANSWER and must render as 'backups are not configured', never as an error and never as an empty list. An operator who is told 'no backups' when the truth is 'nothing is configured to make any' has been given the most alarming possible wrong answer.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/BackupTarget"
              }
            ]
          },
          "objects": {
            "description": "One per Backup object, newest first.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BackupObject"
            }
          }
        }
      },
      "BindingState": {
        "type": "string",
        "enum": [
          "Bound",
          "NotFound",
          "Terminating",
          "Denied",
          "Unknown"
        ],
        "description": "The controller writes only Bound and Unknown today. The other three are in the CRD's enum so an object can carry them, and a client must handle all five rather than assuming the two: an unrecognised value read as a success is the one failure mode worth designing out."
      },
      "BudgetBreach": {
        "type": "object",
        "required": [
          "namespace",
          "name",
          "pods_on_node",
          "disruptions_allowed",
          "current_healthy",
          "desired_healthy"
        ],
        "additionalProperties": false,
        "description": "A PodDisruptionBudget the node's death would breach. Compared against the apiserver's own live status.disruptionsAllowed rather than recomputed from replica counts, so maxUnavailable and percentage forms are handled by the authority that defines them. A pod with no budget is unconstrained — absent is not minAvailable: 1. Only Ready pods count: disrupting one that is already not serving takes nothing away.",
        "properties": {
          "namespace": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "pods_on_node": {
            "type": "integer",
            "description": "How many of the budget's Ready pods are on this node."
          },
          "disruptions_allowed": {
            "type": "integer"
          },
          "current_healthy": {
            "type": "integer"
          },
          "desired_healthy": {
            "type": "integer"
          }
        }
      },
      "Capabilities": {
        "description": "What drydock can EVER do to this node's OS. Structural and derived from the family, so it is stable: it does not change when a credential expires. A configured-but-broken integration fails at the call instead, which is a different problem with a different fix.",
        "type": "object",
        "required": [
          "os_upgrade"
        ],
        "additionalProperties": false,
        "properties": {
          "os_upgrade": {
            "$ref": "#/components/schemas/Capability"
          }
        }
      },
      "Capability": {
        "description": "Whether one machine-level operation exists for this node's OS family, and why not where it does not. A reason rather than a bare false, so the UI never has to invent an explanation.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/SupportedCapability"
          },
          {
            "$ref": "#/components/schemas/UnsupportedCapability"
          }
        ]
      },
      "ClusterScope": {
        "type": "object",
        "description": "Cluster-wide. Always true when present: the arm is chosen by the key, not by the value.",
        "required": [
          "cluster"
        ],
        "additionalProperties": false,
        "properties": {
          "cluster": {
            "type": "boolean"
          }
        }
      },
      "ClusterUpgrade": {
        "type": "object",
        "required": [
          "name",
          "os_version",
          "nodes",
          "paused",
          "next_step",
          "node",
          "message"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "The object name. A DNS-1123 label."
          },
          "os_version": {
            "type": "string",
            "description": "The Talos version every in-scope node should end up running."
          },
          "nodes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "description": "Which nodes this upgrade acts on, by name. Never empty, and there is no way to ask for \"every worker\": an upgrade acts on the nodes it names and on nothing else. A control-plane node named here is refused, because the etcd quorum precondition does not exist. Never empty. The minimum is enforced by the CustomResourceDefinition, so the apiserver refuses an object that would violate it; the server does not re-check it on the way out."
          },
          "paused": {
            "type": "boolean",
            "description": "Stop taking new steps. Does not interrupt a node already in flight — abandoning a cordoned, drained, half-upgraded node is worse than finishing it."
          },
          "next_step": {
            "type": "string",
            "enum": [
              "converged",
              "cordon",
              "drain",
              "snapshot",
              "upgrade_os",
              "await",
              "uncordon",
              "blocked"
            ],
            "description": "What the reconciler decided on its last pass. Null until the first pass, which is the normal state for a few seconds after creation. This is OBSERVATION, not progress: the reconciler never reads it back, and re-derives everything from the cluster each time. The values are exactly drydock/upgrade/decide.step_name's, and a test asserts that every step the reconciler can write appears here.",
            "nullable": true
          },
          "node": {
            "type": "string",
            "description": "The node that step acts on, if any.",
            "nullable": true
          },
          "message": {
            "type": "string",
            "description": "Why, in the operator's terms. For `blocked` this is the precondition that failed and is the most useful field on the object.",
            "nullable": true
          }
        }
      },
      "Component": {
        "type": "object",
        "required": [
          "name",
          "version"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "enum": [
              "kube-apiserver",
              "kube-controller-manager",
              "kube-scheduler"
            ]
          },
          "version": {
            "type": "string",
            "example": "v1.36.0"
          }
        }
      },
      "DisabledOnlyPatch": {
        "type": "object",
        "description": "Switch an account off or on, touching nothing else. Any other field present makes this a full replacement instead, which is why no other field is allowed here.",
        "required": [
          "disabled"
        ],
        "additionalProperties": false,
        "properties": {
          "disabled": {
            "type": "boolean"
          }
        }
      },
      "Displaced": {
        "type": "object",
        "required": [
          "namespace",
          "name",
          "destination",
          "reschedulable",
          "refusals",
          "ownerSurvivors"
        ],
        "additionalProperties": false,
        "properties": {
          "namespace": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "destination": {
            "type": "string",
            "description": "The node it would land on, or null when nothing can take it. An approximation of where, not a prediction: the real scheduler scores among the nodes that fit, and this takes the first.",
            "nullable": true
          },
          "reschedulable": {
            "type": "boolean"
          },
          "refusals": {
            "type": "array",
            "description": "Why each surviving node refused it. Empty when the pod was placed.",
            "items": {
              "$ref": "#/components/schemas/SchedulingRefusal"
            }
          },
          "ownerSurvivors": {
            "type": "integer",
            "description": "How many pods of the same workload keep running elsewhere. The difference between degraded and down: a stranded replica of three leaves a service still serving on two, while zero leaves it off — and off it stays, because whatever would restart it has nowhere to put it. Zero on a stranded pod is a critical. Null for a pod with no controller, which nothing recreates even on a healthy cluster.",
            "nullable": true
          }
        }
      },
      "Drain": {
        "description": "Whether it is currently safe to take this node down. NOT a boolean, on purpose: the checks performable from the Kubernetes API alone are a strict subset of what a real drain requires, and an empty 'blockers' list therefore does not mean safe. 'unchecked' names what was not evaluated so the UI can say so rather than implying a clean bill of health.",
        "type": "object",
        "required": [
          "blockers",
          "unchecked"
        ],
        "additionalProperties": false,
        "properties": {
          "blockers": {
            "type": "array",
            "description": "Checks that were performed and failed. Non-empty means definitely not safe.",
            "items": {
              "type": "string"
            }
          },
          "unchecked": {
            "type": "array",
            "description": "Preconditions this endpoint does not yet evaluate — PodDisruptionBudgets, spare capacity, etcd quorum. See docs/design/upgrade-orchestration.md.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Finding": {
        "type": "object",
        "required": [
          "kind",
          "text"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "budget",
              "quorum",
              "stranded"
            ],
            "description": "budget: a PodDisruptionBudget would be breached; the numbers are in budget_breaches. stranded: a pod has nowhere to go; which pods and why per node is in displaced. quorum: etcd would lose or narrow its majority — the only kind with no structured counterpart, because it is about the cluster rather than any object."
          },
          "text": {
            "type": "string",
            "description": "The finding as a sentence, for a log line or a non-visual consumer."
          }
        }
      },
      "FlapStanding": {
        "type": "object",
        "description": "What the uptime record says about a node flapping (issue 84): now, and over the last 30 days.",
        "additionalProperties": false,
        "required": [
          "flapping_since",
          "episodes_30d",
          "suspect"
        ],
        "properties": {
          "flapping_since": {
            "type": "integer",
            "nullable": true,
            "description": "The first down of a flap episode running now, unix seconds; null unless the node is flapping right now."
          },
          "episodes_30d": {
            "type": "integer",
            "description": "Flap episodes in the last 30 days, a running one included."
          },
          "suspect": {
            "type": "boolean",
            "description": "Three or more episodes in 30 days. On dedicated hardware that is usually a part failing; drydock will not start an upgrade on such a node."
          }
        }
      },
      "GenericOs": {
        "type": "object",
        "required": [
          "family",
          "osImage"
        ],
        "additionalProperties": false,
        "properties": {
          "family": {
            "type": "string",
            "description": "Known only through Kubernetes. Deliberately not 'unmanaged': drydock inventories, cordons, drains, risk-scores and records the uptime of these nodes. The one thing it does not do is upgrade them.",
            "enum": [
              "generic"
            ]
          },
          "osImage": {
            "type": "string",
            "description": "The raw osImage string, reported verbatim because drydock parsed no release out of it. Empty for a node that has never registered and therefore has no status.nodeInfo.",
            "example": "Debian GNU/Linux 13 (trixie)"
          }
        }
      },
      "GrantDrift": {
        "type": "object",
        "required": [
          "binding",
          "namespace",
          "driftDetectedAt",
          "repairedAt"
        ],
        "additionalProperties": false,
        "properties": {
          "binding": {
            "type": "string"
          },
          "namespace": {
            "type": "string",
            "description": "Null for a ClusterRoleBinding, which belongs to no namespace.",
            "nullable": true
          },
          "driftDetectedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "repairedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "GrantPatch": {
        "type": "object",
        "description": "Suspend or resume a grant, and optionally replace what it grants. `suspended` is always required. `subjects`, `role` and `scope` travel together: a body carrying `subjects` must carry all three, and a body carrying none of them changes only `suspended`. That rule is not expressible in the schema, so it is stated here and enforced by the handler.",
        "required": [
          "suspended"
        ],
        "additionalProperties": false,
        "properties": {
          "suspended": {
            "type": "boolean"
          },
          "subjects": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/GrantSubject"
            }
          },
          "role": {
            "$ref": "#/components/schemas/GrantRoleRefRequest"
          },
          "scope": {
            "$ref": "#/components/schemas/GrantScope"
          }
        }
      },
      "GrantRequest": {
        "type": "object",
        "description": "An AccessGrant to create.",
        "required": [
          "name",
          "subjects",
          "role",
          "scope"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "subjects": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/GrantSubject"
            }
          },
          "role": {
            "$ref": "#/components/schemas/GrantRoleRefRequest"
          },
          "scope": {
            "$ref": "#/components/schemas/GrantScope"
          }
        }
      },
      "GrantRoleRef": {
        "type": "object",
        "required": [
          "kind",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "ClusterRole",
              "Role"
            ],
            "description": "A Role is namespaced, so it can only be named by a grant whose scope is a single namespace containing it. The common case is a ClusterRole bound at namespace scope, which is how the built-in view/edit/admin are meant to be used."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 253,
            "description": "A role that ALREADY EXISTS. Nothing in this contract authors rules and there is no rule-level field anywhere in it."
          }
        }
      },
      "GrantRoleRefRequest": {
        "type": "object",
        "description": "The role to grant: one of the ClusterRoles `AccessGrantList.roles` names, with a cluster-wide scope. Any other role answers 422. `kind` may be omitted, and defaults to ClusterRole.",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "ClusterRole",
              "Role"
            ],
            "default": "ClusterRole"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "GrantScope": {
        "description": "Cluster-wide, or an explicit list of namespaces — exactly one. There is deliberately no selector: namespaces are named, never matched. A selector is default-allow, so creating a namespace becomes the act of granting access inside it, and it also makes the admission check inexact because RBAC evaluates 'bind' per namespace. Exactly one arm, which the CustomResourceDefinition's own oneOf enforces. A stored object carrying neither would read back as an empty namespace list — deliberately, so that an object the apiserver should never have accepted reports no bindings rather than binding cluster-wide by accident.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/ClusterScope"
          },
          {
            "$ref": "#/components/schemas/NamespaceScope"
          }
        ]
      },
      "GrantStatus": {
        "type": "object",
        "description": "What the controller last observed — effective, never intended. Render this rather than the spec: a grant naming a namespace that does not exist looks perfect in its spec and binds nobody.",
        "required": [
          "namespaces",
          "clusterBinding",
          "drift"
        ],
        "additionalProperties": false,
        "properties": {
          "namespaces": {
            "type": "array",
            "description": "ONE ENTRY PER SPEC NAMESPACE, always — including the ones that failed. 'Granted in 4 of 5' must stay visible per namespace and must never collapse into a success nor into a failure, which is why this is keyed off the spec rather than off the bindings that happen to exist. Empty on a cluster-wide grant, and empty on a grant the controller has not reached: `reconciled` is what separates those two.",
            "items": {
              "$ref": "#/components/schemas/NamespaceStatus"
            }
          },
          "clusterBinding": {
            "type": "object",
            "description": "Present only on a cluster-wide grant. Null on a namespaced one, and null on a cluster-wide grant the controller has not reached yet.",
            "required": [
              "name",
              "state"
            ],
            "additionalProperties": false,
            "properties": {
              "name": {
                "type": "string"
              },
              "state": {
                "$ref": "#/components/schemas/BindingState"
              }
            },
            "nullable": true
          },
          "drift": {
            "type": "array",
            "description": "Bindings that went missing and were re-created. Repaired and REPORTED, never healed silently: a reconciler that quietly wins the fight destroys the only evidence that somebody was trying to revoke access during an incident. A client should surface a non-empty list rather than treat it as noise.",
            "items": {
              "$ref": "#/components/schemas/GrantDrift"
            }
          }
        }
      },
      "GrantSubject": {
        "type": "object",
        "required": [
          "kind",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "Group",
              "User"
            ],
            "description": "ServiceAccount is deliberately absent and is not an oversight. This is about which PEOPLE hold access; workload identity has a different lifecycle and belongs beside the workload. If a ServiceAccount is not something a person can be granted, it should not be something a person can become."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 253,
            "description": "As the identity provider asserts it, e.g. 'oidc#user' or 'drydock:admin'. FREE TEXT, unavoidably: there is no `kubectl get users` and no way to enumerate the groups an IdP might assert, so a client cannot offer a picker and must not pretend to. A typo produces a binding that matches nobody and fails silently, which is why the status below is checked against the cluster rather than assumed."
          }
        }
      },
      "GroupAfter": {
        "type": "object",
        "required": [
          "group",
          "cpu_percent",
          "memory_percent"
        ],
        "additionalProperties": false,
        "description": "One placement group after this node is lost.",
        "properties": {
          "group": {
            "type": "string",
            "description": "A `PlacementGroup.id`."
          },
          "cpu_percent": {
            "type": "integer"
          },
          "memory_percent": {
            "type": "integer"
          }
        }
      },
      "GroupFill": {
        "type": "object",
        "required": [
          "cpu_percent",
          "memory_percent"
        ],
        "additionalProperties": false,
        "description": "A share of a placement group's allocatable, in whole percent.",
        "properties": {
          "cpu_percent": {
            "type": "integer"
          },
          "memory_percent": {
            "type": "integer"
          }
        }
      },
      "GroupUsage": {
        "type": "object",
        "required": [
          "before",
          "after"
        ],
        "additionalProperties": false,
        "properties": {
          "before": {
            "$ref": "#/components/schemas/UsageFigures"
          },
          "after": {
            "$ref": "#/components/schemas/UsageFigures"
          }
        },
        "description": "A placement group's live use (issue 119): its nodes now, and its survivors after the worst loss with the displaced pods' use landing there. The lost node's own outside-pod use leaves with it."
      },
      "GroupWorst": {
        "type": "object",
        "required": [
          "node",
          "cpu_percent",
          "memory_percent"
        ],
        "additionalProperties": false,
        "properties": {
          "node": {
            "type": "string"
          },
          "cpu_percent": {
            "type": "integer"
          },
          "memory_percent": {
            "type": "integer"
          }
        }
      },
      "Headroom": {
        "type": "object",
        "required": [
          "moving_cpu_millicores",
          "moving_memory_bytes",
          "survivors"
        ],
        "additionalProperties": false,
        "description": "What every surviving node would be running at if the dead node's pods moved to them (issue 119); `survivors` is every surviving node after the move. Only pods move: the dead node's system use leaves with it, a pod that cannot be placed does not transfer, and DaemonSet and static pods are never displaced. Approximate by construction, since load does not transfer one-for-one, and it answers the question requests cannot: a cluster whose requests all fit can still end up at 95% real CPU. Present only with both node and pod metrics.",
        "properties": {
          "moving_cpu_millicores": {
            "type": "integer",
            "description": "The use of the dead node's pods that are placed elsewhere."
          },
          "moving_memory_bytes": {
            "type": "integer",
            "description": "The same, in bytes."
          },
          "survivors": {
            "$ref": "#/components/schemas/UsageFigures"
          }
        }
      },
      "IdentityProvider": {
        "type": "object",
        "description": "A configured identity provider, as the providers screen shows it.",
        "required": [
          "name",
          "issuer",
          "clientId",
          "scopes",
          "usernameClaim",
          "usernamePrefix",
          "groupsClaim",
          "hasClientSecret",
          "disabled"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "issuer": {
            "type": "string"
          },
          "clientId": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "usernameClaim": {
            "type": "string",
            "description": "Defaults to `sub` when the object omits it."
          },
          "usernamePrefix": {
            "type": "string"
          },
          "groupsClaim": {
            "type": "string",
            "description": "Null when this provider carries no group claim.",
            "nullable": true
          },
          "hasClientSecret": {
            "type": "boolean",
            "description": "Whether one is configured, never where it lives or what it is."
          },
          "disabled": {
            "type": "boolean"
          }
        }
      },
      "IdentityProviderList": {
        "type": "object",
        "description": "Every configured provider.",
        "required": [
          "providers"
        ],
        "additionalProperties": false,
        "properties": {
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IdentityProvider"
            }
          }
        }
      },
      "IdentityProviderPatch": {
        "description": "Either a body carrying only `disabled`, or a full replacement. Same two-decoder shape as a local user patch, without the password arm.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/DisabledOnlyPatch"
          },
          {
            "$ref": "#/components/schemas/IdentityProviderRequest"
          }
        ]
      },
      "IdentityProviderRequest": {
        "type": "object",
        "description": "A provider to create, or a full replacement of one.\n\nThis is the one request body in the API shaped like a Kubernetes object, which sits awkwardly beside the rule about not passing raw Kubernetes objects through. It is described as it is rather than quietly reshaped; `clientSecret` and `publicClient` sit outside `spec` because they are instructions to the server, not fields of the object.",
        "required": [
          "metadata",
          "spec"
        ],
        "additionalProperties": false,
        "properties": {
          "metadata": {
            "type": "object",
            "description": "",
            "required": [
              "name"
            ],
            "additionalProperties": false,
            "properties": {
              "name": {
                "type": "string",
                "description": "The object name, and how the provider is named in a token exchange."
              }
            }
          },
          "spec": {
            "type": "object",
            "description": "",
            "required": [
              "oidc"
            ],
            "additionalProperties": false,
            "properties": {
              "oidc": {
                "$ref": "#/components/schemas/OidcSpec"
              },
              "disabled": {
                "type": "boolean"
              }
            }
          },
          "clientSecret": {
            "type": "string",
            "description": "The secret value, which the server writes into a Secret and then references. Trimmed, because it is pasted. An empty string means the same as absent: leave the existing reference alone.",
            "nullable": true
          },
          "publicClient": {
            "type": "boolean",
            "description": "A client with no secret at all. Drops any existing reference."
          }
        }
      },
      "Inventory": {
        "description": "Per-node lifecycle inventory. Deliberately not a passthrough of Kubernetes Node objects: every field here is something the dashboard renders, and nothing here requires the dashboard to know the Kubernetes schema.",
        "type": "object",
        "required": [
          "nodes"
        ],
        "additionalProperties": false,
        "properties": {
          "nodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InventoryNode"
            }
          }
        }
      },
      "InventoryNode": {
        "type": "object",
        "required": [
          "name",
          "role",
          "os",
          "kubeletVersion",
          "architecture",
          "kernelVersion",
          "containerRuntime",
          "ready",
          "schedulable",
          "controlPlaneComponents",
          "capabilities",
          "upgrade",
          "drain"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "control-plane",
              "worker"
            ],
            "description": "Derived from the node-role.kubernetes.io/control-plane label."
          },
          "os": {
            "$ref": "#/components/schemas/NodeOs"
          },
          "kubeletVersion": {
            "type": "string",
            "example": "v1.36.0"
          },
          "architecture": {
            "type": "string",
            "example": "amd64"
          },
          "kernelVersion": {
            "type": "string",
            "example": "6.18.24-talos"
          },
          "containerRuntime": {
            "type": "string",
            "example": "containerd://2.2.3"
          },
          "ready": {
            "type": "string",
            "enum": [
              "ready",
              "not-ready",
              "unknown"
            ],
            "description": "The Ready condition. 'unknown' means the condition is absent or itself reports Unknown, which is what a node whose kubelet has stopped reporting looks like."
          },
          "schedulable": {
            "type": "boolean",
            "description": "False when the node is cordoned (spec.unschedulable). A control-plane node carrying only the standard control-plane NoSchedule taint is still schedulable=true; the taint is not a cordon."
          },
          "controlPlaneComponents": {
            "type": "array",
            "description": "Versions of the static control-plane pods running on this node, read from their container images. Empty for worker nodes.",
            "items": {
              "$ref": "#/components/schemas/Component"
            }
          },
          "capabilities": {
            "$ref": "#/components/schemas/Capabilities"
          },
          "upgrade": {
            "$ref": "#/components/schemas/Upgrade"
          },
          "drain": {
            "$ref": "#/components/schemas/Drain"
          },
          "flap": {
            "$ref": "#/components/schemas/FlapStanding"
          }
        },
        "description": "`flap` is absent when the uptime record could not be read."
      },
      "License": {
        "description": "The server's own licence state, for signed-in callers only: it names the customer, the cluster and the contract. Every claim-derived field is present and null when there is no valid licence, so a client renders one shape in every state rather than branching before it can read anything.",
        "type": "object",
        "required": [
          "status",
          "allows_writes",
          "detail",
          "id",
          "expires_at",
          "grace_ends_at",
          "days_left",
          "cluster_id",
          "data",
          "limits",
          "usage",
          "compliant"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "$ref": "#/components/schemas/LicenseStatus"
          },
          "allows_writes": {
            "description": "Whether operations that change the cluster are permitted. Reads are never gated on the licence, so this never affects the inventory. True exactly when status is active or grace.",
            "type": "boolean"
          },
          "detail": {
            "description": "Why the licence is not usable, in terms an operator can act on. Null when there is a valid key.",
            "type": "string",
            "nullable": true
          },
          "id": {
            "description": "The licence identifier, for support. Not a secret. Null when there is no valid key.",
            "type": "string",
            "nullable": true
          },
          "expires_at": {
            "description": "Unix seconds. After this the licence is expired, but see grace_ends_at.",
            "type": "integer",
            "nullable": true
          },
          "grace_ends_at": {
            "description": "Unix seconds. When cluster operations stop.",
            "type": "integer",
            "nullable": true
          },
          "days_left": {
            "description": "Whole days, rounded up, until expiry while active or until grace ends while in grace. Null once lapsed — a countdown that has stopped meaning anything should not be rendered as zero.",
            "type": "integer",
            "nullable": true,
            "minimum": 0
          },
          "cluster_id": {
            "description": "The cluster this licence is bound to, or null when it is portable. When set it is the kube-system namespace UID.",
            "type": "string",
            "nullable": true
          },
          "data": {
            "description": "The licence payload: a flat string map, empty when there is none. Deliberately opaque to this contract — whatever needs a number parses it at the point of use.",
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "limits": {
            "$ref": "#/components/schemas/LicenseLimits"
          },
          "usage": {
            "$ref": "#/components/schemas/LicenseUsage"
          },
          "compliant": {
            "description": "Whether the cluster is inside every licensed limit. Null when there is no valid licence or nothing has been counted — true would claim compliance nobody checked, false would allege a breach on no evidence. IMPORTANT: false does NOT affect allows_writes. Exceeding a node limit is reported, never enforced: drydock's writes are upgrades and drains, and refusing them because an autoscaler added a node would leave a cluster unpatched.",
            "type": "boolean",
            "nullable": true
          }
        }
      },
      "LicenseLimits": {
        "description": "What the licence permits. Null means unlimited, NOT zero. Both fields are null when there is no valid licence.",
        "type": "object",
        "required": [
          "max_worker_nodes",
          "max_control_plane_nodes"
        ],
        "additionalProperties": false,
        "properties": {
          "max_worker_nodes": {
            "type": "integer",
            "nullable": true,
            "minimum": 0
          },
          "max_control_plane_nodes": {
            "type": "integer",
            "nullable": true,
            "minimum": 0
          }
        }
      },
      "LicenseStatus": {
        "type": "string",
        "enum": [
          "active",
          "grace",
          "lapsed",
          "unlicensed",
          "invalid"
        ],
        "description": "active and grace both permit cluster operations; grace means the licence has expired and is inside its grace window. lapsed means expired and past grace. unlicensed means no key was configured. invalid means a key was configured and refused."
      },
      "LicenseUsage": {
        "description": "What the cluster actually has, counted by the server as itself on a timer. Null until the first count succeeds — the Pod may lack `list nodes`, or may have just started. Every registered node is counted whatever its condition, so cordoning one does not duck a limit.",
        "type": "object",
        "required": [
          "worker_nodes",
          "control_plane_nodes",
          "observed_at"
        ],
        "additionalProperties": false,
        "properties": {
          "worker_nodes": {
            "type": "integer",
            "minimum": 0
          },
          "control_plane_nodes": {
            "type": "integer",
            "minimum": 0
          },
          "observed_at": {
            "description": "Unix seconds of the count.",
            "type": "integer"
          }
        },
        "nullable": true
      },
      "LocalDisk": {
        "type": "object",
        "required": [
          "lost_requested_bytes",
          "projected_requested_percent"
        ],
        "additionalProperties": false,
        "description": "The survivors' local disk (`ephemeral-storage`) after the loss, from requests against allocatable. Requests only: the metrics API reports no disk usage, so this says what was asked for, not what is written. Nodes that report no `ephemeral-storage` allocatable are left out of both sides.",
        "properties": {
          "lost_requested_bytes": {
            "type": "integer",
            "description": "What the pods that would move ask for. A pod with nowhere to go takes no disk anywhere and is not counted."
          },
          "projected_requested_percent": {
            "type": "integer",
            "description": "The survivors' requests, plus `lost_requested_bytes`, as a percentage of their allocatable."
          }
        }
      },
      "LocalUser": {
        "type": "object",
        "description": "A local user as the users screen shows it. No verifier and no secret contents: the type behind this never held them.",
        "required": [
          "name",
          "username",
          "displayName",
          "groups",
          "disabled",
          "hasPassword"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "The LocalUser object's name."
          },
          "username": {
            "type": "string",
            "description": "The login name, which is what a credential carries."
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "disabled": {
            "type": "boolean"
          },
          "hasPassword": {
            "type": "boolean",
            "description": "Whether a password is set, never where it lives or what it is."
          }
        }
      },
      "LocalUserList": {
        "type": "object",
        "description": "Every local user.",
        "required": [
          "users"
        ],
        "additionalProperties": false,
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LocalUser"
            }
          }
        }
      },
      "LocalUserPatch": {
        "description": "Three shapes, tried in order: a body carrying only `disabled`, a body carrying only `password`, or a full replacement. The narrow two forbid every other field, because taking the narrow path with a fuller body would silently discard the rest of it. `anyOf` and not `oneOf`: every field of the replacement is optional, so `{\"disabled\": true}` is also a valid replacement, and the server takes the first shape that matches.",
        "anyOf": [
          {
            "$ref": "#/components/schemas/DisabledOnlyPatch"
          },
          {
            "$ref": "#/components/schemas/PasswordOnlyPatch"
          },
          {
            "$ref": "#/components/schemas/LocalUserReplacement"
          }
        ]
      },
      "LocalUserReplacement": {
        "type": "object",
        "description": "A full replacement of a local user, not a partial edit. A field may be omitted, but an omitted field is written as its default rather than left as it is: omitting `groups` removes every group membership, `drydock:admin` included, and omitting `disabled` re-enables the account. Send the whole user. An empty body is refused with 422. `name` is ignored: the name comes from the path, so a body naming a different user cannot edit somebody else through a URL that says it is editing this one.",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "Ignored. The path names the user being replaced."
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "disabled": {
            "type": "boolean"
          },
          "password": {
            "type": "string",
            "description": "The chosen password. Never echoed back by any response. Absent leaves the existing one alone.",
            "nullable": true
          }
        }
      },
      "LocalUserRequest": {
        "type": "object",
        "description": "A local user to create. A create without a name is refused, so `name` is required here even though the decoder defaults it. The PATCH replacement is `LocalUserReplacement`, which does not require it.",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "Becomes the object name and the login name."
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "disabled": {
            "type": "boolean"
          },
          "password": {
            "type": "string",
            "description": "The chosen password. Never echoed back by any response. Absent leaves the existing one alone.",
            "nullable": true
          }
        }
      },
      "LoginAnswer": {
        "type": "object",
        "description": "A minted session. One message is returned for every reason a sign-in can fail, so nothing here distinguishes a wrong password from an unknown user.",
        "required": [
          "token",
          "expiresIn",
          "username",
          "displayName",
          "groups"
        ],
        "additionalProperties": false,
        "properties": {
          "token": {
            "type": "string",
            "description": "The session token, to be sent as a bearer credential."
          },
          "expiresIn": {
            "type": "integer",
            "description": "Seconds from now."
          },
          "username": {
            "type": "string"
          },
          "displayName": {
            "type": "string",
            "nullable": true
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "LoginRequest": {
        "type": "object",
        "description": "A local sign-in. The username is trimmed; the password deliberately is not.",
        "required": [
          "username",
          "password"
        ],
        "additionalProperties": false,
        "properties": {
          "username": {
            "type": "string"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "NamespaceScope": {
        "type": "object",
        "description": "An explicit list of namespaces. Namespaces are named, never matched.",
        "required": [
          "namespaces"
        ],
        "additionalProperties": false,
        "properties": {
          "namespaces": {
            "type": "array",
            "minItems": 1,
            "maxItems": 64,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "maxLength": 63,
              "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
            },
            "description": "The 64 ceiling is the CRD's, and it is PROVISIONAL and unmeasured: the admission policy runs one authorizer call per namespace under a CEL cost budget, and exceeding that budget makes the policy ERROR rather than reject the grant."
          }
        }
      },
      "NamespaceStatus": {
        "type": "object",
        "required": [
          "name",
          "state",
          "binding",
          "message",
          "observedAt"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "state": {
            "$ref": "#/components/schemas/BindingState"
          },
          "binding": {
            "type": "string",
            "description": "The generated RoleBinding name. Reported because a binding found by `kubectl` is otherwise untraceable back to the grant that created it, which is the first thing anyone asks. Do not parse it — it is a slug plus a digest, and this field is the authority on what it is.",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "observedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "NodeBound": {
        "type": "object",
        "required": [
          "namespace",
          "name",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "namespace": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        }
      },
      "NodeOs": {
        "description": "The node's operating system, keyed by the family whose driver handles it. A discriminated union rather than one object with an optional version: a family drydock can parse a release out of always has one, and making the field optional would lose that guarantee at the type level. 'osImage' is status.nodeInfo.osImage verbatim and is present on every arm.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/TalosOs"
          },
          {
            "$ref": "#/components/schemas/GenericOs"
          }
        ]
      },
      "NodeRisk": {
        "type": "object",
        "required": [
          "name",
          "control_plane",
          "severity",
          "findings",
          "displaced",
          "node_bound",
          "budget_breaches",
          "owners",
          "headroom",
          "local_disk",
          "group_after"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "control_plane": {
            "type": "boolean",
            "description": "Carries the etcd quorum question with it: losing a control-plane node is never merely a pod-placement problem."
          },
          "severity": {
            "$ref": "#/components/schemas/Severity"
          },
          "findings": {
            "type": "array",
            "description": "Most severe first. Every finding is actionable and the list explains the severity — a node whose severity is above info always says why. `kind` says what sort it is so a UI can group without matching on the sentence, which is how a reworded string becomes a layout bug. Node-bound pods deliberately produce no finding: every node in every cluster has some, so a line about them would fire always and mean nothing.",
            "items": {
              "$ref": "#/components/schemas/Finding"
            }
          },
          "displaced": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Displaced"
            },
            "description": "Pods that would have to move, and where they would land. Placement is sequential and largest-first, so each pod consumes room on the node it lands on — three 800m pods do not all fit in one node's 1000m of headroom. A pod bound to a PersistentVolume with node affinity is refused by every node the volume does not name, because such a pod does not move at all."
          },
          "node_bound": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NodeBound"
            },
            "description": "Pods that die with the node and are not recreated elsewhere: DaemonSet pods, which exist once per node by definition, and static pods, which the kubelet owns. Detail only — they are excluded from placement, from redundancy counting and from severity."
          },
          "budget_breaches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BudgetBreach"
            }
          },
          "owners": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OwnerImpact"
            }
          },
          "headroom": {
            "description": "Null without node and pod metrics. Advisory in every case: it never decides whether a pod can be placed.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/Headroom"
              }
            ]
          },
          "local_disk": {
            "description": "Null when no surviving node reports `ephemeral-storage` allocatable. Computed from requests, so unlike `headroom` it does not need metrics.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/LocalDisk"
              }
            ]
          },
          "group_after": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GroupAfter"
            },
            "description": "Each placement group this loss changes, as it would be afterwards: the groups the node is in, and those its pods land in. A group not listed is as it was."
          }
        }
      },
      "OidcSpec": {
        "type": "object",
        "description": "The provider's OIDC configuration, in the object's own spelling.",
        "required": [
          "issuer",
          "clientId"
        ],
        "additionalProperties": false,
        "properties": {
          "issuer": {
            "type": "string"
          },
          "clientId": {
            "type": "string"
          },
          "clientSecretRef": {
            "$ref": "#/components/schemas/SecretRef"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Defaulted by the CRD when absent."
          },
          "usernameClaim": {
            "type": "string",
            "description": "Defaults to `sub`."
          },
          "usernamePrefix": {
            "type": "string",
            "description": "Deliberately not defaulted: a shared default is exactly the collision this field exists to prevent, so an object without one is refused."
          },
          "groupsClaim": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "OwnerImpact": {
        "type": "object",
        "required": [
          "namespace",
          "kind",
          "name",
          "running_pods",
          "pods_on_node",
          "stranded_pods"
        ],
        "additionalProperties": false,
        "description": "A workload losing pods. Counted from the pods that actually exist rather than from spec.replicas: a Deployment scaled to 3 with one pod Pending has two real replicas, and counting the desired number would claim a resilience the cluster does not have. ReplicaSets are resolved to the Deployment that owns them so the name is one a human recognises.",
        "properties": {
          "namespace": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "running_pods": {
            "type": "integer"
          },
          "pods_on_node": {
            "type": "integer"
          },
          "stranded_pods": {
            "type": "integer",
            "description": "How many of the lost pods have nowhere to go."
          }
        }
      },
      "PasswordOnlyPatch": {
        "type": "object",
        "description": "Set a password, touching nothing else.",
        "required": [
          "password"
        ],
        "additionalProperties": false,
        "properties": {
          "password": {
            "type": "string"
          }
        }
      },
      "PlacementGroup": {
        "type": "object",
        "required": [
          "id",
          "name",
          "constraint",
          "nodes",
          "requested",
          "worst",
          "losses",
          "usage"
        ],
        "additionalProperties": false,
        "description": "The nodes a kind of pod may use (issue 112), taken from the pods' own static constraints: taints, nodeSelector, required node affinity and local-volume affinity. Pods allowed the same nodes share a group. Never configured: how a customer groups nodes is not needed when the pods already say where they may go. Groups may overlap, since `zone=b` is also inside \"any node\".",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable within one answer only, for `NodeRisk.group_after` to refer to."
          },
          "name": {
            "type": "string",
            "description": "The constraint that narrowed the set, in words: `pool=gpu`, `without taint node-role.kubernetes.io/control-plane`, or `any node`."
          },
          "constraint": {
            "type": "string",
            "enum": [
              "selector",
              "affinity",
              "taint",
              "none"
            ]
          },
          "nodes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requested": {
            "description": "Requests now, over the group's allocatable.",
            "allOf": [
              {
                "$ref": "#/components/schemas/GroupFill"
              }
            ]
          },
          "worst": {
            "description": "The loss that leaves the group fullest, by its higher figure, and the node lost. Pods landing from any node count. Null only when no loss changes the group.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/GroupWorst"
              }
            ]
          },
          "losses": {
            "type": "array",
            "description": "Each member's own loss (issue 118), one per entry in `nodes` and in the same order: the group's requests over its survivors' allocatable after losing that node, pods landing from any node counted. The same figure `worst` is chosen by, so the dashboard can rank every member, findings or not.",
            "items": {
              "$ref": "#/components/schemas/GroupWorst"
            }
          },
          "usage": {
            "description": "Live use before and after the worst loss, as amounts (issue 119). Null without node and pod metrics.",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/GroupUsage"
              }
            ]
          }
        }
      },
      "PodReference": {
        "type": "object",
        "required": [
          "namespace",
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "namespace": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "description": "A pod, by its full name. Never abbreviated on the wire: shortening a generated name is a display decision."
      },
      "Problem": {
        "type": "object",
        "description": "An error, as RFC 9457 problem details. Served as `application/problem+json`.\n\n`type` identifies the problem and is the only member a client should branch on. `title` is a fixed sentence for that type and is safe to show a user; `detail` is about this one occurrence and may be reworded, so do not match on either. `status` repeats the HTTP status, which remains authoritative if they ever disagree.\n\n`type` is `about:blank` where the status code carries the whole meaning — a body that is not JSON, a method that does not apply, an oversized body, a wrong content type, a crash. RFC 9457 section 4.2.1 asks for the status phrase as the `title` in that case, and that is what is sent.\n\nOne schema is used for every error response rather than a per-response schema narrowing `type` to a single value. That would be more precise and would make oaspec flatten eighteen near-identical records; the response descriptions name their type in prose instead.\n\nClosed to additional properties. RFC 9457 lets a producer add extension members and requires consumers to ignore ones they do not know, but everything this server sends is listed here, and closing it is what lets validation catch an undeclared one.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "instance"
        ],
        "additionalProperties": false,
        "properties": {
          "type": {
            "type": "string",
            "description": "The problem type. Branch on this.",
            "enum": [
              "about:blank",
              "https://drydock.asergo.com/problems/exchange-failed",
              "https://drydock.asergo.com/problems/unauthenticated",
              "https://drydock.asergo.com/problems/invalid-credentials",
              "https://drydock.asergo.com/problems/license-required",
              "https://drydock.asergo.com/problems/account-refused",
              "https://drydock.asergo.com/problems/forbidden",
              "https://drydock.asergo.com/problems/not-found",
              "https://drydock.asergo.com/problems/unknown-endpoint",
              "https://drydock.asergo.com/problems/disruption-budget",
              "https://drydock.asergo.com/problems/validation-failed",
              "https://drydock.asergo.com/problems/upstream-throttled",
              "https://drydock.asergo.com/problems/not-configured",
              "https://drydock.asergo.com/problems/upstream-unavailable",
              "https://drydock.asergo.com/problems/identity-unavailable",
              "https://drydock.asergo.com/problems/upstream-timeout"
            ]
          },
          "title": {
            "type": "string",
            "description": "A fixed sentence for this `type`, or the HTTP status phrase when `type` is `about:blank`."
          },
          "status": {
            "type": "integer",
            "minimum": 400,
            "maximum": 599,
            "description": "The HTTP status, repeated. Advisory; the real status wins."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong this time, for a human. Never the apiserver's own message, which can name resources the caller has no business learning exist."
          },
          "instance": {
            "type": "string",
            "description": "This one occurrence, as `https://drydock.asergo.com/traces/<trace-id>`, carrying the same trace id as the `traceparent` response header. It does not currently resolve, which RFC 9457 section 3.1.5 permits — the point is that the id in an error a user screenshots is the id that finds the log line."
          },
          "errors": {
            "type": "array",
            "description": "Which parts of the request body were refused. Present only on `validation-failed`, and the reason twelve separate error codes collapsed into it: a form needs the field, not a token.",
            "items": {
              "$ref": "#/components/schemas/ProblemItem"
            }
          }
        }
      },
      "ProblemItem": {
        "type": "object",
        "description": "One refused part of a request body.",
        "required": [
          "pointer",
          "detail"
        ],
        "additionalProperties": false,
        "properties": {
          "pointer": {
            "type": "string",
            "description": "An RFC 6901 JSON Pointer into the request body, so `/nodes` or `/subjects/0/kind`. Empty string means the body as a whole."
          },
          "detail": {
            "type": "string",
            "description": "What is wrong with it."
          }
        }
      },
      "PublicProvider": {
        "type": "object",
        "description": "An identity provider as the sign-in page sees it, before there is any credential. The client secret is not here and cannot be: the type carries a reference, never a value. Everything here is what the browser is about to be redirected with anyway.",
        "required": [
          "name",
          "issuer",
          "clientId",
          "scopes",
          "usernamePrefix"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "The IdentityProvider object's name, and what a token exchange names."
          },
          "issuer": {
            "type": "string"
          },
          "clientId": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "usernamePrefix": {
            "type": "string",
            "description": "Prepended to the username claim, so a provider's users cannot collide with local ones or with another provider's."
          }
        }
      },
      "RefreshTokenGrant": {
        "type": "object",
        "description": "Exchange a refresh token for a fresh set.",
        "required": [
          "provider",
          "grantType",
          "refreshToken"
        ],
        "additionalProperties": false,
        "properties": {
          "provider": {
            "type": "string"
          },
          "grantType": {
            "type": "string",
            "enum": [
              "refresh_token"
            ]
          },
          "refreshToken": {
            "type": "string"
          }
        }
      },
      "ResourceGap": {
        "type": "string",
        "enum": [
          "cpu_request",
          "memory_request",
          "cpu_limit",
          "memory_limit"
        ],
        "description": "One thing a workload's containers do not set."
      },
      "ResourceNotice": {
        "type": "object",
        "required": [
          "namespace",
          "kind",
          "name",
          "missing"
        ],
        "additionalProperties": false,
        "description": "A workload running without a resource request or limit. Read from its pods rather than its pod template, so a default supplied by a LimitRange counts as set. One entry per workload: ReplicaSets are resolved to their Deployment and Jobs to their CronJob.",
        "properties": {
          "namespace": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "The owner's kind, such as Deployment, StatefulSet, DaemonSet, CronJob or Job. `Pod` for a pod with no controller."
          },
          "name": {
            "type": "string"
          },
          "missing": {
            "type": "array",
            "minItems": 1,
            "description": "What at least one long-running container of the workload does not set. Ordinary init containers are not examined.",
            "items": {
              "$ref": "#/components/schemas/ResourceGap"
            }
          }
        }
      },
      "RiskAssessment": {
        "description": "GET /api/v1/risk — what would break if any one node died. A cluster-wide sweep: every node is assessed as though it had just been lost, so the answer is about how much slack the cluster has rather than about one node. Read-only, acts as the calling user, never gated on the licence.",
        "type": "object",
        "required": [
          "severity",
          "metrics_used",
          "nodes_assessed",
          "critical_nodes",
          "nodes",
          "unchecked",
          "resource_notices",
          "groups",
          "pinned_pods"
        ],
        "additionalProperties": false,
        "properties": {
          "severity": {
            "$ref": "#/components/schemas/Severity",
            "description": "The worst severity across every node."
          },
          "metrics_used": {
            "type": "boolean",
            "description": "Whether live usage was read from metrics.k8s.io. False on a cluster without metrics-server, or where the caller's RBAC does not reach it. Placement never depends on this — it is computed from resource requests either way, because that is what kube-scheduler reserves. Metrics only add the headroom figures."
          },
          "critical_nodes": {
            "type": "integer",
            "minimum": 0,
            "description": "How many nodes cannot be lost without something certainly breaking. The number to act on: anything above zero means the cluster cannot currently absorb the failure of at least one of its nodes."
          },
          "nodes_assessed": {
            "type": "integer",
            "minimum": 0,
            "description": "How many nodes were examined, as opposed to reported. Without it a client cannot tell 'three nodes have findings' from 'the cluster has three nodes', and that difference is the whole meaning of the list below."
          },
          "nodes": {
            "type": "array",
            "description": "Only the nodes with something to say. A node whose death changes nothing is omitted entirely — a list of 'info, info, info' is a report people stop reading. An empty array is the good outcome: every node can be lost without anything breaking that would not immediately heal. Nothing is hidden by this: every severity above info comes from a breach, a stranded pod or a lost quorum, and each of those emits a finding, so an omitted node is always info.",
            "items": {
              "$ref": "#/components/schemas/NodeRisk"
            }
          },
          "unchecked": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Preconditions this assessment did not evaluate. An empty findings list means nothing was found by the checks that ran, NOT that the node is safe to lose — these are what was not looked at. Same contract as `drain.unchecked` in inventory.v1."
          },
          "flapping": {
            "type": "array",
            "description": "Nodes flapping now or in the last 30 days, worst first. Absent when the uptime record could not be read, which is not the same as none flapping.",
            "items": {
              "$ref": "#/components/schemas/RiskFlap"
            }
          },
          "resource_notices": {
            "type": "array",
            "description": "Workloads whose pods run without a CPU or memory request or limit, sorted by namespace and name. A notice, not a finding: it never raises `severity` and is not tied to a node. Pods in kube-system, pods under the system-node-critical or system-cluster-critical priority class, and static pods are never listed.",
            "items": {
              "$ref": "#/components/schemas/ResourceNotice"
            }
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlacementGroup"
            },
            "description": "The placement groups, each before and after the worst single-node loss (issue 112)."
          },
          "pinned_pods": {
            "type": "integer",
            "description": "Pods whose constraints allow one node or none, so they are in no group. Their findings say they cannot move."
          }
        }
      },
      "RiskFlap": {
        "type": "object",
        "description": "A node that is flapping now or has flapped in the last 30 days: the likeliest node to be lost next.",
        "additionalProperties": false,
        "required": [
          "node",
          "flapping_since",
          "episodes_30d",
          "suspect"
        ],
        "properties": {
          "node": {
            "type": "string"
          },
          "flapping_since": {
            "type": "integer",
            "nullable": true,
            "description": "The first down of a flap episode running now, unix seconds; null unless the node is flapping right now."
          },
          "episodes_30d": {
            "type": "integer",
            "description": "Flap episodes in the last 30 days, a running one included."
          },
          "suspect": {
            "type": "boolean",
            "description": "Three or more episodes in 30 days. On dedicated hardware that is usually a part failing; drydock will not start an upgrade on such a node."
          }
        }
      },
      "SchedulingRefusal": {
        "type": "object",
        "required": [
          "node",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "node": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "conflicts_with": {
            "$ref": "#/components/schemas/PodReference"
          }
        },
        "description": "One surviving node's reason for refusing the pod, as drydock/risk/schedule.describe renders it. `conflicts_with` is present only when the reason is a required pod anti-affinity rule, and names the pod already on that node exactly as `reason` writes it — so a client can shorten or link the name without searching the sentence for it."
      },
      "SecretRef": {
        "type": "object",
        "description": "A reference to a key in a Secret. Never a value.",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "description": "Defaults to `clientSecret`."
          }
        }
      },
      "Severity": {
        "type": "string",
        "enum": [
          "critical",
          "warning",
          "info"
        ],
        "description": "Reflects only what the cluster cannot fix by itself. critical: a PodDisruptionBudget would be breached, or etcd would lose quorum — both certainties rather than risks, and neither self-resolving. warning: a pod has nowhere to go, which is the actual 'this cluster needs another node' signal. info: everything else. Losing a replica, or every replica of a workload, is deliberately NOT reported when the pods can be rescheduled — that is a gap of seconds Kubernetes closes on its own, and flagging it would fire on nearly every node of a healthy cluster."
      },
      "SupportedCapability": {
        "type": "object",
        "required": [
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "supported"
            ]
          }
        }
      },
      "TalosOs": {
        "type": "object",
        "required": [
          "family",
          "osImage",
          "version"
        ],
        "additionalProperties": false,
        "properties": {
          "family": {
            "type": "string",
            "enum": [
              "talos"
            ]
          },
          "osImage": {
            "type": "string",
            "example": "Talos (v1.13.0)"
          },
          "version": {
            "type": "string",
            "example": "v1.13.0"
          }
        }
      },
      "TokenAnswer": {
        "type": "object",
        "description": "The tokens the issuer returned. `accessToken` and `refreshToken` are null when the issuer did not send one, which is normal for a provider configured without offline access.",
        "required": [
          "idToken",
          "accessToken",
          "refreshToken",
          "expiresAt"
        ],
        "additionalProperties": false,
        "properties": {
          "idToken": {
            "type": "string"
          },
          "accessToken": {
            "type": "string",
            "nullable": true
          },
          "refreshToken": {
            "type": "string",
            "nullable": true
          },
          "expiresAt": {
            "type": "integer",
            "description": "Unix seconds."
          }
        }
      },
      "TokenRequest": {
        "description": "A token exchange, performed by the server so that the client secret never reaches the browser. The arm is chosen by `grantType`.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/AuthorizationCodeGrant"
          },
          {
            "$ref": "#/components/schemas/RefreshTokenGrant"
          }
        ],
        "discriminator": {
          "propertyName": "grantType",
          "mapping": {
            "authorization_code": "#/components/schemas/AuthorizationCodeGrant",
            "refresh_token": "#/components/schemas/RefreshTokenGrant"
          }
        }
      },
      "UnsupportedCapability": {
        "type": "object",
        "required": [
          "status",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "unsupported"
            ]
          },
          "reason": {
            "type": "string"
          }
        }
      },
      "Upgrade": {
        "description": "Whether an OS upgrade is AVAILABLE, which is a different question from whether one is POSSIBLE — that is `capabilities.os_upgrade`. There is no release feed wired up yet, so `unknown` is the honest answer for an upgradeable node rather than `up-to-date`, and for a family drydock cannot upgrade this is `not-applicable`. Both carry a `reason`, and it is the same string the capability reports rather than a second wording of it.\n\nThe enum carried `available` and `up-to-date`, and a `latestVersion` for the first of them, before any code could produce them. They are gone: a contract that describes a feature nobody has built makes every consumer handle a case that cannot arrive, and the dashboard did. They come back when a release feed does.",
        "type": "object",
        "required": [
          "status",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "unknown",
              "not-applicable"
            ]
          },
          "reason": {
            "type": "string",
            "description": "Why. Says so rather than leaving the UI to invent an explanation — and it is the same string `capabilities.os_upgrade` reports, not a second wording of it."
          }
        }
      },
      "UpgradeList": {
        "description": "ClusterUpgrade objects, read as the calling user. Writes to this collection are the permission to upgrade a node: the apiserver decides against Kubernetes RBAC, so a 403 here is a real answer about the signed-in user rather than a failure. Responses carry an ETag derived from the collection's resourceVersion; send it back as If-None-Match and an unchanged collection answers 304 with no body. That saves the encoding and the transfer, but NOT the apiserver read — only a watch or a cache avoids that.",
        "type": "object",
        "required": [
          "upgrades",
          "executing",
          "poll_after_seconds"
        ],
        "additionalProperties": false,
        "properties": {
          "upgrades": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClusterUpgrade"
            }
          },
          "executing": {
            "type": "boolean",
            "description": "Whether the reconciler acts, or only decides and logs. FALSE means the server is in dry run and nothing will actually be cordoned, drained or upgraded — a UI that showed an upgrade as running without saying this would be misrepresenting the most important fact on the page."
          },
          "poll_after_seconds": {
            "type": "integer",
            "minimum": 1,
            "description": "How long a client should wait before asking again, taken from the server's own reconcile interval. Status only moves when the reconciler runs, so polling faster re-reads the same value and polling slower looks frozen. Clamp to a sane floor rather than trusting it blindly."
          }
        }
      },
      "UpgradePatch": {
        "type": "object",
        "description": "The only mutable field. Pausing stops the reconciler before the next step; it does not interrupt a step already running.",
        "required": [
          "paused"
        ],
        "additionalProperties": false,
        "properties": {
          "paused": {
            "type": "boolean"
          }
        }
      },
      "UpgradeRequest": {
        "type": "object",
        "description": "A ClusterUpgrade to create. The nodes are named; there is no selector.",
        "required": [
          "name",
          "os_version",
          "nodes"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "Becomes metadata.name, so it must be a valid Kubernetes object name. Checked by the server rather than the apiserver, so the reason arrives in the error envelope."
          },
          "os_version": {
            "type": "string",
            "description": "The OS version to bring each node to."
          },
          "nodes": {
            "type": "array",
            "minItems": 1,
            "description": "Never empty: an upgrade naming no node would be created and then do nothing.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Uptime": {
        "description": "What drydock observed of each node over time, and which of it drydock caused. Separate from inventory.v1 on purpose: that endpoint describes what exists right now and is polled constantly, this one is a much larger answer that changes slowly. Bundling them would fetch the expensive one at the cheap one's rate. It is also the only endpoint that can name a node that no longer exists.",
        "type": "object",
        "required": [
          "observed_at",
          "stale_after_seconds",
          "covers",
          "nodes",
          "incidents"
        ],
        "additionalProperties": false,
        "properties": {
          "observed_at": {
            "description": "Unix seconds when the server read the records. Every duration here is meant to be computed against this rather than the reader's clock.",
            "type": "integer"
          },
          "stale_after_seconds": {
            "type": "integer",
            "description": "How old a node's `last_observed_at` may be, against `observed_at`, before its record counts as stopped. The recorder's own gap threshold, sent so that the rule has one definition: past it, the recorder itself would call the time unobserved."
          },
          "covers": {
            "description": "The span the records actually account for. NOT the span requested: a cluster drydock has watched for three days cannot answer about ninety, and returning the shorter answer without saying so would let a chart imply months of uptime that nobody observed.",
            "type": "object",
            "required": [
              "from",
              "until"
            ],
            "additionalProperties": false,
            "properties": {
              "from": {
                "type": "integer",
                "description": "Unix seconds of the earliest day any record holds."
              },
              "until": {
                "type": "integer",
                "description": "Unix seconds of the end of the last complete day."
              }
            }
          },
          "nodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UptimeNode"
            }
          },
          "incidents": {
            "description": "Events on several nodes close enough together in time to be one thing. Grouped by the server, not the browser: three nodes going quiet in the same minute is one cluster problem, and presenting it as three node problems is the mistake this endpoint exists to avoid.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UptimeIncident"
            }
          }
        }
      },
      "UptimeAttribution": {
        "description": "Whether drydock caused this. Computed by joining the event against the maintenance windows the orchestrator wrote before it acted — which is the one thing this endpoint can say that a monitoring stack cannot, because a monitoring stack did not take the node down.",
        "type": "object",
        "required": [
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "drydock",
              "unattributed",
              "unknown"
            ],
            "description": "'drydock' means an operation was in flight for this node at this moment. 'unattributed' means none was, which is EVIDENCE the cause was external and not proof of it. 'unknown' means drydock was not watching, or the event was reconstructed, so it cannot say — and must not be rendered as though it were 'unattributed'."
          },
          "cluster_upgrade": {
            "type": "string",
            "description": "Present only for 'drydock'. The ClusterUpgrade that opened the window, by name."
          },
          "phase": {
            "type": "string",
            "description": "Present only for 'drydock'. What was being done — free text, matching ClusterUpgrade's own nextStep."
          }
        }
      },
      "UptimeDay": {
        "type": "object",
        "required": [
          "date",
          "ready_seconds",
          "not_ready_seconds",
          "unknown_seconds",
          "unobserved_seconds",
          "absent_seconds",
          "reboots"
        ],
        "additionalProperties": false,
        "properties": {
          "date": {
            "type": "string",
            "example": "2026-08-10"
          },
          "ready_seconds": {
            "type": "integer",
            "minimum": 0
          },
          "not_ready_seconds": {
            "type": "integer",
            "minimum": 0
          },
          "unknown_seconds": {
            "type": "integer",
            "minimum": 0,
            "description": "The kubelet stopped reporting while drydock was watching. Not the same as unobserved, and rendering them alike loses the difference between 'the node went quiet' and 'we did'."
          },
          "unobserved_seconds": {
            "type": "integer",
            "minimum": 0,
            "description": "drydock was not watching. MUST render as a distinct third state, never as up and never as down. The first time a chart shows green across an outage the operator remembers, every other figure on the page stops being believed."
          },
          "absent_seconds": {
            "type": "integer",
            "minimum": 0,
            "description": "The node did not exist. Only ever a partial day — whole days of absence are the days missing from the array."
          },
          "reboots": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "UptimeEvent": {
        "type": "object",
        "required": [
          "at",
          "kind",
          "reconstructed",
          "attribution"
        ],
        "additionalProperties": false,
        "properties": {
          "at": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "kind": {
            "type": "string",
            "description": "'flapping' is one consolidated flap episode, written when it ends: its transitions are not stored one by one. The readiness event at the same instant says what state the node was left in.",
            "enum": [
              "ready",
              "not-ready",
              "unknown",
              "rebooted",
              "registered",
              "removed",
              "observation-lost",
              "observation-resumed",
              "cordoned",
              "uncordoned",
              "flapping"
            ]
          },
          "reconstructed": {
            "type": "boolean",
            "description": "True when the transition was deduced afterwards rather than witnessed, so `at` is when drydock noticed and not when it happened. The other end of the window is the preceding observation-lost. Render a band, not a line."
          },
          "attribution": {
            "$ref": "#/components/schemas/UptimeAttribution"
          }
        }
      },
      "UptimeIncident": {
        "type": "object",
        "required": [
          "started_at",
          "ended_at",
          "nodes",
          "attribution",
          "kind",
          "rebooted"
        ],
        "additionalProperties": false,
        "properties": {
          "started_at": {
            "type": "integer"
          },
          "ended_at": {
            "type": "integer",
            "description": "Null while at least one node has not recovered.",
            "nullable": true
          },
          "nodes": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Every node caught up in it. More than one is the signal — that is a cluster problem wearing a node problem's clothes."
          },
          "attribution": {
            "$ref": "#/components/schemas/UptimeAttribution"
          },
          "kind": {
            "type": "string",
            "enum": [
              "not-ready",
              "unknown",
              "mixed",
              "flapping",
              "removed"
            ],
            "description": "What the nodes went through. 'mixed' is NotReady then Unknown, which is what a reboot looks like. 'flapping' is one consolidated flap episode, with `flapping` carrying its totals. 'removed' only comes from a month recorded before outages were kept, where the incident is rebuilt from events."
          },
          "rebooted": {
            "type": "boolean",
            "description": "At least one node rebooted inside it. What separates 'it came back' from 'it was restarted'."
          },
          "flapping": {
            "type": "object",
            "description": "Present when kind is 'flapping': the episode's totals, summed across its nodes.",
            "required": [
              "transitions",
              "reboots",
              "down_seconds"
            ],
            "additionalProperties": false,
            "properties": {
              "transitions": {
                "type": "integer",
                "description": "Readiness changes consolidated into the episode."
              },
              "reboots": {
                "type": "integer"
              },
              "down_seconds": {
                "type": "integer",
                "description": "NotReady and Unknown seconds inside the episode."
              }
            }
          }
        }
      },
      "UptimeMonth": {
        "type": "object",
        "description": "One UTC month of the whole cluster, from the daily rollups and the outages that month kept.",
        "additionalProperties": false,
        "required": [
          "period",
          "nodes",
          "observed_seconds",
          "unobserved_seconds",
          "downtime_seconds",
          "unplanned_downtime_seconds",
          "planned_downtime_seconds",
          "unknown_downtime_seconds",
          "unplanned_outages",
          "flap_episodes",
          "outages_recorded"
        ],
        "properties": {
          "period": {
            "type": "string",
            "description": "`YYYY-MM`, UTC."
          },
          "nodes": {
            "type": "integer",
            "description": "Nodes with a record that month."
          },
          "observed_seconds": {
            "type": "integer",
            "description": "Node-seconds drydock saw, summed over nodes: Ready, NotReady and Unknown."
          },
          "unobserved_seconds": {
            "type": "integer"
          },
          "downtime_seconds": {
            "type": "integer",
            "description": "NotReady and Unknown node-seconds, from the daily rollups. Exact whether or not the outages were recorded."
          },
          "unplanned_downtime_seconds": {
            "type": "integer",
            "description": "Downtime in outages drydock did not cause."
          },
          "planned_downtime_seconds": {
            "type": "integer",
            "description": "Downtime in outages inside a drydock maintenance window."
          },
          "unknown_downtime_seconds": {
            "type": "integer",
            "description": "Downtime in outages drydock was not watching for, so it cannot say."
          },
          "unplanned_outages": {
            "type": "integer",
            "description": "Outages drydock did not cause, a flap episode counting as one."
          },
          "flap_episodes": {
            "type": "integer"
          },
          "outages_recorded": {
            "type": "boolean",
            "description": "False for a month recorded before outages were kept (issue 84). Its downtime is still exact, but the planned, unplanned and unknown split and the counts are zero rather than known."
          }
        }
      },
      "UptimeMonths": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "observed_at",
          "months"
        ],
        "properties": {
          "observed_at": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "months": {
            "type": "array",
            "description": "Every month the record holds, oldest first.",
            "items": {
              "$ref": "#/components/schemas/UptimeMonth"
            }
          }
        }
      },
      "UptimeNode": {
        "type": "object",
        "required": [
          "name",
          "present",
          "days",
          "events",
          "summary",
          "flapping_since",
          "last_observed_at"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string"
          },
          "present": {
            "type": "boolean",
            "description": "False for a node that no longer exists in the cluster. These are deliberately included: computing availability over only the nodes that survived excludes every node that failed badly enough to be replaced, which flatters the number in the one direction that ends an operator's trust in it. The UI must not put them in the inventory table.",
            "example": true
          },
          "days": {
            "type": "array",
            "description": "One entry per UTC day the node existed on. A missing day is a day the node did not exist — absence is not zero.",
            "items": {
              "$ref": "#/components/schemas/UptimeDay"
            }
          },
          "events": {
            "description": "The recent transitions, at whatever resolution the record still holds. Older than this the daily rollups are the resolution, which is what a ninety-day strip renders anyway.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UptimeEvent"
            }
          },
          "summary": {
            "$ref": "#/components/schemas/UptimeSummary"
          },
          "flapping_since": {
            "type": "integer",
            "nullable": true,
            "description": "The first down of a flap episode running now, in unix seconds; null unless the node is flapping right now."
          },
          "last_observed_at": {
            "type": "integer",
            "nullable": true,
            "description": "Unix seconds of the newest observation the recorder wrote for this node, across its months; null when none was ever written. Written at least every five minutes while recording runs, so a quiet node still moves it. The difference from `observed_at` is how long nothing has been recorded."
          }
        }
      },
      "UptimeSummary": {
        "description": "Totals across `covers`. Raw counts plus the attribution split, which is the whole product: 'we took it down' and 'it went down' are different numbers and only one of them is a fault.",
        "type": "object",
        "required": [
          "observed_seconds",
          "unobserved_seconds",
          "planned_downtime_seconds",
          "unplanned_downtime_seconds",
          "reboots",
          "last_reboot_at"
        ],
        "additionalProperties": false,
        "properties": {
          "observed_seconds": {
            "type": "integer",
            "minimum": 0
          },
          "unobserved_seconds": {
            "type": "integer",
            "minimum": 0
          },
          "planned_downtime_seconds": {
            "type": "integer",
            "minimum": 0,
            "description": "Downtime inside a drydock maintenance window."
          },
          "unplanned_downtime_seconds": {
            "type": "integer",
            "minimum": 0,
            "description": "Downtime outside one. This is the figure worth putting in a headline slot — raw uptime is not, because a number that goes up when nobody patches argues against the product that reports it."
          },
          "reboots": {
            "type": "integer",
            "minimum": 0
          },
          "last_reboot_at": {
            "type": "integer",
            "description": "Unix seconds of the most recent reboot, or null if none was observed. Null is not 'never rebooted' — it is 'not in the record', which for a node drydock has watched for a week means very little.",
            "nullable": true
          }
        }
      },
      "UsageAmounts": {
        "type": "object",
        "required": [
          "capacity",
          "allocatable",
          "used",
          "used_by_pods"
        ],
        "additionalProperties": false,
        "description": "Live use of one resource over a set of nodes (issue 119), as amounts rather than a share, so a reader sees the scale and the split. CPU in millicores, memory in bytes.",
        "properties": {
          "capacity": {
            "type": "integer",
            "description": "The nodes' `status.capacity`: the whole machine."
          },
          "allocatable": {
            "type": "integer",
            "description": "The nodes' `status.allocatable`: capacity less what is reserved for the system, the kubelet and the eviction threshold. The share Kubernetes gives to pods."
          },
          "used": {
            "type": "integer",
            "description": "Real use on the nodes, every process: metrics-server's node figure. On Talos it includes etcd, the kubelet and containerd, which run outside pods, so it can exceed `allocatable` while the pods are well inside it."
          },
          "used_by_pods": {
            "type": "integer",
            "description": "The part of `used` that is pods: the sum of metrics-server's pod figures for pods on these nodes. `used` less this is the use outside pods."
          }
        }
      },
      "UsageFigures": {
        "type": "object",
        "required": [
          "cpu",
          "memory"
        ],
        "additionalProperties": false,
        "properties": {
          "cpu": {
            "$ref": "#/components/schemas/UsageAmounts"
          },
          "memory": {
            "$ref": "#/components/schemas/UsageAmounts"
          }
        },
        "description": "Live use over a set of nodes, per resource (issue 119): `cpu` in millicores, `memory` in bytes."
      }
    },
    "responses": {
      "Conflict": {
        "description": "A PodDisruptionBudget would be violated. `type` is `disruption-budget`.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Crashed": {
        "description": "An unhandled crash, rescued by `wisp.rescue_crashes` and normalised into this shape on the way out. `type` is `about:blank`.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ExchangeRefused": {
        "description": "Either the identity provider did not issue a token — `type` is `exchange-failed`, usually a redirect URI the provider does not have registered, which is why the detail is worth showing — or the body is not JSON, and `type` is `about:blank`. Two types on one status because one operation can fail both ways, and the status cannot distinguish them. `type` can.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Forbidden": {
        "description": "A real answer about the signed-in user, not a failure: the apiserver refused the action as this user. `type` is `forbidden`, or `account-refused` when the account itself is disabled.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "IdentityUnavailable": {
        "description": "The identity provider or its discovery document could not be reached. `type` is `identity-unavailable`. Declared on every authenticated operation as well as on sign-in: a bearer token from an identity provider is verified against that provider on each request, so an unreachable issuer or JWKS endpoint surfaces from whichever operation it was sent to.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "retry-after": {
            "description": "Seconds to wait, per RFC 9110 section 10.2.3.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Invalid": {
        "description": "The body is JSON, and is not acceptable. `type` is `validation-failed`, and `errors` names the fields — which is what twelve separate codes never did.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "MalformedBody": {
        "description": "The request body is not JSON. `type` is `about:blank`: wisp refuses an unparseable or non-UTF-8 body before any handler runs, and the status says all of it. A body that parses but is not acceptable is 422, not this — RFC 9110 draws that line at sections 15.5.1 and 15.5.21, and this API used to blur it.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The path exists; the method does not. `type` is `about:blank`, because `allow` carries the part a client can act on.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "allow": {
            "description": "The methods this path does serve, comma separated. RFC 9110 section 15.5.6 makes this mandatory on a 405; this API used to put the same information in the message, as \"use GET\".",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource. `type` is `not-found`, which also covers a named identity provider that does not exist or cannot be used.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotImplemented": {
        "description": "This install is not configured for the thing being asked for — local sign-in, or a readable client secret. `type` is `not-configured`. A deployment fact, not a request one, so retrying will not help.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotModified": {
        "description": "The representation has not changed since the `if-none-match` validator, so there is no body. Every read offers a validator and answers this.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "etag": {
            "description": "The validator of the representation the client already has.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        }
      },
      "PaymentRequired": {
        "description": "The licence does not permit this write. `type` is `license-required`. Never returned by a read. Not 403, which would send an operator to fix permissions that are already correct — RFC 9110 section 15.5.3 calls 402 reserved, and the `type` is what actually identifies this now, so the status is kept rather than spent on a break.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Throttled": {
        "description": "The apiserver is rate limiting. `type` is `upstream-throttled`.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "retry-after": {
            "description": "Seconds to wait, per RFC 9110 section 10.2.3.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooLarge": {
        "description": "The request body exceeded the 8,000,000-byte limit. `type` is `about:blank`.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "No credential, an unusable one, or an identity the server will not accept. `type` is `unauthenticated`, or `invalid-credentials` for a username and password that do not match an account — one message for every reason, deliberately, so the API does not enumerate accounts.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "www-authenticate": {
            "description": "The challenge, per RFC 6750 section 3: `Bearer realm=\"drydock\"`, with `error=\"invalid_token\"` when a credential was sent and rejected and `error=\"invalid_request\"` when none arrived. RFC 9110 section 11.6.1 makes this mandatory on a 401 and this API omitted it.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UnknownEndpoint": {
        "description": "No such endpoint. `type` is `unknown-endpoint`, kept distinct from `not-found` because it means a client bug rather than a missing object. No path item can express this: it is the answer to a path this document does not list, so it is documented here rather than under an operation.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "The request did not declare `application/json`. `type` is `about:blank`.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "accept": {
            "description": "The media types this route does accept.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "accept-patch": {
            "description": "The media types a PATCH body may use. Sent on the four PATCH routes only, per RFC 5789 section 3.1. These take plain `application/json` and not `application/merge-patch+json`: the bodies are commands rather than patch documents, and merge-patch's meaning for `null` is not implemented.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UpstreamFailed": {
        "description": "The apiserver could not be reached, answered unexpectedly, or returned a body that did not decode. `type` is `upstream-unavailable`. The third case means the generated Kubernetes types have drifted from the cluster's API version, and after the collapse to one type that is only visible in `detail` and the log line.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UpstreamTimeout": {
        "description": "The apiserver did not respond in time. `type` is `upstream-timeout`.",
        "headers": {
          "cache-control": {
            "description": "`private, no-cache`. The answer is about one caller, and a client must revalidate before reusing it — `no-cache` rather than `no-store` precisely so it MAY keep the validator a 304 depends on.",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          "vary": {
            "description": "`authorization`. What the representation is selected by.",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The caller's own Kubernetes credential, forwarded to the apiserver so that RBAC decides every read and write. Not a session this server issues and not one it can revoke: a 403 is the apiserver's verdict on the signed-in user, which is why it is an answer and not a failure. For an identity-provider or local user the server impersonates instead of forwarding, which is invisible here and deliberate."
      }
    },
    "parameters": {
      "IfNoneMatch": {
        "name": "if-none-match",
        "in": "header",
        "required": false,
        "description": "A validator this client already holds. An unchanged representation answers 304 with no body. A comma-separated list and the weak `W/` prefix are both accepted.",
        "schema": {
          "type": "string"
        }
      }
    }
  },
  "tags": [
    {
      "name": "access",
      "description": "Granting roles to subjects."
    },
    {
      "name": "backups",
      "description": "The etcd snapshot catalogue."
    },
    {
      "name": "contract",
      "description": "This document, as the server serves it."
    },
    {
      "name": "auth",
      "description": "Signing in. Mostly unauthenticated."
    },
    {
      "name": "identity",
      "description": "Local users and identity providers."
    },
    {
      "name": "inventory",
      "description": "Nodes, as the dashboard shows them."
    },
    {
      "name": "license",
      "description": "Licence state, limits and usage."
    },
    {
      "name": "risk",
      "description": "What losing a node would displace."
    },
    {
      "name": "upgrades",
      "description": "OS upgrades across named nodes."
    },
    {
      "name": "uptime",
      "description": "Per-node availability."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ]
}
