diff --git a/docs/deployment_patterns.md b/docs/deployment_patterns.md index fb2e0063..b8a7b5ec 100644 --- a/docs/deployment_patterns.md +++ b/docs/deployment_patterns.md @@ -75,3 +75,40 @@ This overwrites your customizations with defaults from the stack's `commands.py` git pull # Get latest spec.yml from your operator repo laconic-so deployment --dir my-deployment restart ``` + +## Volume Persistence in k8s-kind + +k8s-kind has 3 storage layers: + +- **Docker Host**: The physical server running Docker +- **Kind Node**: A Docker container simulating a k8s node +- **Pod Container**: Your workload + +For k8s-kind, volumes with paths are mounted from Docker Host → Kind Node → Pod via extraMounts. + +| spec.yml volume | Storage Location | Survives Pod Restart | Survives Cluster Restart | +|-----------------|------------------|---------------------|-------------------------| +| `vol:` (empty) | Kind Node PVC | ✅ | ❌ | +| `vol: ./data/x` | Docker Host | ✅ | ✅ | +| `vol: /abs/path`| Docker Host | ✅ | ✅ | + +**Recommendation**: Always use paths for data you want to keep. Relative paths +(e.g., `./data/rpc-config`) resolve to `$DEPLOYMENT_DIR/data/rpc-config` on the +Docker Host. + +### Example + +```yaml +# In spec.yml +volumes: + rpc-config: ./data/rpc-config # Persists to $DEPLOYMENT_DIR/data/rpc-config + chain-data: ./data/chain # Persists to $DEPLOYMENT_DIR/data/chain + temp-cache: # Empty = Kind Node PVC (lost on cluster delete) +``` + +### The Antipattern + +Empty-path volumes appear persistent because they survive pod restarts (data lives +in Kind Node container). However, this data is lost when the kind cluster is +recreated. This "false persistence" has caused data loss when operators assumed +their data was safe. diff --git a/stack_orchestrator/deploy/deployment_create.py b/stack_orchestrator/deploy/deployment_create.py index ec15362f..12277043 100644 --- a/stack_orchestrator/deploy/deployment_create.py +++ b/stack_orchestrator/deploy/deployment_create.py @@ -522,10 +522,14 @@ def _check_volume_definitions(spec): for volume_name, volume_path in spec.get_volumes().items(): if volume_path: if not os.path.isabs(volume_path): - raise Exception( - f"Relative path {volume_path} for volume {volume_name} not " - f"supported for deployment type {spec.get_deployment_type()}" - ) + # For k8s-kind: allow relative paths, they'll be resolved + # by _make_absolute_host_path() during kind config generation + if not spec.is_kind_deployment(): + deploy_type = spec.get_deployment_type() + raise Exception( + f"Relative path {volume_path} for volume " + f"{volume_name} not supported for {deploy_type}" + ) @click.command() diff --git a/stack_orchestrator/deploy/k8s/cluster_info.py b/stack_orchestrator/deploy/k8s/cluster_info.py index 42c41b4b..89529b4e 100644 --- a/stack_orchestrator/deploy/k8s/cluster_info.py +++ b/stack_orchestrator/deploy/k8s/cluster_info.py @@ -352,11 +352,15 @@ class ClusterInfo: continue if not os.path.isabs(volume_path): - print( - f"WARNING: {volume_name}:{volume_path} is not absolute, " - "cannot bind volume." - ) - continue + # For k8s-kind, allow relative paths: + # - PV uses /mnt/{volume_name} (path inside kind node) + # - extraMounts resolve the relative path to Docker Host + if not self.spec.is_kind_deployment(): + print( + f"WARNING: {volume_name}:{volume_path} is not absolute, " + "cannot bind volume." + ) + continue if self.spec.is_kind_deployment(): host_path = client.V1HostPathVolumeSource(