1use serde::{de::DeserializeOwned, Deserialize, Serialize};
9
10pub const MAGIC_BYTES: &[u8; 8] = b"WLCFG001";
12
13pub const FOOTER_SIZE: usize = 12;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct DeployCliConfig {
20 #[serde(skip_serializing_if = "Option::is_none")]
23 pub token: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub deployment_group_id: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub default_platform: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub api_base_url: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub agent_binary_url: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub machine_bundle_url: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub token_env_var: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
45 pub name: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub display_name: Option<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct OperatorConfig {
55 #[serde(skip_serializing_if = "Option::is_none")]
58 pub manager_url: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub token: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub deployment_id: Option<String>,
65 #[serde(default = "default_sync_interval")]
67 pub sync_interval_secs: u64,
68 #[serde(skip_serializing_if = "Option::is_none")]
71 pub name: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub brand: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub display_name: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub env_prefix: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub label_domain: Option<String>,
84}
85
86fn default_sync_interval() -> u64 {
87 30
88}
89
90pub fn load_embedded_config<T: DeserializeOwned>() -> Result<Option<T>, EmbeddedConfigError> {
97 let exe_path = std::env::current_exe().map_err(EmbeddedConfigError::Io)?;
98 load_embedded_config_from_path(&exe_path)
99}
100
101pub fn load_embedded_config_from_path<T: DeserializeOwned>(
103 path: &std::path::Path,
104) -> Result<Option<T>, EmbeddedConfigError> {
105 let data = std::fs::read(path).map_err(EmbeddedConfigError::Io)?;
106
107 if data.len() < FOOTER_SIZE {
108 return Ok(None);
109 }
110
111 let magic_start = data.len() - MAGIC_BYTES.len();
113 if &data[magic_start..] != MAGIC_BYTES {
114 return Ok(None);
115 }
116
117 let len_start = magic_start - 4;
119 let len_bytes: [u8; 4] = data[len_start..magic_start]
120 .try_into()
121 .map_err(|_| EmbeddedConfigError::InvalidFormat("invalid length bytes".into()))?;
122 let json_len = u32::from_le_bytes(len_bytes) as usize;
123
124 if json_len == 0 || len_start < json_len {
125 return Err(EmbeddedConfigError::InvalidFormat(
126 "config length exceeds binary size".into(),
127 ));
128 }
129
130 let json_start = len_start - json_len;
131 let json_bytes = &data[json_start..len_start];
132
133 let config: T =
134 serde_json::from_slice(json_bytes).map_err(EmbeddedConfigError::Deserialization)?;
135
136 Ok(Some(config))
137}
138
139pub fn append_embedded_config<T: Serialize>(
143 binary_data: &[u8],
144 config: &T,
145) -> Result<Vec<u8>, EmbeddedConfigError> {
146 let json_bytes = serde_json::to_vec(config).map_err(EmbeddedConfigError::Deserialization)?;
147 let json_len = json_bytes.len() as u32;
148
149 let mut result = Vec::with_capacity(binary_data.len() + json_bytes.len() + FOOTER_SIZE);
150 result.extend_from_slice(binary_data);
151 result.extend_from_slice(&json_bytes);
152 result.extend_from_slice(&json_len.to_le_bytes());
153 result.extend_from_slice(MAGIC_BYTES);
154
155 Ok(result)
156}
157
158#[derive(Debug)]
160pub enum EmbeddedConfigError {
161 Io(std::io::Error),
162 InvalidFormat(String),
163 Deserialization(serde_json::Error),
164}
165
166impl std::fmt::Display for EmbeddedConfigError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 Self::Io(e) => write!(f, "IO error reading embedded config: {}", e),
170 Self::InvalidFormat(msg) => write!(f, "invalid embedded config format: {}", msg),
171 Self::Deserialization(e) => write!(f, "failed to deserialize embedded config: {}", e),
172 }
173 }
174}
175
176impl std::error::Error for EmbeddedConfigError {}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_roundtrip_deploy_cli_config() {
184 let config = DeployCliConfig {
185 token: Some("ax_dg_abc123".into()),
186 deployment_group_id: Some("dg_xyz".into()),
187 display_name: Some("Production".into()),
188 default_platform: Some("aws".into()),
189 api_base_url: Some("https://api.example.com".into()),
190 agent_binary_url: Some("https://packages.example.com/acme/agent".into()),
191 machine_bundle_url: Some(
192 "https://packages.example.com/acme/machine-bundle.json".into(),
193 ),
194 token_env_var: Some("ACME_DEPLOYMENT_TOKEN".into()),
195 name: Some("acme-deploy".into()),
196 };
197
198 let binary = b"fake binary content";
199 let embedded = append_embedded_config(binary, &config).unwrap();
200
201 let loaded: Option<DeployCliConfig> =
202 load_embedded_config_from_path_bytes(&embedded).unwrap();
203 let loaded = loaded.unwrap();
204
205 assert_eq!(loaded.token, config.token);
206 assert_eq!(loaded.deployment_group_id, config.deployment_group_id);
207 assert_eq!(loaded.default_platform, config.default_platform);
208 assert_eq!(loaded.api_base_url, config.api_base_url);
209 assert_eq!(loaded.agent_binary_url, config.agent_binary_url);
210 assert_eq!(loaded.machine_bundle_url, config.machine_bundle_url);
211 assert_eq!(loaded.token_env_var, config.token_env_var);
212 assert_eq!(loaded.display_name, config.display_name);
213 assert_eq!(loaded.name, config.name);
214 }
215
216 #[test]
217 fn test_no_embedded_config() {
218 let binary = b"just a regular binary";
219 let result: Option<DeployCliConfig> = load_embedded_config_from_path_bytes(binary).unwrap();
220 assert!(result.is_none());
221 }
222
223 #[test]
224 fn test_roundtrip_operator_config() {
225 let config = OperatorConfig {
226 manager_url: Some("https://manager.example.com".into()),
227 token: Some("ax_dep_operator123".into()),
228 deployment_id: Some("dep_abc".into()),
229 sync_interval_secs: 60,
230 name: Some("acme-operator".into()),
231 brand: Some("acme".into()),
232 display_name: Some("Acme Operator".into()),
233 env_prefix: Some("ACME".into()),
234 label_domain: Some("acme.dev".into()),
235 };
236
237 let binary = b"operator binary";
238 let embedded = append_embedded_config(binary, &config).unwrap();
239
240 let loaded: Option<OperatorConfig> =
241 load_embedded_config_from_path_bytes(&embedded).unwrap();
242 let loaded = loaded.unwrap();
243
244 assert_eq!(loaded.manager_url, config.manager_url);
245 assert_eq!(loaded.deployment_id, config.deployment_id);
246 assert_eq!(loaded.sync_interval_secs, 60);
247 assert_eq!(loaded.name, config.name);
248 assert_eq!(loaded.brand, config.brand);
249 assert_eq!(loaded.display_name, config.display_name);
250 assert_eq!(loaded.env_prefix, config.env_prefix);
251 assert_eq!(loaded.label_domain, config.label_domain);
252 }
253
254 fn load_embedded_config_from_path_bytes<T: DeserializeOwned>(
256 data: &[u8],
257 ) -> Result<Option<T>, EmbeddedConfigError> {
258 if data.len() < FOOTER_SIZE {
259 return Ok(None);
260 }
261
262 let magic_start = data.len() - MAGIC_BYTES.len();
263 if &data[magic_start..] != MAGIC_BYTES {
264 return Ok(None);
265 }
266
267 let len_start = magic_start - 4;
268 let len_bytes: [u8; 4] = data[len_start..magic_start]
269 .try_into()
270 .map_err(|_| EmbeddedConfigError::InvalidFormat("invalid length bytes".into()))?;
271 let json_len = u32::from_le_bytes(len_bytes) as usize;
272
273 if json_len == 0 || len_start < json_len {
274 return Err(EmbeddedConfigError::InvalidFormat(
275 "config length exceeds binary size".into(),
276 ));
277 }
278
279 let json_start = len_start - json_len;
280 let json_bytes = &data[json_start..len_start];
281
282 let config: T =
283 serde_json::from_slice(json_bytes).map_err(EmbeddedConfigError::Deserialization)?;
284
285 Ok(Some(config))
286 }
287}