openapi: "3.0.3"

info:
  title: PCD-K Cluster API
  version: "v1beta1"
  description: Manage Kubernetes workload clusters on Platform9 PCD-K.

servers:
  - url: "https://{du-name}-{region}.{du-domain}/oidc-proxy/{tenant}/{region}"
    variables:
      du-name:
        default: pcd
      region:
        default: r1
      du-domain:
        default: example.com
      tenant:
        default: service

security:
  - BearerAuth: []

tags:
  - name: Clusters
    description: Create and manage workload Kubernetes clusters


paths:

  /apis/cluster.x-k8s.io/v1beta1/namespaces/{namespace}/clusters:
    parameters:
      - $ref: "#/components/parameters/namespace"

    get:
      tags: [Clusters]
      operationId: listClusters
      summary: List clusters
      description: |
        Returns all clusters in the namespace. The namespace is scoped to a single
        tenant (`{du-name}-{keystone-domain}-{keystone-tenant}`), so this effectively
        lists all clusters owned by that tenant.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiVersion:
                    type: string
                    example: cluster.x-k8s.io/v1beta1
                  kind:
                    type: string
                    example: ClusterList
                  metadata:
                    type: object
                    properties:
                      resourceVersion:
                        type: string
                        description: Use for efficient re-list or watch from this point.
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Cluster"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

    post:
      tags: [Clusters]
      operationId: createCluster
      summary: Create a cluster
      description: |
        Creates a new workload Kubernetes cluster using ClusterClass topology mode.

        The cluster webhook automatically sets labels, `clusterNetwork` CIDRs, `controlPlaneEndpoint`,
        `topology.class`/`classNamespace`, do not include these in the request. Also it merges default for `apiServerFlags`,
        `kubeadmConfigFiles`, and `customSecurityGroupRules` with user provided values.

        **Class selection** (resolved from variables, no need to set explicitly):

        | `infrastructure.type` | `deploymentUnit.type` | ClusterClass used |
        |---|---|---|
        | `openstack` (default) | `saas` (default) | `openstack-default-v1` |
        | `openstack` | `onprem` | `openstack-default-v1` |
        | `byoh` | any | `byoh-default-v1` |

        **Node pool variables**: `workerNode` can be set at two levels. Set it in
        `spec.topology.variables` as a cluster-wide default (applies to all pools), and/or
        in a pool's `variables.overrides` to override for that pool specifically.
        Pool-level overrides take precedence. In `GET` responses you will see the
        cluster-level `workerNode` variable alongside pool-level overrides — both are normal.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClusterRequest"
            examples:
              openstack:
                summary: OpenStack cluster (SaaS)
                value:
                  apiVersion: cluster.x-k8s.io/v1beta1
                  kind: Cluster
                  metadata:
                    name: dev-cluster
                  spec:
                    topology:
                      version: "1.33.2"
                      workers:
                        machineDeployments:
                          - name: md-0
                            class: ost-md
                            replicas: 2
                            failureDomain: host-cluster1
                            variables:
                              overrides:
                                - name: workerNode
                                  value:
                                    flavor: m1.medium
                                    imageName: ub22-k8s-133
                      variables:
                        - name: deploymentUnit
                          value:
                            name: pcd
                            domain: example.com
                            region: r1
                        - name: network
                          value:
                            networkID: e0d4bd81-b9ca-4454-87ae-f52af394132e
                            subnetID: 86aa8162-d835-4f25-9906-4f4a9f1e4fbf
                        - name: workerNode
                          value:
                            flavor: m1.medium
                            imageName: ub22-k8s-133
              byoh:
                summary: BYOH cluster (pre-provisioned hosts)
                value:
                  apiVersion: cluster.x-k8s.io/v1beta1
                  kind: Cluster
                  metadata:
                    name: my-byoh-cluster
                  spec:
                    topology:
                      version: "1.33.2"
                      workers:
                        machineDeployments:
                          - name: md-0
                            class: byoh-md
                            replicas: 2
                      variables:
                        - name: deploymentUnit
                          value:
                            name: pcd
                            domain: example.com
                            region: r1
                        - name: infrastructure
                          value:
                            type: byoh
              multi-pool:
                summary: Multi-pool OpenStack cluster
                value:
                  apiVersion: cluster.x-k8s.io/v1beta1
                  kind: Cluster
                  metadata:
                    name: prod-cluster
                  spec:
                    topology:
                      version: "1.33.2"
                      workers:
                        machineDeployments:
                          - name: app-pool
                            class: ost-md
                            replicas: 3
                            failureDomain: host-cluster1
                            variables:
                              overrides:
                                - name: workerNode
                                  value:
                                    flavor: m1.medium
                                    imageName: ub22-k8s-133
                          - name: infra-pool
                            class: ost-md
                            replicas: 2
                            failureDomain: host-cluster1
                            variables:
                              overrides:
                                - name: workerNode
                                  value:
                                    flavor: m1.2xlarge
                                    imageName: ub22-k8s-133
                      variables:
                        - name: deploymentUnit
                          value:
                            name: pcd
                            domain: example.com
                            region: r1
                        - name: network
                          value:
                            networkID: e0d4bd81-b9ca-4454-87ae-f52af394132e
                            subnetID: 86aa8162-d835-4f25-9906-4f4a9f1e4fbf
                        - name: workerNode
                          value:
                            flavor: m1.medium
                            imageName: ub22-k8s-133
              onprem-openstack:
                summary: On-prem OpenStack cluster
                value:
                  apiVersion: cluster.x-k8s.io/v1beta1
                  kind: Cluster
                  metadata:
                    name: dev-cluster
                  spec:
                    topology:
                      version: "1.33.2"
                      workers:
                        machineDeployments:
                          - name: md-0
                            class: ost-md
                            replicas: 2
                            failureDomain: host-cluster1
                            variables:
                              overrides:
                                - name: workerNode
                                  value:
                                    flavor: m1.medium
                                    imageName: ub22-k8s-133
                      variables:
                        - name: deploymentUnit
                          value:
                            name: pcd
                            domain: example.com
                            region: r1
                            type: onprem
                            vip: "10.0.1.100"
                        - name: network
                          value:
                            networkID: e0d4bd81-b9ca-4454-87ae-f52af394132e
                            subnetID: 86aa8162-d835-4f25-9906-4f4a9f1e4fbf
                        - name: workerNode
                          value:
                            flavor: m1.medium
                            imageName: ub22-k8s-133
      responses:
        "201":
          description: Created — provisioning is asynchronous.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Cluster"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }

  /apis/cluster.x-k8s.io/v1beta1/namespaces/{namespace}/clusters/{name}:
    parameters:
      - $ref: "#/components/parameters/namespace"
      - $ref: "#/components/parameters/clusterName"

    get:
      tags: [Clusters]
      operationId: getCluster
      summary: Get a cluster
      description: |
        Returns the full Cluster object including status.

        | `status.phase` | Description |
        |---|---|
        | `Provisioning` | Control plane and/or workers are being created |
        | `Provisioned` | Cluster is ready — control plane and all worker pools are up |
        | `Deleting` | Delete is in progress |
        | `Failed` | Provisioning failed — check `status.failureMessage` |
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Cluster"
              example:
                apiVersion: cluster.x-k8s.io/v1beta1
                kind: Cluster
                metadata:
                  name: dev-cluster
                  namespace: pcd-default-service
                  uid: 00000000-0000-0000-0000-000000000001
                  resourceVersion: "100000"
                  creationTimestamp: "2024-01-01T10:00:00Z"
                  finalizers:
                  - cluster.cluster.x-k8s.io
                  labels:
                    cas-capi-version: v1.33
                    core-addons: enabled
                    kaapi.pf9.io/infra-addon: openstack
                    pcd-kaapi.pf9.io/region: r1
                    proxy-addons: enabled
                spec:
                  controlPlaneEndpoint:
                    host: dev-cluster.default-service.k8s.pcd.example.com
                    port: 443
                  topology:
                    class: openstack-default-v1
                    classNamespace: kaapi
                    version: v1.33.2
                    variables:
                    - name: deploymentUnit
                      value:
                        name: pcd
                        domain: example.com
                        region: r1
                        keystoneDomain: default
                        keystoneTenant: service
                        type: saas
                        vip: ""
                    - name: network
                      value:
                        networkID: e0d4bd81-b9ca-4454-87ae-f52af394132e
                        subnetID: 86aa8162-d835-4f25-9906-4f4a9f1e4fbf
                    - name: workerNode
                      value:
                        flavor: m1.medium
                        imageName: ub22-k8s-133
                    - name: apiServerFlags
                      value:
                      - --advertise-address=10.96.0.40
                      - --authentication-config=/etc/kubernetes/oidc-auth/oidc.yaml
                    workers:
                      machineDeployments:
                      - name: md-0
                        class: ost-md
                        replicas: 2
                        failureDomain: host-cluster1
                        variables:
                          overrides:
                          - name: workerNode
                            value:
                              flavor: m1.medium
                              imageName: ub22-k8s-133
                          - name: network
                            value:
                              networkID: e0d4bd81-b9ca-4454-87ae-f52af394132e
                              subnetID: 86aa8162-d835-4f25-9906-4f4a9f1e4fbf
                status:
                  phase: Provisioned
                  controlPlaneReady: true
                  infrastructureReady: true
                  failureDomains:
                    host-cluster1:
                      controlPlane: true
                  conditions:
                  - type: Ready
                    status: "True"
                    lastTransitionTime: "2024-01-01T10:05:00Z"
                  - type: ControlPlaneInitialized
                    status: "True"
                    lastTransitionTime: "2024-01-01T10:04:30Z"
                  - type: ControlPlaneReady
                    status: "True"
                    lastTransitionTime: "2024-01-01T10:04:00Z"
                  - type: InfrastructureReady
                    status: "True"
                    lastTransitionTime: "2024-01-01T10:03:00Z"
                  - type: TopologyReconciled
                    status: "True"
                    lastTransitionTime: "2024-01-01T10:01:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      tags: [Clusters]
      operationId: patchCluster
      summary: Update a cluster
      parameters:
        - $ref: "#/components/parameters/clusterName"
        - name: fieldManager
          in: query
          required: false
          description: >
            Name of the field manager. Required when using `application/apply-patch+yaml`
            (Server-Side Apply) — any non-empty string is valid (e.g. `kubectl`, `my-app`).
            Not needed for `application/json-patch+json`.
          schema:
            type: string
            example: kubectl
      description: |
        Two content types are supported depending on the operation:

        **`application/json-patch+json`** — for node pool (MachineDeployment) operations.
        `spec.topology.workers.machineDeployments` is an array with no server-side name-keying
        for this patch type, so elements must be addressed by their zero-based index.
        Do a GET first to find the position of the pool you want to modify.

        **`application/apply-patch+yaml`** (Server-Side Apply) — for named topology variables
        such as `apiServerFlags` .

        **Autoscaling**: to enable, remove the `replicas` field (so the autoscaler controls it)
        and add both min/max annotations in the same request. To disable, restore a fixed
        `replicas` value and remove both annotations.

        Do not patch `spec.topology.version` directly.
      requestBody:
        required: true
        content:
          application/json-patch+json:
            schema:
              type: array
              items:
                type: object
                required: [op, path]
                properties:
                  op:
                    type: string
                    enum: [add, replace, remove]
                  path:
                    type: string
                  value: {}
            examples:
              scale-pool:
                summary: Scale node pool at index 0 to 4 replicas
                value:
                  - op: replace
                    path: /spec/topology/workers/machineDeployments/0/replicas
                    value: 4

              add-pool:
                summary: Add a new node pool
                value:
                  - op: add
                    path: /spec/topology/workers/machineDeployments/-
                    value:
                      name: gpu-pool
                      class: ost-md
                      replicas: 2
                      variables:
                        overrides:
                          - name: workerNode
                            value:
                              flavor: m1.gpu
                              imageName: ub22-k8s-133-gpu

              remove-pool:
                summary: Remove node pool at index 1 (do a GET first to confirm the index)
                value:
                  - op: remove
                    path: /spec/topology/workers/machineDeployments/1

              enable-autoscaler:
                summary: Enable autoscaler on pool at index 0 (remove fixed replicas, set min/max)
                value:
                  - op: remove
                    path: /spec/topology/workers/machineDeployments/0/replicas
                  - op: add
                    path: /spec/topology/workers/machineDeployments/0/metadata/annotations
                    value:
                      cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "2"
                      cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "6"

              update-autoscaler-bounds:
                summary: Update min/max bounds on a pool that already has autoscaling enabled
                value:
                  - op: replace
                    path: /spec/topology/workers/machineDeployments/0/metadata/annotations/cluster.x-k8s.io~1cluster-api-autoscaler-node-group-min-size
                    value: "3"
                  - op: replace
                    path: /spec/topology/workers/machineDeployments/0/metadata/annotations/cluster.x-k8s.io~1cluster-api-autoscaler-node-group-max-size
                    value: "10"

              disable-autoscaler:
                summary: Disable autoscaler on pool at index 0 (restore fixed replicas, remove annotations)
                value:
                  - op: remove
                    path: /spec/topology/workers/machineDeployments/0/metadata/annotations/cluster.x-k8s.io~1cluster-api-autoscaler-node-group-min-size
                  - op: remove
                    path: /spec/topology/workers/machineDeployments/0/metadata/annotations/cluster.x-k8s.io~1cluster-api-autoscaler-node-group-max-size
                  - op: replace
                    path: /spec/topology/workers/machineDeployments/0/replicas
                    value: 3

              update-rolling-strategy:
                summary: Set rolling update strategy on pool at index 0
                value:
                  - op: add
                    path: /spec/topology/workers/machineDeployments/0/strategy
                    value:
                      type: RollingUpdate
                      rollingUpdate:
                        maxSurge: 1
                        maxUnavailable: 0

          application/apply-patch+yaml:
            schema:
              type: object
            examples:
              update-api-server-flags:
                summary: >
                  Set extra API server flags using Server-Side Apply (requires ?fieldManager=<name> query param).
                  Only the named variable you include is updated — all other variables are left untouched.
                value:
                  apiVersion: cluster.x-k8s.io/v1beta1
                  kind: Cluster
                  metadata:
                    name: dev-cluster
                    namespace: pcd-default-service
                  spec:
                    topology:
                      variables:
                        - name: apiServerFlags
                          value:
                            - "--audit-log-maxsize=100"
                            - "--audit-log-maxbackup=5"
      responses:
        "200":
          description: Patch applied — changes take effect asynchronously.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Cluster"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }

    delete:
      tags: [Clusters]
      operationId: deleteCluster
      summary: Delete a cluster
      description: |
        Deletes the cluster and all owned resources (control plane, workers, OpenStack infrastructure).
        Asynchronous — cluster transitions to `status.phase == "Deleting"` and is removed when complete.
        Typical deletion time is 2–5 minutes.

        **BYOH clusters**: physical host nodes are not deprovisioned. The cluster label is removed
        from the hosts, which return to the unallocated pool and can be reused by another cluster.
      responses:
        "200":
          description: |
            Deletion accepted. Returns the Cluster object with `metadata.deletionTimestamp` set.
            The cluster is fully removed once `status.phase` disappears (resource deleted).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Cluster"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Dex-issued JWT token.

  parameters:
    namespace:
      name: namespace
      in: path
      required: true
      schema:
        type: string
        example: pcd-default-service
      description: "Tenant namespace. Pattern: `{du-name}-{keystone-domain}-{keystone-tenant}`"
    clusterName:
      name: name
      in: path
      required: true
      schema:
        type: string
        example: dev-cluster

  responses:
    Unauthorized:
      description: Missing or invalid JWT token.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Status"
          example:
            apiVersion: v1
            kind: Status
            metadata: {}
            status: Failure
            message: "Unauthorized"
            reason: Unauthorized
            code: 401
    Forbidden:
      description: Token does not have permission for this operation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Status"
          example:
            apiVersion: v1
            kind: Status
            metadata: {}
            status: Failure
            message: "clusters.cluster.x-k8s.io is forbidden: User \"user@example.com\" cannot create resource"
            reason: Forbidden
            code: 403
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Status"
          example:
            apiVersion: v1
            kind: Status
            metadata: {}
            status: Failure
            message: "clusters.cluster.x-k8s.io \"dev-cluster\" not found"
            reason: NotFound
            details:
              name: dev-cluster
              group: cluster.x-k8s.io
              kind: clusters
            code: 404
    BadRequest:
      description: Malformed request body.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Status"
          example:
            apiVersion: v1
            kind: Status
            metadata: {}
            status: Failure
            message: "the body of the request was in an unknown format"
            reason: BadRequest
            code: 400
    Conflict:
      description: A cluster with this name already exists.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Status"
          example:
            apiVersion: v1
            kind: Status
            metadata: {}
            status: Failure
            message: "clusters.cluster.x-k8s.io \"dev-cluster\" already exists"
            reason: AlreadyExists
            details:
              name: dev-cluster
              group: cluster.x-k8s.io
              kind: clusters
            code: 409
    UnprocessableEntity:
      description: Admission webhook rejected the request due to a validation failure.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Status"
          example:
            apiVersion: v1
            kind: Status
            metadata: {}
            status: Failure
            message: "Cluster.cluster.x-k8s.io \"dev-cluster\" is invalid: spec.topology.variables: Required value"
            reason: Invalid
            details:
              name: dev-cluster
              group: cluster.x-k8s.io
              kind: Cluster
              causes:
                - reason: FieldValueRequired
                  message: Required value
                  field: spec.topology.variables
            code: 422

  schemas:

    Status:
      type: object
      properties:
        apiVersion:
          type: string
          example: v1
        kind:
          type: string
          example: Status
        metadata:
          type: object
          description: Always present; typically empty (`{}`).
        status:
          type: string
          enum: [Success, Failure]
        message:
          type: string
          description: Human-readable description of the error.
          example: text
        reason:
          type: string
          description: >
            Machine-readable reason. Common values: `NotFound`, `AlreadyExists`,
            `BadRequest`, `Unauthorized`, `Forbidden`, `Invalid`.
          example: NotFound
        details:
          type: object
          description: >
            Additional error context. Present on `NotFound` (includes `name`, `group`,
            `kind`) and `Invalid` / `422` responses (includes `causes` with per-field errors).
          properties:
            name:
              type: string
              description: Name of the resource involved.
            group:
              type: string
              description: API group of the resource (e.g. `cluster.x-k8s.io`).
            kind:
              type: string
              description: Resource kind (e.g. `clusters`).
            causes:
              type: array
              description: Per-field validation errors (present on `422` responses).
              items:
                type: object
                properties:
                  reason:
                    type: string
                    example: FieldValueRequired
                  message:
                    type: string
                    example: Required value
                  field:
                    type: string
                    example: spec.topology.variables
        code:
          type: integer
          description: HTTP status code.
          example: 404

    ClusterRequest:
      type: object
      required: [apiVersion, kind, metadata, spec]
      properties:
        apiVersion:
          type: string
          enum: [cluster.x-k8s.io/v1beta1]
        kind:
          type: string
          enum: [Cluster]
        metadata:
          type: object
          required: [name]
          properties:
            name:
              type: string
              example: dev-cluster
        spec:
          type: object
          required: [topology]
          properties:
            controlPlaneEndpoint:
              type: object
              description: |
                API server endpoint — webhook-generated from `deploymentUnit` variables, not set by the user.
                Format: `<cluster-name>.<keystone-domain>-<keystone-tenant>.k8s.<du-name>.<du-domain>`.
                Use this host:port to reach the workload cluster's Kubernetes API.
              properties:
                host:
                  type: string
                  example: dev-cluster.default-service.k8s.pcd.example.com
                port:
                  type: integer
                  example: 443
            topology:
              type: object
              required: [version, workers, variables]
              properties:
                version:
                  type: string
                  description: Kubernetes version (no `v` prefix).
                  example: "1.33.2"
                class:
                  type: string
                  enum: [openstack-default-v1, byoh-default-v1]
                  description: |
                    Webhook-resolved from `infrastructure.type` and `deploymentUnit.type` — omit this field.
                    Set `infrastructure.type: byoh` in variables to use `byoh-default-v1`; otherwise `openstack-default-v1` is used.
                classNamespace:
                  type: string
                  enum: [kaapi]
                  description: Webhook-set to `kaapi` — omit this field.
                workers:
                  type: object
                  properties:
                    machineDeployments:
                      type: array
                      items:
                        $ref: "#/components/schemas/MachineDeployment"
                variables:
                  type: array
                  description: |
                    Each entry is `{ name: <variable>, value: <object> }`.
                    The webhook manages `kubeadmConfigFiles`, `customSecurityGroupRules`, and
                    system-required `apiServerFlags` automatically — do not set these.

                    | Variable | Schema | Required | Notes |
                    |---|---|---|---|
                    | `deploymentUnit` | `DeploymentUnitValue` | Always | `name`, `domain`, `region` are required fields |
                    | `network` | `NetworkValue` | OpenStack only | `networkID`, `subnetID` UUIDs |
                    | `infrastructure` | `InfrastructureValue` | Only for BYOH | Omit entirely for OpenStack |
                    | `workerNode` | `WorkerNodeValue` | OpenStack per-pool override | `flavor`, `imageName` required |
                    | `rootVolume` | `RootVolumeValue` | Optional, OpenStack | Boot from Cinder volume |
                    | `apiServerFlags` | string array | Optional | Merged with system defaults |
                    | `controllerManagerFlags` | string array | Optional | — |
                    | `schedulerFlags` | string array | Optional | — |
                    | `enableSSH` | boolean | Optional, OpenStack | Adds SSH security group rule |
                    | `allowedAddressPairs` | string array | Optional, OpenStack | CIDRs for allowed address pairs |
                  items:
                    type: object
                    required: [name, value]
                    properties:
                      name:
                        type: string
                      value: {}

    Cluster:
      allOf:
        - $ref: "#/components/schemas/ClusterRequest"
        - type: object
          properties:
            metadata:
              type: object
              properties:
                namespace:
                  type: string
                  description: Tenant namespace the cluster belongs to.
                  example: pcd-default-service
                uid:
                  type: string
                  format: uuid
                resourceVersion:
                  type: string
                creationTimestamp:
                  type: string
                  format: date-time
                deletionTimestamp:
                  type: string
                  format: date-time
                  description: Set when deletion is in progress. Present until all finalizers are removed.
                labels:
                  type: object
                  additionalProperties:
                    type: string
                  description: Labels set by the webhook and controllers. Do not rely on specific values for business logic.
                finalizers:
                  type: array
                  items:
                    type: string
                  description: CAPI finalizers preventing premature deletion.
            status:
              type: object
              properties:
                phase:
                  type: string
                  enum: [Provisioning, Provisioned, Deleting, Failed]
                controlPlaneReady:
                  type: boolean
                infrastructureReady:
                  type: boolean
                failureMessage:
                  type: string
                  description: Human-readable error message when `phase` is `Failed`.
                failureDomains:
                  type: object
                  description: |
                    Map of OpenStack availability zone names to their control-plane eligibility.
                    Populated from the `OpenStackCluster` status after infrastructure is ready.
                  additionalProperties:
                    type: object
                    properties:
                      controlPlane:
                        type: boolean
                conditions:
                  type: array
                  description: |
                    Standard CAPI conditions. Key `type` values to watch:
                    `Ready`, `ControlPlaneInitialized`, `ControlPlaneReady`,
                    `InfrastructureReady`, `TopologyReconciled`.
                    All conditions reach `status: "True"` when the cluster is fully provisioned.
                  items:
                    type: object
                    properties:
                      type:
                        type: string
                        example: Ready
                      status:
                        type: string
                        enum: ["True", "False", Unknown]
                      reason:
                        type: string
                      message:
                        type: string
                      lastTransitionTime:
                        type: string
                        format: date-time

    MachineDeployment:
      type: object
      required: [name, class]
      properties:
        name:
          type: string
          description: Pool name — must be unique within the cluster.
          example: md-0
        class:
          type: string
          enum: [ost-md, byoh-md]
          description: "`ost-md` for OpenStack worker pools; `byoh-md` for BYOH pools."
        replicas:
          type: integer
          minimum: 0
          description: Fixed replica count. Omit when enabling cluster autoscaler.
          example: 2
        failureDomain:
          type: string
          description: Virtualized cluster name
          example: host-cluster1
        metadata:
          type: object
          properties:
            annotations:
              type: object
              additionalProperties:
                type: string
              description: |
                Autoscaler annotations — set both to enable, null both to disable:
                - `cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size`
                - `cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size`
        variables:
          type: object
          description: |
            Per-pool variable overrides. OpenStack pools use `workerNode` (and optionally
            `rootVolume`) here. BYOH pools do not use overrides.
          properties:
            overrides:
              type: array
              items:
                type: object
                required: [name, value]
                properties:
                  name:
                    type: string
                    enum: [workerNode, rootVolume, network]
                  value: {}

