← Control PlaneCONTENT HISTORY

Update to Control Plane

Snapshot Oct 28, 2026 · 18:05 UTC · version 1.1.0

Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.

WHAT CHANGED

Instructions updated for stateful-storage

Skill instructions — Now says: > **Tool availability:** the snapshot tools, `shrink_volumeset`, and `delete_volumeset_volume` need `?toolsets=full`; reconnect with it or use the...

Observed in instructions or declared skills. Runtime behavior has not been tested.

Save Control Plane and check future changes in Following.

Up to 10 plugins free · no card required.

All fields and original values.

Skill instructions

Before

Sep 30, 2026 · 23:00 UTC

**Most databases don't need this skill:** `template-catalog` installs Postgres, Redis, MySQL, MongoDB, and more with the volumeset, snapshots, and credentials already wired — hand-build only for a custom app or an unsupported engine. Use...

After

Oct 28, 2026 · 18:05 UTC

> **Tool availability:** the snapshot tools, `shrink_volumeset`, and `delete_volumeset_volume` need `?toolsets=full`; reconnect with it or use the CLI. **Most databases don't need this skill:** `mcp__cpln__add_database` installs Postgres...

Compare saved observations

Download comparison JSON

SKILL.md text changes

− removed · + added

--- before
+++ after
@@ -5,9 +5,11 @@
 
 # Stateful Storage & VolumeSets
 
+> **Tool availability:** the snapshot tools, `shrink_volumeset`, and `delete_volumeset_volume` need `?toolsets=full`; reconnect with it or use the CLI.
+
 A **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type, reserved mount paths, the 15-volume limit, create-then-verify); this skill is the full volume-set detail. The one trap that drives most rework: **`fileSystemType` and `performanceClass` are immutable** (a PATCH that changes either returns HTTP 400) — to change either you must create a new volumeset, and the old data does not carry over. Choose both at creation.
 
-**Most databases don't need this skill:** `template-catalog` installs Postgres, Redis, MySQL, MongoDB, and more with the volumeset, snapshots, and credentials already wired — hand-build only for a custom app or an unsupported engine.
+**Most databases don't need this skill:** `mcp__cpln__add_database` installs Postgres, MySQL, MariaDB, MongoDB, or Redis with the volume set and generated credentials wired, and the `template-catalog` skill covers the rest. An app that only needs a data directory passes `storage` to `mcp__cpln__deploy_app`, which creates and mounts the volume set and picks the workload type. Hand-build only for anything else.
 
 ## Filesystem types and performance classes
 
@@ -27,7 +29,7 @@
 
 ## Create a volumeset
 
-Use `create_volumeset` (MCP create/mount tools default `fileSystemType` to **xfs** and `performanceClass` to `general-purpose-ssd`; the raw API/`cpln apply` default is **ext4**). YAML for IaC / CLI fallback:
+Use `mcp__cpln__create_volumeset` (MCP create/mount tools default `fileSystemType` to **xfs** and `performanceClass` to `general-purpose-ssd`; the raw API/`cpln apply` default is **ext4**). YAML for IaC / CLI fallback:
 
 ```yaml
 kind: volumeset
@@ -46,7 +48,7 @@
     retentionDuration: 7d      # float + d/h/m; tool default 7d
 ```
 
-Apply with `cpln apply -f volumeset.yaml --gvc GVC`. Update mutable fields (capacity, autoscaling, snapshot policy, tags) with `update_volumeset`.
+Apply with `cpln apply -f volumeset.yaml --gvc GVC`. Update mutable fields (capacity, autoscaling, snapshot policy, tags) with `mcp__cpln__update_volumeset`.
 
 ### Autoscaling
 
@@ -70,7 +72,7 @@
 
 ## Mount to a workload
 
-Mount with `mount_volumeset_to_workload` — it attaches to the **first container** and creates the volumeset if missing (create-only defaults, ignored when the volumeset already exists: path `/mnt/{volumesetName}`, filesystem `xfs`, class `general-purpose-ssd`). Volume URI is `cpln://volumeset/VOLUMESET`.
+Mount with `mcp__cpln__mount_volumeset_to_workload` — it attaches to the **first container** and creates the volumeset if missing (create-only defaults, ignored when the volumeset already exists: path `/mnt/{volumesetName}`, filesystem `xfs`, class `general-purpose-ssd`). Volume URI is `cpln://volumeset/VOLUMESET`.
 
 - **ext4/xfs require a `stateful` or `vm` workload** (mounting on serverless/standard returns HTTP 400); `shared` mounts on any type. Workload type is immutable — see "Migrating to stateful" below.
 - Up to **15 volumes** per container. **Reserved mount paths** (rejected): `/dev`, `/dev/log`, `/tmp`, `/var`, `/var/log`.
@@ -97,19 +99,19 @@
 
 ## Snapshots
 
-Snapshots are **ext4/xfs only — never `shared`**. Automatic policy lives in `spec.snapshots`: `createFinalSnapshot` (default `true` — a snapshot is taken before any volume in the set is deleted), `retentionDuration`, and `schedule` (cron whose minute field must be a single concrete value, so no more than once per hour). Manual: `create_volumeset_snapshot`, `list_volumeset_snapshots`, `restore_volumeset_snapshot`, `delete_volumeset_snapshot`. A restore creates a **new volume** and discards everything written since the snapshot.
+Snapshots are **ext4/xfs only — never `shared`**. Automatic policy lives in `spec.snapshots`: `createFinalSnapshot` (default `true` — a snapshot is taken before any volume in the set is deleted), `retentionDuration`, and `schedule` (cron whose minute field must be a single concrete value, so no more than once per hour). Manual: `mcp__cpln__create_volumeset_snapshot`, `list_volumeset_snapshots`, `restore_volumeset_snapshot`, `delete_volumeset_snapshot`. A restore creates a **new volume** and discards everything written since the snapshot.
 
 ## Resize and delete volumes
 
-- **Expand** — live, no downtime, all filesystems. Throttled to **4 expansions per volume per rolling 24 hours**; the 5th returns **HTTP 429** and a brief wait does not help (the oldest expansion must age out of the window). `expand_volumeset`.
-- **Shrink** (ext4/xfs only) — data is migrated to the new smaller volume via an online presync + final delta sync, and the replica restarts during the swap. The platform **rejects the shrink with HTTP 400 when known used bytes (+5% metadata headroom) would not fit**; data is only lost if used bytes genuinely exceed the new capacity. Floor is the class minimum (10 / 200 GB). `shrink_volumeset`.
-- **Delete a volume** (ext4/xfs only) — permanent loss of that volume's data. `delete_volumeset_volume`.
+- **Expand** is live, with no downtime, on every filesystem; `expand_volumeset` states its throttle.
+- **Shrink** (ext4/xfs only) — data is migrated to the new smaller volume via an online presync + final delta sync, and the replica restarts during the swap. The platform **rejects the shrink with HTTP 400 when known used bytes (+5% metadata headroom) would not fit**; data is only lost if used bytes genuinely exceed the new capacity. Floor is the class minimum (10 / 200 GB). `mcp__cpln__shrink_volumeset`.
+- **Delete a volume** (ext4/xfs only) — permanent loss of that volume's data. `mcp__cpln__delete_volumeset_volume`.
 
