Skip to main content

zlayer_types/api/
volumes.rs

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