1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! Volume management API DTOs.
//!
//! Wire-format types shared between the daemon's `/api/v1/volumes`
//! endpoints and SDK clients. Moved out of `zlayer-api` so SDK crates can
//! depend on them without pulling in the full server stack.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
/// Request body for `POST /api/v1/volumes`.
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct CreateVolumeRequest {
/// Volume name. Required. Must match `^[a-z0-9][a-z0-9_-]{0,63}$`.
pub name: String,
/// Optional size hint (humansize format: `"512Mi"`, `"10Gi"`).
/// Recorded in the sidecar for display and future quota enforcement.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
/// Optional storage tier. Accepts `"local"` (default), `"cached"`,
/// `"network"`, matching `zlayer_spec::StorageTier`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
/// Optional labels to attach to the volume.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub labels: Option<HashMap<String, String>>,
}
/// Full volume response shape used by the list, inspect, and create
/// endpoints.
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct VolumeInfo {
/// Volume name (directory name).
pub name: String,
/// Host filesystem path.
pub path: String,
/// Approximate size in bytes (sum of regular files in the volume
/// directory). `None` when the directory could not be walked, or for
/// freshly created empty volumes where `0` would be equally
/// informative.
#[serde(skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<u64>,
/// Labels from the sidecar. Empty when no sidecar is present.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub labels: HashMap<String, String>,
/// RFC 3339 creation timestamp. For volumes without a sidecar this is
/// the directory's mtime (best-effort).
pub created_at: String,
/// Container IDs currently mounting this volume. Empty when no
/// `VolumeUsageSource` is wired.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub in_use_by: Vec<String>,
}
/// Legacy response shape kept for backwards compatibility with older SDK
/// consumers that deserialize strictly. New consumers should use
/// [`VolumeInfo`]. `list_volumes` now returns [`VolumeInfo`] which is a
/// strict superset of the fields in `VolumeSummary`.
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct VolumeSummary {
/// Volume name (directory name).
pub name: String,
/// Host filesystem path.
pub path: String,
/// Approximate size in bytes.
pub size_bytes: Option<u64>,
}
/// Query parameters for the delete endpoint.
#[derive(Debug, Deserialize)]
pub struct DeleteVolumeQuery {
/// Force removal of a volume even when it is non-empty OR currently
/// in use by one or more containers. Default `false`.
#[serde(default)]
pub force: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_volume_info_serialization() {
let info = VolumeInfo {
name: "test-vol".to_string(),
path: "/data/volumes/test-vol".to_string(),
size_bytes: Some(1024),
labels: HashMap::from([("owner".to_string(), "zarc".to_string())]),
created_at: "2026-04-20T00:00:00Z".to_string(),
in_use_by: vec!["c1".to_string()],
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("test-vol"));
assert!(json.contains("1024"));
assert!(json.contains("owner"));
assert!(json.contains("in_use_by"));
}
#[test]
fn test_volume_summary_legacy_serialization() {
// Ensure the legacy DTO still serializes cleanly for any SDK that
// hasn't migrated yet.
let summary = VolumeSummary {
name: "legacy".to_string(),
path: "/data/volumes/legacy".to_string(),
size_bytes: Some(1024),
};
let json = serde_json::to_string(&summary).unwrap();
assert!(json.contains("legacy"));
}
#[test]
fn test_delete_volume_query_defaults() {
let query: DeleteVolumeQuery = serde_json::from_str("{}").unwrap();
assert!(!query.force);
}
#[test]
fn test_delete_volume_query_force() {
let query: DeleteVolumeQuery = serde_json::from_str(r#"{"force": true}"#).unwrap();
assert!(query.force);
}
}