-Shrink, volume-delete, snapshot-delete, and restore are destructive: **snapshot first** as the recovery net, then confirm the blast radius (the destructive-ops guardrail returns an impact preview before executing).
+Shrink, volume-delete, snapshot-delete, and restore are destructive: **snapshot first** as the recovery net, then present the blast radius and get the user's approval before the call (it runs at once, with no server-side preview).
 
 ## Shared filesystem
 
-A `shared` volumeset is mounted read-write by many workloads at once but supports only expand — no snapshots, shrink, or volume-delete. Each mount point is provisioned its own CPU/memory; tune with `mountOptions.resources` (defaults `minCpu 500m`, `maxCpu 2000m`, `minMemory 1Gi`, `maxMemory 2Gi`; max/min at most 4000m and 4096Mi apart, ratio at most 4:1):
+A `shared` volumeset is mounted read-write by many workloads at once but supports only expand (no snapshots, shrink, or volume-delete). Each mount point is provisioned its own CPU/memory, tuned with `mountOptions.resources`:
 
 ```yaml
 spec:
@@ -119,6 +121,14 @@
     resources: { minCpu: 500m, maxCpu: 2000m, minMemory: 1Gi, maxMemory: 2Gi }
 ```
 
+**Every `mountOptions.resources` bound is validated, and hand-written values usually violate it.** Defaults are `minCpu 500m`, `maxCpu 2000m`, `minMemory 1Gi`, `maxMemory 2Gi`. The rules:
+
+- `maxCpu / minCpu` at most **4**, and `maxCpu - minCpu` at most **4000m**
+- `maxMemory / minMemory` at most **4**, and `maxMemory - minMemory` at most **4096Mi**
+- **Omitting a bound does not skip the check.** The default fills in for whichever side you leave out: `maxCpu: 4000m` alone is compared against the default `minCpu: 500m` (8:1) and rejected, and `minCpu: 100m` alone is compared against the default `maxCpu: 2000m` (20:1) and rejected the same way.
+
+Raising a ceiling means raising its floor with it: `minCpu 1000m` / `maxCpu 4000m` passes, `minCpu 500m` / `maxCpu 4000m` does not. The rejection reads `"spec.mountOptions.resources" failed custom validation because The ratio between maxCpu and minCpu must be less than 4:1`; the check is actually `> 4`, so an exact 4:1 spread (the defaults) is accepted. The block is accepted on ext4/xfs volumesets too, and validated the same way, but only `shared` provisions mount points that use it.
+
 ## Custom encryption (AWS only)
 
 Volumes are encrypted by default. To use your own AWS KMS keys on ext4/xfs volumes (not `shared`, not BYOK):
@@ -143,8 +153,8 @@
 
 ## Verify
 
-- `get_resource` (kind `volumeset`): `status.locations[].volumes[]` show per-volume `currentSize`, `currentBytesUsed`, `lifecycle` (expect `bound`), and snapshot counts; `status.usedByWorkload` names the bound workload.
-- After mounting, poll `list_deployments` until ready and confirm the container's volume is mounted at the expected path.
+- `mcp__cpln__get_resource` (kind `volumeset`): `status.locations[].volumes[]` show per-volume `currentSize`, `currentBytesUsed`, `lifecycle` (expect `bound`), and snapshot counts; `status.usedByWorkload` names the bound workload.
+- After mounting, wait with `mcp__cpln__list_deployments` and `waitSeconds` until ready, and confirm the container's volume is mounted at the expected path.
 
 ## Troubleshooting
 
@@ -155,37 +165,7 @@
 | 400 on mount with a path | Path is reserved (`/dev`, `/tmp`, `/var`, ...) | Mount elsewhere (e.g. `/data`, `/mnt/...`) |
 | HTTP 429 on expand | 4 expansions on that volume in the last 24 h | Wait for the oldest to age out; plan larger steps |
 | 400 on shrink | New size cannot hold used bytes (+5%) | Shrink less, or free space / snapshot then rebuild |
+| 400 "The ratio between maxCpu and minCpu must be less than 4:1" (same for memory) | `mountOptions.resources` spread past 4:1 or past 4000m / 4096Mi, counting the default that fills any bound you left out | Raise `minCpu` / `minMemory` or lower `maxCpu` / `maxMemory`, and always set both bounds of a pair |
+| 400 "Workload X references non-existent volume sets" | The volumeset in the same apply failed its own validation. `cpln apply` reports every failure and does not stop at the first, so the workload PUT still ran | Fix the volumeset error above it and re-apply the bundle; the ordering is already correct |
 | Snapshot fields rejected | Volumeset is `shared` | Snapshots need ext4/xfs |
