1use sha2::Digest;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct TemplateConfig {
9 #[serde(default)]
11 pub template_id: String,
12 pub flake_ref: String,
13 #[serde(default = "default_profile")]
15 pub profile: String,
16 pub variants: Vec<TemplateVariant>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TemplateVariant {
21 #[serde(default)]
23 pub name: String,
24 pub role: String,
25 #[serde(default = "default_profile")]
26 pub profile: String,
27 pub vcpus: u8,
28 pub mem_mib: u32,
29 #[serde(default)]
30 pub data_disk_mib: u32,
31}
32
33fn default_profile() -> String {
34 "minimal".to_string()
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct TemplateSpec {
40 pub template_id: String,
41 pub flake_ref: String,
42 pub profile: String,
43 pub role: String,
44 pub vcpus: u8,
45 pub mem_mib: u32,
46 pub data_disk_mib: u32,
47 pub created_at: String,
48 pub updated_at: String,
49}
50
51pub fn templates_base_dir() -> String {
53 format!("{}/templates", crate::config::mvm_data_dir())
54}
55
56pub fn template_dir(template_id: &str) -> String {
57 format!("{}/{}", templates_base_dir(), template_id)
58}
59
60pub fn template_spec_path(template_id: &str) -> String {
61 format!("{}/template.json", template_dir(template_id))
62}
63
64pub fn template_artifacts_dir(template_id: &str) -> String {
66 format!("{}/artifacts", template_dir(template_id))
67}
68
69pub fn template_revision_dir(template_id: &str, revision: &str) -> String {
71 format!("{}/{}", template_artifacts_dir(template_id), revision)
72}
73
74pub fn template_current_symlink(template_id: &str) -> String {
76 format!("{}/current", template_dir(template_id))
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct TemplateRevision {
81 pub revision_hash: String,
82 pub flake_ref: String,
83 pub flake_lock_hash: String,
84 pub artifact_paths: crate::pool::ArtifactPaths,
85 pub built_at: String,
86 pub profile: String,
87 pub role: String,
88 pub vcpus: u8,
89 pub mem_mib: u32,
90 pub data_disk_mib: u32,
91}
92
93impl TemplateRevision {
94 pub fn cache_key(&self) -> String {
97 let mut hasher = sha2::Sha256::new();
98 hasher.update(self.flake_lock_hash.as_bytes());
99 hasher.update(b":");
100 hasher.update(self.profile.as_bytes());
101 hasher.update(b":");
102 hasher.update(self.role.as_bytes());
103 format!("{:x}", hasher.finalize())
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::pool::ArtifactPaths;
111
112 fn make_revision(flake_lock_hash: &str, profile: &str, role: &str) -> TemplateRevision {
113 TemplateRevision {
114 revision_hash: "abc123".to_string(),
115 flake_ref: ".".to_string(),
116 flake_lock_hash: flake_lock_hash.to_string(),
117 artifact_paths: ArtifactPaths {
118 vmlinux: "vmlinux".to_string(),
119 rootfs: "rootfs.ext4".to_string(),
120 fc_base_config: "fc-base.json".to_string(),
121 },
122 built_at: "2025-01-01T00:00:00Z".to_string(),
123 profile: profile.to_string(),
124 role: role.to_string(),
125 vcpus: 2,
126 mem_mib: 1024,
127 data_disk_mib: 0,
128 }
129 }
130
131 #[test]
132 fn same_inputs_same_cache_key() {
133 let a = make_revision("lock1", "minimal", "worker");
134 let b = make_revision("lock1", "minimal", "worker");
135 assert_eq!(a.cache_key(), b.cache_key());
136 }
137
138 #[test]
139 fn different_profile_different_cache_key() {
140 let a = make_revision("lock1", "minimal", "worker");
141 let b = make_revision("lock1", "full", "worker");
142 assert_ne!(a.cache_key(), b.cache_key());
143 }
144
145 #[test]
146 fn different_role_different_cache_key() {
147 let a = make_revision("lock1", "minimal", "worker");
148 let b = make_revision("lock1", "minimal", "gateway");
149 assert_ne!(a.cache_key(), b.cache_key());
150 }
151
152 #[test]
153 fn different_flake_different_cache_key() {
154 let a = make_revision("lock1", "minimal", "worker");
155 let b = make_revision("lock2", "minimal", "worker");
156 assert_ne!(a.cache_key(), b.cache_key());
157 }
158
159 #[test]
160 fn cache_key_depends_on_flake_lock_not_revision_hash() {
161 let mut a = make_revision("same-lock", "minimal", "worker");
162 a.revision_hash = "rev-aaa".to_string();
163 let mut b = make_revision("same-lock", "minimal", "worker");
164 b.revision_hash = "rev-zzz".to_string();
165 assert_eq!(a.cache_key(), b.cache_key());
167 }
168}