{
  "description": "MiroirVolume is one provisioned volume (1:1 with a PV).",
  "properties": {
    "apiVersion": {
      "description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
      "type": [
        "string",
        "null"
      ]
    },
    "kind": {
      "description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
      "type": [
        "string",
        "null"
      ]
    },
    "metadata": {
      "type": [
        "object",
        "null"
      ]
    },
    "spec": {
      "additionalProperties": false,
      "description": "MiroirVolumeSpec is the desired state, written by the controller at\nCreateVolume time and reconciled by node agents. Replicas may be edited\non a live replicated volume — add an entry (node + backend; the\nmembership reconciler completes it) or remove one — within the bounds\nthe validation rule pins: an unreplicated volume cannot grow a\nreplication layer in place (internal DRBD metadata cannot be added\nunder a live filesystem), and a replicated one keeps 2–3 replicas.",
      "properties": {
        "allowRemoteAccess": {
          "description": "AllowRemoteAccess permits pods on nodes without a replica: the PV\ncarries no node affinity, and staging on a non-replica node attaches\nan ephemeral diskless client leg (spec.clients). From the\nStorageClass parameter; replicated volumes only.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "clients": {
          "description": "Clients are ephemeral diskless consumer legs, added at stage time on\nnon-replica nodes and removed at unstage. Bounded small: RWO means\none consumer, plus headroom for an attach/detach overlap during a\npod move.",
          "items": {
            "additionalProperties": false,
            "description": "VolumeClient is an ephemeral diskless consumer leg: a DRBD peer with no\nbacking device on a node that runs a pod but holds no replica. All its\nI/O crosses the replication network. Added by the CSI node service at\nstage time (spec.allowRemoteAccess volumes only), completed by the\nmembership reconciler like a replica, removed at unstage. Unlike a\ndiskless tie-breaker it is not placed for quorum, and it is rendered\nwith \"tiebreaker no\" so DRBD never counts its vote — attach/detach\nleaves the majority threshold alone (see README).",
            "properties": {
              "addedAt": {
                "description": "AddedAt is when the CSI node service attached this leg. The\nauto-diskful reconciler keys its conversion threshold on it.",
                "format": "date-time",
                "type": [
                  "string",
                  "null"
                ]
              },
              "address": {
                "description": "Address is the replication endpoint, resolved like a replica's\n(node-map override, else the Node's InternalIP).",
                "type": [
                  "string",
                  "null"
                ]
              },
              "node": {
                "description": "Node is the Kubernetes node name consuming the volume remotely.",
                "type": "string"
              },
              "nodeID": {
                "description": "NodeID is the DRBD node id, unique across replicas and clients,\nassigned by the membership reconciler.",
                "format": "int32",
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "node"
            ],
            "type": "object"
          },
          "maxItems": 2,
          "type": [
            "array",
            "null"
          ]
        },
        "drbd": {
          "additionalProperties": false,
          "description": "DRBD is set by the controller when len(Replicas) \u003e 1.",
          "properties": {
            "bitmapGranularityBytes": {
              "description": "BitmapGranularityBytes is the DRBD bitmap block size handed to\ncreate-md (--bitmap-block-size): each dirty bit tracks this many\nbytes, so coarser bits cut bitmap RAM proportionally at the cost of\nresyncing more per dirty bit. Applied only when a replica's metadata\nis first created — replicas created earlier keep their granularity\n(DRBD exchanges bitmaps across differing block sizes). 0 means the\nDRBD default (4096). Needs kmod ≥ 9.3.0 (the agent's startup floor).",
              "enum": [
                0,
                4096,
                8192,
                16384,
                32768,
                65536,
                131072,
                262144,
                524288,
                1048576
              ],
              "format": "int64",
              "type": [
                "integer",
                "null"
              ]
            },
            "port": {
              "description": "Port is the TCP port for this resource's replication links,\nallocated cluster-wide by the controller at creation.",
              "format": "int32",
              "maximum": 65535,
              "minimum": 1024,
              "type": "integer"
            },
            "sharedSecret": {
              "description": "SharedSecret authenticates peers (cram-hmac-alg challenge-response):\nwithout it, anything that can reach the replication port can join the\nresource. Generated by the controller at creation; both agents render\nthe same value. DRBD caps secrets at 64 characters.",
              "maxLength": 64,
              "type": [
                "string",
                "null"
              ]
            }
          },
          "required": [
            "port"
          ],
          "type": [
            "object",
            "null"
          ]
        },
        "export": {
          "additionalProperties": false,
          "description": "Export, if set, serves the volume as a shared filesystem over NFS\n(RWX). Set by the controller when the volume is created from a\nMULTI_NODE access mode; immutable.",
          "properties": {
            "fsType": {
              "description": "FSType is the filesystem the gateway formats and mounts on the\ndevice. Persisted here because mkfs happens in the gateway, which\nnever sees a CSI VolumeCapability; consumers only ever see NFS.",
              "enum": [
                "ext4",
                "xfs"
              ],
              "type": "string"
            }
          },
          "required": [
            "fsType"
          ],
          "type": [
            "object",
            "null"
          ]
        },
        "quorumPolicy": {
          "description": "QuorumPolicy applies only when len(Replicas) \u003e 1.",
          "enum": [
            "last-man-standing",
            "freeze"
          ],
          "type": [
            "string",
            "null"
          ]
        },
        "replicas": {
          "description": "Replicas lists the placement of the volume: one entry for local\nvolumes, two or more for DRBD-replicated ones. MaxItems matches the\nCEL size rule and bounds the apiserver's CEL cost estimate — the\ndiskless transition rule is nested over this list and exceeds the\nrule budget on an unbounded array.",
          "items": {
            "additionalProperties": false,
            "description": "Replica is one placement of the volume's data — a DRBD peer when the\nvolume is replicated.",
            "properties": {
              "address": {
                "description": "Address is the replication endpoint (host IP), from the node map.\nOnly set on replicated volumes.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "backend": {
                "description": "Backend selects how the backing device is provisioned on this node.\nUnused for diskless tie-breaker replicas (they have no backing device).",
                "enum": [
                  "lvmthin",
                  "zfs",
                  "loopfile"
                ],
                "type": [
                  "string",
                  "null"
                ]
              },
              "diskless": {
                "description": "Diskless makes this a quorum-only tie-breaker: DRBD joins for\nvoting but stores no data. No backend device, never a mount\ntarget. The agent skips backend create/resize/delete and DRBD\ncreate-md; the resource renders \"disk none\" for this peer.",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "fullSync": {
                "description": "FullSync marks a replica added after volume creation: its agent\nskips the day0 GI seed so DRBD full-syncs it from the existing\npeers instead of treating it as a pristine twin. Set by the\nmembership reconciler when it completes the entry.",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "node": {
                "description": "Node is the Kubernetes node name hosting this replica.",
                "type": "string"
              },
              "nodeID": {
                "description": "NodeID is the DRBD node id, assigned by the controller at creation\nand stable for the volume's lifetime. Only set on replicated volumes.",
                "format": "int32",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "pool": {
                "description": "Pool names the node-map storage pool holding this replica's backing\ndevice. Persisted like Backend so the volume survives later topology\nedits. Empty means the default pool (pre-multi-pool volumes are\nadopted this way). Unused for diskless replicas.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "node"
            ],
            "type": "object"
          },
          "maxItems": 3,
          "minItems": 1,
          "type": "array"
        },
        "sizeBytes": {
          "description": "SizeBytes is the provisioned (virtual, thin) size of the volume.\nThe transition rule pins it monotonic: nothing downstream supports\nshrinking — agents only grow the backing device and the filesystem,\nand a shrunk spec would desync DRBD metadata from the device.",
          "format": "int64",
          "minimum": 1,
          "type": "integer",
          "x-kubernetes-validations": [
            {
              "message": "a volume cannot shrink in place: agents only grow the backing device and filesystem; restore a snapshot into a new, smaller volume instead",
              "rule": "self \u003e= oldSelf"
            }
          ]
        },
        "source": {
          "additionalProperties": false,
          "description": "Source, if set, provisions content from a snapshot (CoW clone).",
          "properties": {
            "snapshotName": {
              "description": "SnapshotName references a MiroirSnapshot by name.",
              "type": "string"
            }
          },
          "required": [
            "snapshotName"
          ],
          "type": [
            "object",
            "null"
          ]
        }
      },
      "required": [
        "replicas",
        "sizeBytes"
      ],
      "type": "object",
      "x-kubernetes-validations": [
        {
          "message": "replicated volumes (spec.drbd set) need 2-3 replicas; unreplicated exactly 1",
          "rule": "has(self.drbd) ? (size(self.replicas) \u003e= 2 \u0026\u0026 size(self.replicas) \u003c= 3) : size(self.replicas) == 1"
        },
        {
          "message": "replicated volumes need at least 2 diskful (non-diskless) replicas",
          "rule": "has(self.drbd) ? size(self.replicas.filter(r, !has(r.diskless) || !r.diskless)) \u003e= 2 : true"
        },
        {
          "message": "diskless replicas are only valid on replicated volumes",
          "rule": "!has(self.drbd) ? !self.replicas.exists(r, has(r.diskless) \u0026\u0026 r.diskless) : true"
        },
        {
          "message": "the first replica must be diskful (not a diskless tie-breaker)",
          "rule": "size(self.replicas) \u003e 0 ? !has(self.replicas[0].diskless) || !self.replicas[0].diskless : true"
        },
        {
          "message": "a diskful replica cannot become diskless in place; remove the replica and re-add it instead (diskless→diskful is allowed: the agent attaches a disk to the live leg)",
          "rule": "self.replicas.all(r, oldSelf.replicas.all(o, o.node != r.node || (has(o.diskless) \u0026\u0026 o.diskless) || !(has(r.diskless) \u0026\u0026 r.diskless)))"
        },
        {
          "message": "a completed replica's pool is immutable: the agent would realize a fresh device in the new pool and abandon the old one; remove the replica and re-add it in the target pool instead",
          "rule": "self.replicas.all(r, oldSelf.replicas.all(o, o.node != r.node || (has(o.diskless) \u0026\u0026 o.diskless) || !has(o.address) || (has(o.pool) ? o.pool : 'default') == (has(r.pool) ? r.pool : 'default')))"
        },
        {
          "message": "all diskful replicas must live in one pool: snapshots and restores are pool-local, so a mixed-pool volume's snapshots would be unrestorable",
          "rule": "self.replicas.all(r, (has(r.diskless) \u0026\u0026 r.diskless) || !has(r.address) || self.replicas.all(o, (has(o.diskless) \u0026\u0026 o.diskless) || !has(o.address) || (has(r.pool) ? r.pool : 'default') == (has(o.pool) ? o.pool : 'default')))"
        },
        {
          "message": "a volume cannot gain or lose its replication layer in place",
          "rule": "has(self.drbd) == has(oldSelf.drbd)"
        },
        {
          "message": "client legs are only valid on replicated volumes",
          "rule": "!has(self.clients) || has(self.drbd)"
        },
        {
          "message": "a client leg cannot share a node with a replica",
          "rule": "!has(self.clients) || self.clients.all(c, !self.replicas.exists(r, r.node == c.node))"
        },
        {
          "message": "an NFS-exported (RWX) volume is consumed over NFS and cannot have DRBD client legs",
          "rule": "!has(self.clients) || !has(self.export)"
        },
        {
          "message": "a volume cannot gain or lose its NFS export (RWX) in place",
          "rule": "has(self.export) == has(oldSelf.export)"
        },
        {
          "message": "replicated RWX volumes must use freeze quorum (a rescheduled gateway under last-man-standing risks dual-primary split-brain)",
          "rule": "!(has(self.export) \u0026\u0026 has(self.drbd)) || self.quorumPolicy == 'freeze'"
        },
        {
          "message": "replica nodes must be unique: a node holds at most one leg of a volume",
          "rule": "self.replicas.all(r, self.replicas.exists_one(o, o.node == r.node))"
        },
        {
          "message": "client-leg nodes must be unique",
          "rule": "!has(self.clients) || self.clients.all(c, self.clients.exists_one(o, o.node == c.node))"
        },
        {
          "message": "drbd.port is immutable: it was allocated cluster-wide at creation and the allocator assumes existing volumes keep their ports",
          "rule": "!has(self.drbd) || !has(oldSelf.drbd) || self.drbd.port == oldSelf.drbd.port"
        },
        {
          "message": "source is immutable: it records the snapshot this volume was cloned from",
          "rule": "has(self.source) == has(oldSelf.source) \u0026\u0026 (!has(self.source) || !has(oldSelf.source) || self.source.snapshotName == oldSelf.source.snapshotName)"
        },
        {
          "message": "export.fsType is immutable: the gateway formatted the volume with it at first start",
          "rule": "!has(self.export) || !has(oldSelf.export) || self.export.fsType == oldSelf.export.fsType"
        }
      ]
    },
    "status": {
      "additionalProperties": false,
      "description": "MiroirVolumeStatus is the observed state aggregated from node agents.",
      "properties": {
        "activated": {
          "description": "Activated is set the first time any node successfully stages the\nvolume for a consumer (filesystem or raw block). It latches \"this\nvolume may hold data\": split-brain auto-recovery, which discards one\nleg's generation, runs only while this is false — on an activated\nvolume divergence is never auto-resolved. Broader than Formatted,\nwhich a raw-block volume never sets.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "conditions": {
          "description": "Conditions follow the standard Kubernetes condition conventions.",
          "items": {
            "additionalProperties": false,
            "description": "Condition contains details for one aspect of the current state of this API Resource.",
            "properties": {
              "lastTransitionTime": {
                "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.",
                "format": "date-time",
                "type": "string"
              },
              "message": {
                "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.",
                "maxLength": 32768,
                "type": "string"
              },
              "observedGeneration": {
                "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.",
                "format": "int64",
                "minimum": 0,
                "type": [
                  "integer",
                  "null"
                ]
              },
              "reason": {
                "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.",
                "maxLength": 1024,
                "minLength": 1,
                "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$",
                "type": "string"
              },
              "status": {
                "description": "status of the condition, one of True, False, Unknown.",
                "enum": [
                  "True",
                  "False",
                  "Unknown"
                ],
                "type": "string"
              },
              "type": {
                "description": "type of condition in CamelCase or in foo.example.com/CamelCase.",
                "maxLength": 316,
                "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$",
                "type": "string"
              }
            },
            "required": [
              "lastTransitionTime",
              "message",
              "reason",
              "status",
              "type"
            ],
            "type": "object"
          },
          "type": [
            "array",
            "null"
          ]
        },
        "export": {
          "additionalProperties": false,
          "description": "Export is the observed state of the NFS gateway, set only on RWX\nvolumes (spec.export). Written by the export reconciler.",
          "properties": {
            "address": {
              "description": "Address is the share Service's ClusterIP. Consumers mount\n\u003caddress\u003e:/\u003cvolume\u003e over NFS. Empty until the reconciler has\ncreated the Service and read back its allocated ClusterIP.",
              "type": [
                "string",
                "null"
              ]
            }
          },
          "type": [
            "object",
            "null"
          ]
        },
        "formatted": {
          "description": "Formatted is set once the volume has ever carried a filesystem —\nfirst mkfs at stage time, or inherited from the snapshot source on\nrestore. A blank device on a formatted volume is data loss: the\nnode refuses to mkfs again rather than silently wipe it.",
          "type": [
            "boolean",
            "null"
          ]
        },
        "perNode": {
          "additionalProperties": {
            "additionalProperties": false,
            "description": "ReplicaStatus is the per-node observed state, written by that node's agent.",
            "properties": {
              "connected": {
                "description": "Connected is true when this node's replication links to every\ndiskful peer are established. A diskless tie-breaker's link state\nis deliberately excluded: it holds no data leg, so its downtime\nmust not read as degraded replication (it would block snapshots\nand replica removal). Always true for unreplicated volumes.",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "deviceCreated": {
                "description": "DeviceCreated is true once the backing device exists on the node.",
                "type": "boolean"
              },
              "devicePath": {
                "description": "DevicePath is the node-local path pods attach to (backing device for\nreplicas:1; /dev/drbd\u003cminor\u003e when replicated).",
                "type": [
                  "string",
                  "null"
                ]
              },
              "discardGranularityBytes": {
                "description": "DiscardGranularityBytes is the discard granularity this diskful\nleg probed from its backing device (0: unsupported or unprobed).\nClient legs advertise the max over the diskful legs' values, so\ntrims from remote consumers align with the real backings instead\nof the 512-byte default DRBD assumes for diskless devices.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "diskFailed": {
                "description": "DiskFailed latches a diskful leg DRBD detached after a backing I/O\nerror (on-io-error detach): the agent renders `adjust --skip-disk`\nso the next reconcile does not re-attach the failing disk and\nre-trigger the error. Sticky until the replica is removed and\nre-added (a fresh backing clears it). Serving continues via the peer.",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "diskState": {
                "description": "DiskState is the DRBD disk state on this node (UpToDate,\nInconsistent, Outdated, Diskless, …). Empty for unreplicated volumes.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "diskless": {
                "description": "Diskless records that this node's replica is a quorum-only\ntie-breaker. Self-reported by the agent so removal handling still\nknows after the entry has left spec.replicas.",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "drbdMinor": {
                "description": "DRBDMinor is the DRBD device minor assigned locally by the agent.\nZero means unassigned (fresh volume or unreplicated).",
                "format": "int32",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "lastVerifyOutOfSyncBytes": {
                "description": "LastVerifyOutOfSyncBytes is the out-of-sync total the last verify\nfound (0 = clean). Findings need a disconnect/connect cycle to resync;\nthey persist in the out-of-sync metric until then. A pointer so a\nclean verify (0) reads differently from \"never verified\" (nil).",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "lastVerifyTime": {
                "description": "LastVerifyTime is when the last scheduled online verify completed for\nthis volume. Only the coordinator (first diskful replica) initiates a\nverify, so only its slot carries this.",
                "format": "date-time",
                "type": [
                  "string",
                  "null"
                ]
              },
              "message": {
                "description": "Message carries the last reconcile error, if any.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "pool": {
                "description": "Pool records which storage pool holds this leg's backing device.\nSelf-reported by the agent like Diskless, so teardown after the\nentry has left spec.replicas still targets the right pool. Empty\nmeans the default pool.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "primarySince": {
                "description": "PrimarySince is when this node's leg last became DRBD Primary (a\nconsumer holds the device open); cleared on demotion. Maintained by\nthe agent from the kernel role, so it survives unstage bookkeeping\nbeing skipped (force-killed pods). The auto-diskful reconciler keys\ntie-breaker conversion on its age.",
                "format": "date-time",
                "type": [
                  "string",
                  "null"
                ]
              },
              "sizeBytes": {
                "description": "SizeBytes is the currently realized size on this node, used to\nacknowledge expansion.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "splitBrain": {
                "description": "SplitBrain is true when this node's DRBD refused a reconnect after\ndetecting divergent data — operator intervention required.",
                "type": [
                  "boolean",
                  "null"
                ]
              }
            },
            "required": [
              "deviceCreated"
            ],
            "type": "object"
          },
          "description": "PerNode maps node name to that agent's observed state.",
          "type": [
            "object",
            "null"
          ]
        },
        "phase": {
          "description": "Phase summarizes the volume state for the controller and humans.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "type": [
        "object",
        "null"
      ]
    }
  },
  "required": [
    "spec"
  ],
  "type": "object"
}