-| Deployment stuck after mount | Volume provisioning (2-5 min on first deploy) | Poll `list_deployments`; check `get_workload_logs` if it stays unready |
-
-## MCP tools quick reference
-
-| Tool | Purpose | Tier |
-|---|---|---|
-| `create_volumeset` | Create a volumeset | core |
-| `update_volumeset` | Update mutable fields (capacity, autoscaling, snapshots, tags) | core |
-| `mount_volumeset_to_workload` | Mount to a workload (creates the volumeset if missing) | core |
-| `expand_volumeset` | Grow a volume (4 / 24 h limit) | core |
-| `shrink_volumeset` | Shrink a volume (ext4/xfs) | full |
-| `delete_volumeset_volume` | Delete one volume (ext4/xfs) | full |
-| `create_volumeset_snapshot` | Point-in-time snapshot | full |
-| `list_volumeset_snapshots` | List snapshots | full |
-| `restore_volumeset_snapshot` | Restore a snapshot to a new volume | full |
-| `delete_volumeset_snapshot` | Delete a snapshot | full |
-| `get_resource` / `list_resources` / `delete_resource` (kind `volumeset`) | Read / list / delete a volumeset | core |
-
-CLI fallback (CI/CD via a service-account `CPLN_TOKEN`): `cpln volumeset create|get|update|delete|expand|shrink`, `cpln volumeset snapshot create|get|restore|delete`, `cpln volumeset volume get|delete`; `expand`/`shrink` need `--new-size` (`--location`/`--volume-index` optional), and `cpln apply -f` for YAML.
-
-## Related skills
-
-| Skill | For |
-|---|---|
-| `workload` | Workload types, the deploy-and-verify flow, load-balancer/`replicaDirect` config |
-| `template-catalog` | Postgres, Redis, and other databases that provision volumesets for you |
-| `firewall-networking` | Outbound rules for cloud-bucket volumes (`s3://`, `gs://`, `azureblob://`) |
-
-## Documentation
-
-- [Volume Set Reference](https://docs.controlplane.com/reference/volumeset.md)
-- [Workload Volumes](https://docs.controlplane.com/reference/workload/volumes.md)
-- [CLI volumeset Commands](https://docs.controlplane.com/cli-reference/commands/volumeset.md)
+| Deployment stuck after mount | Volume provisioning (2 to 5 min on first deploy) | `list_deployments` with `waitSeconds`; `diagnose_workload` if it stays unready |
Full technical diff · 1 changed fields

changed /skill_md_contents

BEFORE
"---\nname: stateful-storage\ndescription: \"Creates persistent storage for stateful workloads on Control Plane. Use when the user asks about volumes, volume sets, disks, mounting storage, snapshots, volume expansion, filesystems, shared storage, or backups.\"\n---\n\n# Stateful Storage & VolumeSets\n\nA **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type, reserved mount paths, the 15-volume limit, create-then-verify); this skill is the full volume-set detail. The one trap that drives most rework: **`fileSystemType` and `performanceClass` are immutable** (a PATCH that changes either returns HTTP 400) — to change either you must create a new volumeset, and the old data does not carry over. Choose both at creation.\n\n**Most databases don't need this skill:** `template-catalog` installs Postgres, Redis, MySQL, MongoDB, and more with the volumeset, snapshots, and credentials already wired — hand-build only for a custom app or an unsupported engine.\n\n## Filesystem types and performance classes\n\n| Filesystem | Access | Workloads | Volumes provisioned | Snapshots / shrink / delete-volume |\n|---|---|---|---|---|\n| `ext4` | read-write-once | one stateful/vm workload | one per replica, per location | yes |\n| `xfs` | read-write-once | one stateful/vm workload | one per replica, per location | yes |\n| `shared` | read-write-many | any workload type, many at once | one per location (shared by all replicas) | no — expand only |\n\n| Performance class | Min | Max | Filesystems |\n|---|---|---|---|\n| `general-purpose-ssd` | 10 GB | 65536 GB | ext4, xfs |\n| `high-throughput-ssd` | 200 GB | 65536 GB | ext4, xfs |\n| `shared` | 10 GB | 65536 GB | shared (auto-set) |\n\nWhen `fileSystemType: shared`, `performanceClass` is **auto-set to `shared`** — do not specify another. Data is **per-location** and never replicated across locations; for cross-location redundancy, replicate at the application layer (e.g. WAL streaming).\n\n## Create a volumeset\n\nUse `create_volumeset` (MCP create/mount tools default `fileSystemType` to **xfs** and `performanceClass` to `general-purpose-ssd`; the raw API/`cpln apply` default is **ext4**). YAML for IaC / CLI fallback:\n\n```yaml\nkind: volumeset\nname: pg-data\ngvc: GVC\nspec:\n  fileSystemType: ext4\n  performanceClass: general-purpose-ssd\n  initialCapacity: 20          # GB; within the class min/max and <= autoscaling.maxCapacity\n  autoscaling:\n    maxCapacity: 100\n    minFreePercentage: 20      # 1-100\n    scalingFactor: 1.5         # >= 1.1\n  snapshots:\n    schedule: \"0 2 * * *\"      # cron; no more than once per hour\n    retentionDuration: 7d      # float + d/h/m; tool default 7d\n```\n\nApply with `cpln apply -f volumeset.yaml --gvc GVC`. Update mutable fields (capacity, autoscaling, snapshot policy, tags) with `update_volumeset`.\n\n### Autoscaling\n\n**Reactive**: a background job checks volumes about once a minute; when free space falls below `minFreePercentage` it resizes the volume to hold current usage at that margin, scaled up: `new_capacity = ceil(usedGB / (1 - minFreePercentage/100) * scalingFactor)`, capped at `maxCapacity`. Both fields are required, or autoscaling does nothing.\n\n**Predictive** runs the same formula on *projected* usage (from the recent growth rate) to expand ahead of demand; the larger of the reactive and predictive targets wins. Requires `minFreePercentage > 0` and `scalingFactor >= 1.1`:\n\n```yaml\n  autoscaling:\n    maxCapacity: 200\n    minFreePercentage: 20\n    scalingFactor: 1.5\n    predictive:\n      enabled: true            # default false\n      lookbackHours: 24        # 1-168\n      projectionHours: 6       # 1-72\n      minDataPoints: 10        # 2-100\n      minGrowthRateGBPerHour: 0.01\n      scalingFactor: 1.2       # >= 1.1; defaults to the parent scalingFactor\n```\n\n## Mount to a workload\n\nMount with `mount_volumeset_to_workload` — it attaches to the **first container** and creates the volumeset if missing (create-only defaults, ignored when the volumeset already exists: path `/mnt/{volumesetName}`, filesystem `xfs`, class `general-purpose-ssd`). Volume URI is `cpln://volumeset/VOLUMESET`.\n\n- **ext4/xfs require a `stateful` or `vm` workload** (mounting on serverless/standard returns HTTP 400); `shared` mounts on any type. Workload type is immutable — see \"Migrating to stateful\" below.\n- Up to **15 volumes** per container. **Reserved mount paths** (rejected): `/dev`, `/dev/log`, `/tmp`, `/var`, `/var/log`.\n- `recoveryPolicy`: `retain` (default — reuse an existing volume's data on a new replica) or `recycle` (start fresh).\n- `path` is required for non-vm workloads and rejected for `vm` (VM disks use `name`/`bus`/`bootOrder` instead).\n- Stateful workloads give each replica a stable index and its own volume; `spec.loadBalancer.replicaDirect` (stateful-only) exposes per-replica endpoints — see the `workload` skill.\n\n```yaml\nkind: workload\nname: pg\ngvc: GVC\nspec:\n  type: stateful\n  containers:\n    - name: postgres\n      image: //image/postgres:16\n      ports:\n        - number: 5432\n          protocol: tcp        # http | http2 | grpc | tcp — a DB is tcp, not http\n      volumes:\n        - uri: cpln://volumeset/pg-data\n          path: /var/lib/postgresql/data\n```\n\n## Snapshots\n\nSnapshots are **ext4/xfs only — never `shared`**. Automatic policy lives in `spec.snapshots`: `createFinalSnapshot` (default `true` — a snapshot is taken before any volume in the set is deleted), `retentionDuration`, and `schedule` (cron whose minute field must be a single concrete value, so no more than once per hour). Manual: `create_volumeset_snapshot`, `list_volumeset_snapshots`, `restore_volumeset_snapshot`, `delete_volumeset_snapshot`. A restore creates a **new volume** and discards everything written since the snapshot.\n\n## Resize and delete volumes\n\n- **Expand** — live, no downtime, all filesystems. Throttled to **4 expansions per volume per rolling 24 hours**; the 5th returns **HTTP 429** and a brief wait does not help (the oldest expansion must age out of the window). `expand_volumeset`.\n- **Shrink** (ext4/xfs only) — data is migrated to the new smaller volume via an online presync + final delta sync, and the replica restarts during the swap. The platform **rejects the shrink with HTTP 400 when known used bytes (+5% metadata headroom) would not fit**; data is only lost if used bytes genuinely exceed the new capacity. Floor is the class minimum (10 / 200 GB). `shrink_volumeset`.\n- **Delete a volume** (ext4/xfs only) — permanent loss of that volume's data. `delete_volumeset_volume`.\n\nShrink, volume-delete, snapshot-delete, and restore are destructive: **snapshot first** as the recovery net, then confirm the blast radius (the destructive-ops guardrail returns an impact preview before executing).\n\n## Shared filesystem\n\nA `shared` volumeset is mounted read-write by many workloads at once but supports only expand — no snapshots, shrink, or volume-delete. Each mount point is provisioned its own CPU/memory; tune with `mountOptions.resources` (defaults `minCpu 500m`, `maxCpu 2000m`, `minMemory 1Gi`, `maxMemory 2Gi`; max/min at most 4000m and 4096Mi apart, ratio at most 4:1):\n\n```yaml\nspec:\n  fileSystemType: shared       # performanceClass auto-set to \"shared\"\n  initialCapacity: 50\n  mountOptions:\n    resources: { minCpu: 500m, maxCpu: 2000m, minMemory: 1Gi, maxMemory: 2Gi }\n```\n\n## Custom encryption (AWS only)\n\nVolumes are encrypted by default. To use your own AWS KMS keys on ext4/xfs volumes (not `shared`, not BYOK):\n\n```yaml\nspec:\n  customEncryption:\n    regions:\n      aws-us-east-1:           # format: {cloud-provider}-{region}\n        keyId: \"arn:aws:kms:us-east-1:123456789:key/KEY_ID\"\n```\n\nThe `keyId` is injected as the EBS storage-class `kmsKeyId`. The KMS key policy **must grant Control Plane's AWS account `arn:aws:iam::957753459089:root`** the volume-encryption permissions (`Decrypt`, `Encrypt`, `GenerateDataKey`, `CreateGrant`, etc.); the key is immutable once a volume exists.\n\n## BYOK storage classes\n\nOn self-hosted clusters, volumes use the storage class `{performanceClass}-{fileSystemType}` (e.g. `general-purpose-ssd-ext4`) and the cluster needs a CSI-compatible driver. `spec.storageClassSuffix` selects an alternative `{performanceClass}-{fileSystemType}-{suffix}`, falling back to the unsuffixed class if it is not found.\n\n## Migrating a workload to stateful\n\nWorkload type is immutable, so adding an ext4/xfs volume to a serverless/standard workload means **delete + recreate as `stateful`** — destructive. Before deleting, confirm with the user: the public URL that 5xx's during the cutover, any internal callers that fail until recreate, runtime/in-memory state lost at delete, and that the recreate typically takes 2-5 min. Sequence: capture the spec (`cpln workload get WORKLOAD --gvc GVC -o yaml-slim > bak.yaml`) as a rollback artifact; apply the volumeset; delete the old workload; apply the new manifest with `spec.type: stateful` + the volume mount, **keeping the same name** to preserve URL/DNS/policy/identity links. For the deploy-wait pattern, see the `workload` skill.\n\n## Verify\n\n- `get_resource` (kind `volumeset`): `status.locations[].volumes[]` show per-volume `currentSize`, `currentBytesUsed`, `lifecycle` (expect `bound`), and snapshot counts; `status.usedByWorkload` names the bound workload.\n- After mounting, poll `list_deployments` until ready and confirm the container's volume is mounted at the expected path.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| 400 mounting a volumeset | ext4/xfs on a serverless/standard workload | Use a `stateful` or `vm` workload (recreate to change type) |\n| 400 \"performanceClass / fileSystemType is immutable\" | Tried to change either on update | Create a new volumeset; migrate data via snapshot/restore |\n| 400 on mount with a path | Path is reserved (`/dev`, `/tmp`, `/var`, ...) | Mount elsewhere (e.g. `/data`, `/mnt/...`) |\n| HTTP 429 on expand | 4 expansions on that volume in the last 24 h | Wait for the oldest to age out; plan larger steps |\n| 400 on shrink | New size cannot hold used bytes (+5%) | Shrink less, or free space / snapshot then rebuild |\n| Snapshot fields rejected | Volumeset is `shared` | Snapshots need ext4/xfs |\n| Deployment stuck after mount | Volume provisioning (2-5 min on first deploy) | Poll `list_deployments`; check `get_workload_logs` if it stays unready |\n\n## MCP tools quick reference\n\n| Tool | Purpose | Tier |\n|---|---|---|\n| `create_volumeset` | Create a volumeset | core |\n| `update_volumeset` | Update mutable fields (capacity, autoscaling, snapshots, tags) | core |\n| `mount_volumeset_to_workload` | Mount to a workload (creates the volumeset if missing) | core |\n| `expand_volumeset` | Grow a volume (4 / 24 h limit) | core |\n| `shrink_volumeset` | Shrink a volume (ext4/xfs) | full |\n| `delete_volumeset_volume` | Delete one volume (ext4/xfs) | full |\n| `create_volumeset_snapshot` | Point-in-time snapshot | full |\n| `list_volumeset_snapshots` | List snapshots | full |\n| `restore_volumeset_snapshot` | Restore a snapshot to a new volume | full |\n| `delete_volumeset_snapshot` | Delete a snapshot | full |\n| `get_resource` / `list_resources` / `delete_resource` (kind `volumeset`) | Read / list / delete a volumeset | core |\n\nCLI fallback (CI/CD via a service-account `CPLN_TOKEN`): `cpln volumeset create|get|update|delete|expand|shrink`, `cpln volumeset snapshot create|get|restore|delete`, `cpln volumeset volume get|delete`; `expand`/`shrink` need `--new-size` (`--location`/`--volume-index` optional), and `cpln apply -f` for YAML.\n\n## Related skills\n\n| Skill | For |\n|---|---|\n| `workload` | Workload types, the deploy-and-verify flow, load-balancer/`replicaDirect` config |\n| `template-catalog` | Postgres, Redis, and other databases that provision volumesets for you |\n| `firewall-networking` | Outbound rules for cloud-bucket volumes (`s3://`, `gs://`, `azureblob://`) |\n\n## Documentation\n\n- [Volume Set Reference](https://docs.controlplane.com/reference/volumeset.md)\n- [Workload Volumes](https://docs.controlplane.com/reference/workload/volumes.md)\n- [CLI volumeset Commands](https://docs.controlplane.com/cli-reference/commands/volumeset.md)\n"
AFTER
"---\nname: stateful-storage\ndescription: \"Creates persistent storage for stateful workloads on Control Plane. Use when the user asks about volumes, volume sets, disks, mounting storage, snapshots, volume expansion, filesystems, shared storage, or backups.\"\n---\n\n# Stateful Storage & VolumeSets\n\n> **Tool availability:** the snapshot tools, `shrink_volumeset`, and `delete_volumeset_volume` need `?toolsets=full`; reconnect with it or use the CLI.\n\nA **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type, reserved mount paths, the 15-volume limit, create-then-verify); this skill is the full volume-set detail. The one trap that drives most rework: **`fileSystemType` and `performanceClass` are immutable** (a PATCH that changes either returns HTTP 400) — to change either you must create a new volumeset, and the old data does not carry over. Choose both at creation.\n\n**Most databases don't need this skill:** `mcp__cpln__add_database` installs Postgres, MySQL, MariaDB, MongoDB, or Redis with the volume set and generated credentials wired, and the `template-catalog` skill covers the rest. An app that only needs a data directory passes `storage` to `mcp__cpln__deploy_app`, which creates and mounts the volume set and picks the workload type. Hand-build only for anything else.\n\n## Filesystem types and performance classes\n\n| Filesystem | Access | Workloads | Volumes provisioned | Snapshots / shrink / delete-volume |\n|---|---|---|---|---|\n| `ext4` | read-write-once | one stateful/vm workload | one per replica, per location | yes |\n| `xfs` | read-write-once | one stateful/vm workload | one per replica, per location | yes |\n| `shared` | read-write-many | any workload type, many at once | one per location (shared by all replicas) | no — expand only |\n\n| Performance class | Min | Max | Filesystems |\n|---|---|---|---|\n| `general-purpose-ssd` | 10 GB | 65536 GB | ext4, xfs |\n| `high-throughput-ssd` | 200 GB | 65536 GB | ext4, xfs |\n| `shared` | 10 GB | 65536 GB | shared (auto-set) |\n\nWhen `fileSystemType: shared`, `performanceClass` is **auto-set to `shared`** — do not specify another. Data is **per-location** and never replicated across locations; for cross-location redundancy, replicate at the application layer (e.g. WAL streaming).\n\n## Create a volumeset\n\nUse `mcp__cpln__create_volumeset` (MCP create/mount tools default `fileSystemType` to **xfs** and `performanceClass` to `general-purpose-ssd`; the raw API/`cpln apply` default is **ext4**). YAML for IaC / CLI fallback:\n\n```yaml\nkind: volumeset\nname: pg-data\ngvc: GVC\nspec:\n  fileSystemType: ext4\n  performanceClass: general-purpose-ssd\n  initialCapacity: 20          # GB; within the class min/max and <= autoscaling.maxCapacity\n  autoscaling:\n    maxCapacity: 100\n    minFreePercentage: 20      # 1-100\n    scalingFactor: 1.5         # >= 1.1\n  snapshots:\n    schedule: \"0 2 * * *\"      # cron; no more than once per hour\n    retentionDuration: 7d      # float + d/h/m; tool default 7d\n```\n\nApply with `cpln apply -f volumeset.yaml --gvc GVC`. Update mutable fields (capacity, autoscaling, snapshot policy, tags) with `mcp__cpln__update_volumeset`.\n\n### Autoscaling\n\n**Reactive**: a background job checks volumes about once a minute; when free space falls below `minFreePercentage` it resizes the volume to hold current usage at that margin, scaled up: `new_capacity = ceil(usedGB / (1 - minFreePercentage/100) * scalingFactor)`, capped at `maxCapacity`. Both fields are required, or autoscaling does nothing.\n\n**Predictive** runs the same formula on *projected* usage (from the recent growth rate) to expand ahead of demand; the larger of the reactive and predictive targets wins. Requires `minFreePercentage > 0` and `scalingFactor >= 1.1`:\n\n```yaml\n  autoscaling:\n    maxCapacity: 200\n    minFreePercentage: 20\n    scalingFactor: 1.5\n    predictive:\n      enabled: true            # default false\n      lookbackHours: 24        # 1-168\n      projectionHours: 6       # 1-72\n      minDataPoints: 10        # 2-100\n      minGrowthRateGBPerHour: 0.01\n      scalingFactor: 1.2       # >= 1.1; defaults to the parent scalingFactor\n```\n\n## Mount to a workload\n\nMount with `mcp__cpln__mount_volumeset_to_workload` — it attaches to the **first container** and creates the volumeset if missing (create-only defaults, ignored when the volumeset already exists: path `/mnt/{volumesetName}`, filesystem `xfs`, class `general-purpose-ssd`). Volume URI is `cpln://volumeset/VOLUMESET`.\n\n- **ext4/xfs require a `stateful` or `vm` workload** (mounting on serverless/standard returns HTTP 400); `shared` mounts on any type. Workload type is immutable — see \"Migrating to stateful\" below.\n- Up to **15 volumes** per container. **Reserved mount paths** (rejected): `/dev`, `/dev/log`, `/tmp`, `/var`, `/var/log`.\n- `recoveryPolicy`: `retain` (default — reuse an existing volume's data on a new replica) or `recycle` (start fresh).\n- `path` is required for non-vm workloads and rejected for `vm` (VM disks use `name`/`bus`/`bootOrder` instead).\n- Stateful workloads give each replica a stable index and its own volume; `spec.loadBalancer.replicaDirect` (stateful-only) exposes per-replica endpoints — see the `workload` skill.\n\n```yaml\nkind: workload\nname: pg\ngvc: GVC\nspec:\n  type: stateful\n  containers:\n    - name: postgres\n      image: //image/postgres:16\n      ports:\n        - number: 5432\n          protocol: tcp        # http | http2 | grpc | tcp — a DB is tcp, not http\n      volumes:\n        - uri: cpln://volumeset/pg-data\n          path: /var/lib/postgresql/data\n```\n\n## Snapshots\n\nSnapshots are **ext4/xfs only — never `shared`**. Automatic policy lives in `spec.snapshots`: `createFinalSnapshot` (default `true` — a snapshot is taken before any volume in the set is deleted), `retentionDuration`, and `schedule` (cron whose minute field must be a single concrete value, so no more than once per hour). Manual: `mcp__cpln__create_volumeset_snapshot`, `list_volumeset_snapshots`, `restore_volumeset_snapshot`, `delete_volumeset_snapshot`. A restore creates a **new volume** and discards everything written since the snapshot.\n\n## Resize and delete volumes\n\n- **Expand** is live, with no downtime, on every filesystem; `expand_volumeset` states its throttle.\n- **Shrink** (ext4/xfs only) — data is migrated to the new smaller volume via an online presync + final delta sync, and the replica restarts during the swap. The platform **rejects the shrink with HTTP 400 when known used bytes (+5% metadata headroom) would not fit**; data is only lost if used bytes genuinely exceed the new capacity. Floor is the class minimum (10 / 200 GB). `mcp__cpln__shrink_volumeset`.\n- **Delete a volume** (ext4/xfs only) — permanent loss of that volume's data. `mcp__cpln__delete_volumeset_volume`.\n\nShrink, volume-delete, snapshot-delete, and restore are destructive: **snapshot first** as the recovery net, then present the blast radius and get the user's approval before the call (it runs at once, with no server-side preview).\n\n## Shared filesystem\n\nA `shared` volumeset is mounted read-write by many workloads at once but supports only expand (no snapshots, shrink, or volume-delete). Each mount point is provisioned its own CPU/memory, tuned with `mountOptions.resources`:\n\n```yaml\nspec:\n  fileSystemType: shared       # performanceClass auto-set to \"shared\"\n  initialCapacity: 50\n  mountOptions:\n    resources: { minCpu: 500m, maxCpu: 2000m, minMemory: 1Gi, maxMemory: 2Gi }\n```\n\n**Every `mountOptions.resources` bound is validated, and hand-written values usually violate it.** Defaults are `minCpu 500m`, `maxCpu 2000m`, `minMemory 1Gi`, `maxMemory 2Gi`. The rules:\n\n- `maxCpu / minCpu` at most **4**, and `maxCpu - minCpu` at most **4000m**\n- `maxMemory / minMemory` at most **4**, and `maxMemory - minMemory` at most **4096Mi**\n- **Omitting a bound does not skip the check.** The default fills in for whichever side you leave out: `maxCpu: 4000m` alone is compared against the default `minCpu: 500m` (8:1) and rejected, and `minCpu: 100m` alone is compared against the default `maxCpu: 2000m` (20:1) and rejected the same way.\n\nRaising a ceiling means raising its floor with it: `minCpu 1000m` / `maxCpu 4000m` passes, `minCpu 500m` / `maxCpu 4000m` does not. The rejection reads `\"spec.mountOptions.resources\" failed custom validation because The ratio between maxCpu and minCpu must be less than 4:1`; the check is actually `> 4`, so an exact 4:1 spread (the defaults) is accepted. The block is accepted on ext4/xfs volumesets too, and validated the same way, but only `shared` provisions mount points that use it.\n\n## Custom encryption (AWS only)\n\nVolumes are encrypted by default. To use your own AWS KMS keys on ext4/xfs volumes (not `shared`, not BYOK):\n\n```yaml\nspec:\n  customEncryption:\n    regions:\n      aws-us-east-1:           # format: {cloud-provider}-{region}\n        keyId: \"arn:aws:kms:us-east-1:123456789:key/KEY_ID\"\n```\n\nThe `keyId` is injected as the EBS storage-class `kmsKeyId`. The KMS key policy **must grant Control Plane's AWS account `arn:aws:iam::957753459089:root`** the volume-encryption permissions (`Decrypt`, `Encrypt`, `GenerateDataKey`, `CreateGrant`, etc.); the key is immutable once a volume exists.\n\n## BYOK storage classes\n\nOn self-hosted clusters, volumes use the storage class `{performanceClass}-{fileSystemType}` (e.g. `general-purpose-ssd-ext4`) and the cluster needs a CSI-compatible driver. `spec.storageClassSuffix` selects an alternative `{performanceClass}-{fileSystemType}-{suffix}`, falling back to the unsuffixed class if it is not found.\n\n## Migrating a workload to stateful\n\nWorkload type is immutable, so adding an ext4/xfs volume to a serverless/standard workload means **delete + recreate as `stateful`** — destructive. Before deleting, confirm with the user: the public URL that 5xx's during the cutover, any internal callers that fail until recreate, runtime/in-memory state lost at delete, and that the recreate typically takes 2-5 min. Sequence: capture the spec (`cpln workload get WORKLOAD --gvc GVC -o yaml-slim > bak.yaml`) as a rollback artifact; apply the volumeset; delete the old workload; apply the new manifest with `spec.type: stateful` + the volume mount, **keeping the same name** to preserve URL/DNS/policy/identity links. For the deploy-wait pattern, see the `workload` skill.\n\n## Verify\n\n- `mcp__cpln__get_resource` (kind `volumeset`): `status.locations[].volumes[]` show per-volume `currentSize`, `currentBytesUsed`, `lifecycle` (expect `bound`), and snapshot counts; `status.usedByWorkload` names the bound workload.\n- After mounting, wait with `mcp__cpln__list_deployments` and `waitSeconds` until ready, and confirm the container's volume is mounted at the expected path.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| 400 mounting a volumeset | ext4/xfs on a serverless/standard workload | Use a `stateful` or `vm` workload (recreate to change type) |\n| 400 \"performanceClass / fileSystemType is immutable\" | Tried to change either on update | Create a new volumeset; migrate data via snapshot/restore |\n| 400 on mount with a path | Path is reserved (`/dev`, `/tmp`, `/var`, ...) | Mount elsewhere (e.g. `/data`, `/mnt/...`) |\n| HTTP 429 on expand | 4 expansions on that volume in the last 24 h | Wait for the oldest to age out; plan larger steps |\n| 400 on shrink | New size cannot hold used bytes (+5%) | Shrink less, or free space / snapshot then rebuild |\n| 400 \"The ratio between maxCpu and minCpu must be less than 4:1\" (same for memory) | `mountOptions.resources` spread past 4:1 or past 4000m / 4096Mi, counting the default that fills any bound you left out | Raise `minCpu` / `minMemory` or lower `maxCpu` / `maxMemory`, and always set both bounds of a pair |\n| 400 \"Workload X references non-existent volume sets\" | The volumeset in the same apply failed its own validation. `cpln apply` reports every failure and does not stop at the first, so the workload PUT still ran | Fix the volumeset error above it and re-apply the bundle; the ordering is already correct |\n| Snapshot fields rejected | Volumeset is `shared` | Snapshots need ext4/xfs |\n| Deployment stuck after mount | Volume provisioning (2 to 5 min on first deploy) | `list_deployments` with `waitSeconds`; `diagnose_workload` if it stays unready |\n"
Full snapshot data
{
  "description": "Creates persistent storage for stateful workloads on Control Plane. Use when the user asks about volumes, volume sets, disks, mounting storage, snapshots, volume expansion, filesystems, shared storage, or backups.",
  "included_files": [],
  "name": "stateful-storage",
  "skill_md_contents": "---\nname: stateful-storage\ndescription: \"Creates persistent storage for stateful workloads on Control Plane. Use when the user asks about volumes, volume sets, disks, mounting storage, snapshots, volume expansion, filesystems, shared storage, or backups.\"\n---\n\n# Stateful Storage & VolumeSets\n\n> **Tool availability:** the snapshot tools, `shrink_volumeset`, and `delete_volumeset_volume` need `?toolsets=full`; reconnect with it or use the CLI.\n\nA **VolumeSet** is GVC-scoped persistent storage for workloads. The `workload` skill covers the basics (stateful type, reserved mount paths, the 15-volume limit, create-then-verify); this skill is the full volume-set detail. The one trap that drives most rework: **`fileSystemType` and `performanceClass` are immutable** (a PATCH that changes either returns HTTP 400) — to change either you must create a new volumeset, and the old data does not carry over. Choose both at creation.\n\n**Most databases don't need this skill:** `mcp__cpln__add_database` installs Postgres, MySQL, MariaDB, MongoDB, or Redis with the volume set and generated credentials wired, and the `template-catalog` skill covers the rest. An app that only needs a data directory passes `storage` to `mcp__cpln__deploy_app`, which creates and mounts the volume set and picks the workload type. Hand-build only for anything else.\n\n## Filesystem types and performance classes\n\n| Filesystem | Access | Workloads | Volumes provisioned | Snapshots / shrink / delete-volume |\n|---|---|---|---|---|\n| `ext4` | read-write-once | one stateful/vm workload | one per replica, per location | yes |\n| `xfs` | read-write-once | one stateful/vm workload | one per replica, per location | yes |\n| `shared` | read-write-many | any workload type, many at once | one per location (shared by all replicas) | no — expand only |\n\n| Performance class | Min | Max | Filesystems |\n|---|---|---|---|\n| `general-purpose-ssd` | 10 GB | 65536 GB | ext4, xfs |\n| `high-throughput-ssd` | 200 GB | 65536 GB | ext4, xfs |\n| `shared` | 10 GB | 65536 GB | shared (auto-set) |\n\nWhen `fileSystemType: shared`, `performanceClass` is **auto-set to `shared`** — do not specify another. Data is **per-location** and never replicated across locations; for cross-location redundancy, replicate at the application layer (e.g. WAL streaming).\n\n## Create a volumeset\n\nUse `mcp__cpln__create_volumeset` (MCP create/mount tools default `fileSystemType` to **xfs** and `performanceClass` to `general-purpose-ssd`; the raw API/`cpln apply` default is **ext4**). YAML for IaC / CLI fallback:\n\n```yaml\nkind: volumeset\nname: pg-data\ngvc: GVC\nspec:\n  fileSystemType: ext4\n  performanceClass: general-purpose-ssd\n  initialCapacity: 20          # GB; within the class min/max and <= autoscaling.maxCapacity\n  autoscaling:\n    maxCapacity: 100\n    minFreePercentage: 20      # 1-100\n    scalingFactor: 1.5         # >= 1.1\n  snapshots:\n    schedule: \"0 2 * * *\"      # cron; no more than once per hour\n    retentionDuration: 7d      # float + d/h/m; tool default 7d\n```\n\nApply with `cpln apply -f volumeset.yaml --gvc GVC`. Update mutable fields (capacity, autoscaling, snapshot policy, tags) with `mcp__cpln__update_volumeset`.\n\n### Autoscaling\n\n**Reactive**: a background job checks volumes about once a minute; when free space falls below `minFreePercentage` it resizes the volume to hold current usage at that margin, scaled up: `new_capacity = ceil(usedGB / (1 - minFreePercentage/100) * scalingFactor)`, capped at `maxCapacity`. Both fields are required, or autoscaling does nothing.\n\n**Predictive** runs the same formula on *projected* usage (from the recent growth rate) to expand ahead of demand; the larger of the reactive and predictive targets wins. Requires `minFreePercentage > 0` and `scalingFactor >= 1.1`:\n\n```yaml\n  autoscaling:\n    maxCapacity: 200\n    minFreePercentage: 20\n    scalingFactor: 1.5\n    predictive:\n      enabled: true            # default false\n      lookbackHours: 24        # 1-168\n      projectionHours: 6       # 1-72\n      minDataPoints: 10        # 2-100\n      minGrowthRateGBPerHour: 0.01\n      scalingFactor: 1.2       # >= 1.1; defaults to the parent scalingFactor\n```\n\n## Mount to a workload\n\nMount with `mcp__cpln__mount_volumeset_to_workload` — it attaches to the **first container** and creates the volumeset if missing (create-only defaults, ignored when the volumeset already exists: path `/mnt/{volumesetName}`, filesystem `xfs`, class `general-purpose-ssd`). Volume URI is `cpln://volumeset/VOLUMESET`.\n\n- **ext4/xfs require a `stateful` or `vm` workload** (mounting on serverless/standard returns HTTP 400); `shared` mounts on any type. Workload type is immutable — see \"Migrating to stateful\" below.\n- Up to **15 volumes** per container. **Reserved mount paths** (rejected): `/dev`, `/dev/log`, `/tmp`, `/var`, `/var/log`.\n- `recoveryPolicy`: `retain` (default — reuse an existing volume's data on a new replica) or `recycle` (start fresh).\n- `path` is required for non-vm workloads and rejected for `vm` (VM disks use `name`/`bus`/`bootOrder` instead).\n- Stateful workloads give each replica a stable index and its own volume; `spec.loadBalancer.replicaDirect` (stateful-only) exposes per-replica endpoints — see the `workload` skill.\n\n```yaml\nkind: workload\nname: pg\ngvc: GVC\nspec:\n  type: stateful\n  containers:\n    - name: postgres\n      image: //image/postgres:16\n      ports:\n        - number: 5432\n          protocol: tcp        # http | http2 | grpc | tcp — a DB is tcp, not http\n      volumes:\n        - uri: cpln://volumeset/pg-data\n          path: /var/lib/postgresql/data\n```\n\n## Snapshots\n\nSnapshots are **ext4/xfs only — never `shared`**. Automatic policy lives in `spec.snapshots`: `createFinalSnapshot` (default `true` — a snapshot is taken before any volume in the set is deleted), `retentionDuration`, and `schedule` (cron whose minute field must be a single concrete value, so no more than once per hour). Manual: `mcp__cpln__create_volumeset_snapshot`, `list_volumeset_snapshots`, `restore_volumeset_snapshot`, `delete_volumeset_snapshot`. A restore creates a **new volume** and discards everything written since the snapshot.\n\n## Resize and delete volumes\n\n- **Expand** is live, with no downtime, on every filesystem; `expand_volumeset` states its throttle.\n- **Shrink** (ext4/xfs only) — data is migrated to the new smaller volume via an online presync + final delta sync, and the replica restarts during the swap. The platform **rejects the shrink with HTTP 400 when known used bytes (+5% metadata headroom) would not fit**; data is only lost if used bytes genuinely exceed the new capacity. Floor is the class minimum (10 / 200 GB). `mcp__cpln__shrink_volumeset`.\n- **Delete a volume** (ext4/xfs only) — permanent loss of that volume's data. `mcp__cpln__delete_volumeset_volume`.\n\nShrink, volume-delete, snapshot-delete, and restore are destructive: **snapshot first** as the recovery net, then present the blast radius and get the user's approval before the call (it runs at once, with no server-side preview).\n\n## Shared filesystem\n\nA `shared` volumeset is mounted read-write by many workloads at once but supports only expand (no snapshots, shrink, or volume-delete). Each mount point is provisioned its own CPU/memory, tuned with `mountOptions.resources`:\n\n```yaml\nspec:\n  fileSystemType: shared       # performanceClass auto-set to \"shared\"\n  initialCapacity: 50\n  mountOptions:\n    resources: { minCpu: 500m, maxCpu: 2000m, minMemory: 1Gi, maxMemory: 2Gi }\n```\n\n**Every `mountOptions.resources` bound is validated, and hand-written values usually violate it.** Defaults are `minCpu 500m`, `maxCpu 2000m`, `minMemory 1Gi`, `maxMemory 2Gi`. The rules:\n\n- `maxCpu / minCpu` at most **4**, and `maxCpu - minCpu` at most **4000m**\n- `maxMemory / minMemory` at most **4**, and `maxMemory - minMemory` at most **4096Mi**\n- **Omitting a bound does not skip the check.** The default fills in for whichever side you leave out: `maxCpu: 4000m` alone is compared against the default `minCpu: 500m` (8:1) and rejected, and `minCpu: 100m` alone is compared against the default `maxCpu: 2000m` (20:1) and rejected the same way.\n\nRaising a ceiling means raising its floor with it: `minCpu 1000m` / `maxCpu 4000m` passes, `minCpu 500m` / `maxCpu 4000m` does not. The rejection reads `\"spec.mountOptions.resources\" failed custom validation because The ratio between maxCpu and minCpu must be less than 4:1`; the check is actually `> 4`, so an exact 4:1 spread (the defaults) is accepted. The block is accepted on ext4/xfs volumesets too, and validated the same way, but only `shared` provisions mount points that use it.\n\n## Custom encryption (AWS only)\n\nVolumes are encrypted by default. To use your own AWS KMS keys on ext4/xfs volumes (not `shared`, not BYOK):\n\n```yaml\nspec:\n  customEncryption:\n    regions:\n      aws-us-east-1:           # format: {cloud-provider}-{region}\n        keyId: \"arn:aws:kms:us-east-1:123456789:key/KEY_ID\"\n```\n\nThe `keyId` is injected as the EBS storage-class `kmsKeyId`. The KMS key policy **must grant Control Plane's AWS account `arn:aws:iam::957753459089:root`** the volume-encryption permissions (`Decrypt`, `Encrypt`, `GenerateDataKey`, `CreateGrant`, etc.); the key is immutable once a volume exists.\n\n## BYOK storage classes\n\nOn self-hosted clusters, volumes use the storage class `{performanceClass}-{fileSystemType}` (e.g. `general-purpose-ssd-ext4`) and the cluster needs a CSI-compatible driver. `spec.storageClassSuffix` selects an alternative `{performanceClass}-{fileSystemType}-{suffix}`, falling back to the unsuffixed class if it is not found.\n\n## Migrating a workload to stateful\n\nWorkload type is immutable, so adding an ext4/xfs volume to a serverless/standard workload means **delete + recreate as `stateful`** — destructive. Before deleting, confirm with the user: the public URL that 5xx's during the cutover, any internal callers that fail until recreate, runtime/in-memory state lost at delete, and that the recreate typically takes 2-5 min. Sequence: capture the spec (`cpln workload get WORKLOAD --gvc GVC -o yaml-slim > bak.yaml`) as a rollback artifact; apply the volumeset; delete the old workload; apply the new manifest with `spec.type: stateful` + the volume mount, **keeping the same name** to preserve URL/DNS/policy/identity links. For the deploy-wait pattern, see the `workload` skill.\n\n## Verify\n\n- `mcp__cpln__get_resource` (kind `volumeset`): `status.locations[].volumes[]` show per-volume `currentSize`, `currentBytesUsed`, `lifecycle` (expect `bound`), and snapshot counts; `status.usedByWorkload` names the bound workload.\n- After mounting, wait with `mcp__cpln__list_deployments` and `waitSeconds` until ready, and confirm the container's volume is mounted at the expected path.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| 400 mounting a volumeset | ext4/xfs on a serverless/standard workload | Use a `stateful` or `vm` workload (recreate to change type) |\n| 400 \"performanceClass / fileSystemType is immutable\" | Tried to change either on update | Create a new volumeset; migrate data via snapshot/restore |\n| 400 on mount with a path | Path is reserved (`/dev`, `/tmp`, `/var`, ...) | Mount elsewhere (e.g. `/data`, `/mnt/...`) |\n| HTTP 429 on expand | 4 expansions on that volume in the last 24 h | Wait for the oldest to age out; plan larger steps |\n| 400 on shrink | New size cannot hold used bytes (+5%) | Shrink less, or free space / snapshot then rebuild |\n| 400 \"The ratio between maxCpu and minCpu must be less than 4:1\" (same for memory) | `mountOptions.resources` spread past 4:1 or past 4000m / 4096Mi, counting the default that fills any bound you left out | Raise `minCpu` / `minMemory` or lower `maxCpu` / `maxMemory`, and always set both bounds of a pair |\n| 400 \"Workload X references non-existent volume sets\" | The volumeset in the same apply failed its own validation. `cpln apply` reports every failure and does not stop at the first, so the workload PUT still ran | Fix the volumeset error above it and re-apply the bundle; the ordering is already correct |\n| Snapshot fields rejected | Volumeset is `shared` | Snapshots need ext4/xfs |\n| Deployment stuck after mount | Volume provisioning (2 to 5 min on first deploy) | `list_deployments` with `waitSeconds`; `diagnose_workload` if it stays unready |\n"
}

SHA-256 of public snapshot: 9dc32b36f1bd12d16a79d6a1d8bf55f47529f04b1611a77cb15190776cf26ff3