keygen_rs/
machine.rs

1use crate::certificate::CertificateFileResponse;
2use crate::client::{Client, Response};
3use crate::errors::Error;
4use crate::machine_file::MachineFile;
5use crate::KeygenResponseData;
6use chrono::{DateTime, Utc};
7use futures::future::{BoxFuture, FutureExt};
8use futures::StreamExt;
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11use std::collections::HashMap;
12use std::sync::mpsc::{Receiver, Sender};
13use std::sync::Arc;
14use std::time::Duration;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub(crate) struct MachineAttributes {
18    pub fingerprint: String,
19    pub name: Option<String>,
20    pub platform: Option<String>,
21    pub hostname: Option<String>,
22    pub ip: Option<String>,
23    pub cores: Option<i32>,
24    pub metadata: Option<HashMap<String, Value>>,
25    #[serde(rename = "requireHeartbeat")]
26    pub require_heartbeat: bool,
27    #[serde(rename = "heartbeatStatus")]
28    pub heartbeat_status: String,
29    #[serde(rename = "heartbeatDuration")]
30    pub heartbeat_duration: Option<i32>,
31    pub created: DateTime<Utc>,
32    pub updated: DateTime<Utc>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub(crate) struct MachineResponse {
37    pub data: KeygenResponseData<MachineAttributes>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub(crate) struct MachinesResponse {
42    pub data: Vec<KeygenResponseData<MachineAttributes>>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Machine {
47    pub id: String,
48    pub fingerprint: String,
49    pub name: Option<String>,
50    pub platform: Option<String>,
51    pub hostname: Option<String>,
52    pub ip: Option<String>,
53    pub cores: Option<i32>,
54    pub metadata: Option<HashMap<String, Value>>,
55    #[serde(rename = "requireHeartbeat")]
56    pub require_heartbeat: bool,
57    #[serde(rename = "heartbeatStatus")]
58    pub heartbeat_status: String,
59    #[serde(rename = "heartbeatDuration")]
60    pub heartbeat_duration: Option<i32>,
61    pub created: DateTime<Utc>,
62    pub updated: DateTime<Utc>,
63    pub account_id: Option<String>,
64    pub environment_id: Option<String>,
65    pub product_id: Option<String>,
66    pub license_id: Option<String>,
67    pub owner_id: Option<String>,
68    pub group_id: Option<String>,
69}
70
71pub struct MachineCheckoutOpts {
72    pub ttl: Option<i64>,
73    pub include: Option<Vec<String>>,
74}
75
76impl MachineCheckoutOpts {
77    pub fn new() -> Self {
78        Self {
79            ttl: None,
80            include: None,
81        }
82    }
83
84    pub fn with_ttl(ttl: i64) -> Self {
85        Self {
86            ttl: Some(ttl),
87            include: None,
88        }
89    }
90
91    pub fn with_include(include: Vec<String>) -> Self {
92        Self {
93            ttl: None,
94            include: Some(include),
95        }
96    }
97}
98
99impl Default for MachineCheckoutOpts {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct MachineListFilters {
107    pub license: Option<String>,
108    pub user: Option<String>,
109    pub platform: Option<String>,
110    pub name: Option<String>,
111    pub fingerprint: Option<String>,
112    pub ip: Option<String>,
113    pub hostname: Option<String>,
114    pub product: Option<String>,
115    pub owner: Option<String>,
116    pub group: Option<String>,
117    pub metadata: Option<HashMap<String, Value>>,
118    pub page_number: Option<i32>,
119    pub page_size: Option<i32>,
120    pub limit: Option<i32>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct MachineCreateRequest {
125    pub fingerprint: String,
126    pub name: Option<String>,
127    pub platform: Option<String>,
128    pub hostname: Option<String>,
129    pub ip: Option<String>,
130    pub cores: Option<i32>,
131    pub metadata: Option<HashMap<String, Value>>,
132    pub license_id: String,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct MachineUpdateRequest {
137    pub name: Option<String>,
138    pub platform: Option<String>,
139    pub hostname: Option<String>,
140    pub ip: Option<String>,
141    pub cores: Option<i32>,
142    pub metadata: Option<HashMap<String, Value>>,
143}
144
145impl Machine {
146    pub(crate) fn from(data: KeygenResponseData<MachineAttributes>) -> Machine {
147        Machine {
148            id: data.id,
149            fingerprint: data.attributes.fingerprint,
150            name: data.attributes.name,
151            platform: data.attributes.platform,
152            hostname: data.attributes.hostname,
153            ip: data.attributes.ip,
154            cores: data.attributes.cores,
155            metadata: data.attributes.metadata,
156            require_heartbeat: data.attributes.require_heartbeat,
157            heartbeat_status: data.attributes.heartbeat_status,
158            heartbeat_duration: data.attributes.heartbeat_duration,
159            created: data.attributes.created,
160            updated: data.attributes.updated,
161            account_id: data
162                .relationships
163                .account
164                .as_ref()
165                .and_then(|a| a.data.as_ref().map(|d| d.id.clone())),
166            environment_id: data
167                .relationships
168                .environment
169                .as_ref()
170                .and_then(|e| e.data.as_ref().map(|d| d.id.clone())),
171            product_id: data
172                .relationships
173                .product
174                .as_ref()
175                .and_then(|p| p.data.as_ref().map(|d| d.id.clone())),
176            license_id: data
177                .relationships
178                .license
179                .as_ref()
180                .and_then(|l| l.data.as_ref().map(|d| d.id.clone())),
181            owner_id: data
182                .relationships
183                .owner
184                .as_ref()
185                .and_then(|o| o.data.as_ref().map(|d| d.id.clone())),
186            group_id: data
187                .relationships
188                .group
189                .as_ref()
190                .and_then(|g| g.data.as_ref().map(|d| d.id.clone())),
191        }
192    }
193
194    pub async fn deactivate(&self) -> Result<(), Error> {
195        let client = Client::default()?;
196        let _response = client
197            .delete::<(), serde_json::Value>(&format!("machines/{}", self.id), None::<&()>)
198            .await?;
199        Ok(())
200    }
201
202    pub async fn checkout(&self, options: &MachineCheckoutOpts) -> Result<MachineFile, Error> {
203        let client = Client::default()?;
204        let mut query = json!({
205            "encrypt": 1
206        });
207
208        if let Some(ttl) = options.ttl {
209            query["ttl"] = ttl.into();
210        }
211
212        if let Some(ref include) = options.include {
213            query["include"] = json!(include.join(","));
214        } else {
215            query["include"] = "license.entitlements".into();
216        }
217
218        let response = client
219            .post(
220                &format!("machines/{}/actions/check-out", self.id),
221                None::<&()>,
222                Some(&query),
223            )
224            .await?;
225
226        let machine_file_response: CertificateFileResponse = serde_json::from_value(response.body)?;
227        let machine_file = MachineFile::from(machine_file_response.data);
228        Ok(machine_file)
229    }
230
231    pub async fn ping(&self) -> Result<Machine, Error> {
232        let client: Client = Client::default()?;
233        let response: Response<MachineResponse> = client
234            .post(
235                &format!("machines/{}/actions/ping", self.id),
236                None::<&()>,
237                None::<&()>,
238            )
239            .await?;
240        let machine = Machine::from(response.body.data);
241        Ok(machine)
242    }
243
244    pub fn monitor(
245        self: Arc<Self>,
246        heartbeat_interval: Duration,
247        tx: Option<Sender<Result<Machine, Error>>>,
248        cancel_rx: Option<Receiver<()>>,
249    ) -> BoxFuture<'static, ()> {
250        async move {
251            let send = |result: Result<Machine, Error>| {
252                if let Some(tx) = &tx {
253                    tx.send(result).unwrap();
254                }
255            };
256
257            let mut interval_stream = futures::stream::unfold((), move |_| {
258                let delay = futures_timer::Delay::new(heartbeat_interval);
259                Box::pin(async move {
260                    delay.await;
261                    Some(((), ()))
262                })
263            });
264
265            send(self.ping().await);
266            while interval_stream.next().await.is_some() {
267                if let Some(ref rx) = cancel_rx {
268                    if rx.try_recv().is_ok() {
269                        break;
270                    }
271                }
272                send(self.ping().await);
273            }
274        }
275        .boxed()
276    }
277
278    /// Create a new machine
279    #[cfg(feature = "token")]
280    pub async fn create(request: MachineCreateRequest) -> Result<Machine, Error> {
281        let client = Client::default()?;
282
283        let mut attributes = serde_json::Map::new();
284        attributes.insert("fingerprint".to_string(), json!(request.fingerprint));
285
286        if let Some(name) = request.name {
287            attributes.insert("name".to_string(), json!(name));
288        }
289        if let Some(platform) = request.platform {
290            attributes.insert("platform".to_string(), json!(platform));
291        }
292        if let Some(hostname) = request.hostname {
293            attributes.insert("hostname".to_string(), json!(hostname));
294        }
295        if let Some(ip) = request.ip {
296            attributes.insert("ip".to_string(), json!(ip));
297        }
298        if let Some(cores) = request.cores {
299            attributes.insert("cores".to_string(), json!(cores));
300        }
301        if let Some(metadata) = request.metadata {
302            attributes.insert("metadata".to_string(), json!(metadata));
303        }
304
305        let body = json!({
306            "data": {
307                "type": "machines",
308                "attributes": attributes,
309                "relationships": {
310                    "license": {
311                        "data": {
312                            "type": "licenses",
313                            "id": request.license_id
314                        }
315                    }
316                }
317            }
318        });
319
320        let response = client.post("machines", Some(&body), None::<&()>).await?;
321        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
322        Ok(Machine::from(machine_response.data))
323    }
324
325    /// List machines with optional filters
326    #[cfg(feature = "token")]
327    pub async fn list(filters: Option<MachineListFilters>) -> Result<Vec<Machine>, Error> {
328        let client = Client::default()?;
329
330        let mut query_params = Vec::new();
331        if let Some(filters) = filters {
332            if let Some(license) = filters.license {
333                query_params.push(("license".to_string(), license));
334            }
335            if let Some(user) = filters.user {
336                query_params.push(("user".to_string(), user));
337            }
338            if let Some(platform) = filters.platform {
339                query_params.push(("platform".to_string(), platform));
340            }
341            if let Some(name) = filters.name {
342                query_params.push(("name".to_string(), name));
343            }
344            if let Some(fingerprint) = filters.fingerprint {
345                query_params.push(("fingerprint".to_string(), fingerprint));
346            }
347            if let Some(ip) = filters.ip {
348                query_params.push(("ip".to_string(), ip));
349            }
350            if let Some(hostname) = filters.hostname {
351                query_params.push(("hostname".to_string(), hostname));
352            }
353            if let Some(product) = filters.product {
354                query_params.push(("product".to_string(), product));
355            }
356            if let Some(owner) = filters.owner {
357                query_params.push(("owner".to_string(), owner));
358            }
359            if let Some(group) = filters.group {
360                query_params.push(("group".to_string(), group));
361            }
362            if let Some(metadata) = filters.metadata {
363                for (key, value) in metadata {
364                    query_params.push((format!("metadata[{key}]"), value.to_string()));
365                }
366            }
367            // Add pagination parameters
368            if let Some(page_number) = filters.page_number {
369                query_params.push(("page[number]".to_string(), page_number.to_string()));
370            }
371            if let Some(page_size) = filters.page_size {
372                query_params.push(("page[size]".to_string(), page_size.to_string()));
373            }
374            if let Some(limit) = filters.limit {
375                query_params.push(("limit".to_string(), limit.to_string()));
376            }
377        }
378
379        let query = if query_params.is_empty() {
380            None
381        } else {
382            Some(
383                query_params
384                    .into_iter()
385                    .collect::<HashMap<String, String>>(),
386            )
387        };
388
389        let response = client.get("machines", query.as_ref()).await?;
390        let machines_response: MachinesResponse = serde_json::from_value(response.body)?;
391        Ok(machines_response
392            .data
393            .into_iter()
394            .map(Machine::from)
395            .collect())
396    }
397
398    /// Get a machine by ID
399    #[cfg(feature = "token")]
400    pub async fn get(id: &str) -> Result<Machine, Error> {
401        let client = Client::default()?;
402        let endpoint = format!("machines/{id}");
403        let response = client.get(&endpoint, None::<&()>).await?;
404        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
405        Ok(Machine::from(machine_response.data))
406    }
407
408    /// Update a machine
409    #[cfg(feature = "token")]
410    pub async fn update(&self, request: MachineUpdateRequest) -> Result<Machine, Error> {
411        let client = Client::default()?;
412        let endpoint = format!("machines/{}", self.id);
413
414        let mut attributes = serde_json::Map::new();
415        if let Some(name) = request.name {
416            attributes.insert("name".to_string(), json!(name));
417        }
418        if let Some(platform) = request.platform {
419            attributes.insert("platform".to_string(), json!(platform));
420        }
421        if let Some(hostname) = request.hostname {
422            attributes.insert("hostname".to_string(), json!(hostname));
423        }
424        if let Some(ip) = request.ip {
425            attributes.insert("ip".to_string(), json!(ip));
426        }
427        if let Some(cores) = request.cores {
428            attributes.insert("cores".to_string(), json!(cores));
429        }
430        if let Some(metadata) = request.metadata {
431            attributes.insert("metadata".to_string(), json!(metadata));
432        }
433
434        let body = json!({
435            "data": {
436                "type": "machines",
437                "attributes": attributes
438            }
439        });
440
441        let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
442        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
443        Ok(Machine::from(machine_response.data))
444    }
445
446    /// Reset machine heartbeat
447    #[cfg(feature = "token")]
448    pub async fn reset(&self) -> Result<Machine, Error> {
449        let client = Client::default()?;
450        let endpoint = format!("machines/{}/actions/reset", self.id);
451        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
452        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
453        Ok(Machine::from(machine_response.data))
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::{
461        KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
462    };
463    use chrono::Utc;
464
465    #[test]
466    fn test_machine_relationships() {
467        // Test that all relationship IDs are properly extracted
468        let machine_data = KeygenResponseData {
469            id: "test-machine-id".to_string(),
470            r#type: "machines".to_string(),
471            attributes: MachineAttributes {
472                fingerprint: "test-fingerprint".to_string(),
473                name: Some("Test Machine".to_string()),
474                platform: Some("linux".to_string()),
475                hostname: Some("test-host".to_string()),
476                ip: Some("192.168.1.1".to_string()),
477                cores: Some(8),
478                metadata: Some(HashMap::new()),
479                require_heartbeat: true,
480                heartbeat_status: "ALIVE".to_string(),
481                heartbeat_duration: Some(3600),
482                created: Utc::now(),
483                updated: Utc::now(),
484            },
485            relationships: KeygenRelationships {
486                policy: None,
487                account: Some(KeygenRelationship {
488                    data: Some(KeygenRelationshipData {
489                        r#type: "accounts".to_string(),
490                        id: "test-account-id".to_string(),
491                    }),
492                    links: None,
493                }),
494                product: Some(KeygenRelationship {
495                    data: Some(KeygenRelationshipData {
496                        r#type: "products".to_string(),
497                        id: "test-product-id".to_string(),
498                    }),
499                    links: None,
500                }),
501                group: Some(KeygenRelationship {
502                    data: Some(KeygenRelationshipData {
503                        r#type: "groups".to_string(),
504                        id: "test-group-id".to_string(),
505                    }),
506                    links: None,
507                }),
508                owner: Some(KeygenRelationship {
509                    data: Some(KeygenRelationshipData {
510                        r#type: "users".to_string(),
511                        id: "test-owner-id".to_string(),
512                    }),
513                    links: None,
514                }),
515                users: None,
516                machines: None,
517                environment: Some(KeygenRelationship {
518                    data: Some(KeygenRelationshipData {
519                        r#type: "environments".to_string(),
520                        id: "test-environment-id".to_string(),
521                    }),
522                    links: None,
523                }),
524                license: Some(KeygenRelationship {
525                    data: Some(KeygenRelationshipData {
526                        r#type: "licenses".to_string(),
527                        id: "test-license-id".to_string(),
528                    }),
529                    links: None,
530                }),
531                other: HashMap::new(),
532            },
533        };
534
535        let machine = Machine::from(machine_data);
536
537        assert_eq!(machine.account_id, Some("test-account-id".to_string()));
538        assert_eq!(
539            machine.environment_id,
540            Some("test-environment-id".to_string())
541        );
542        assert_eq!(machine.product_id, Some("test-product-id".to_string()));
543        assert_eq!(machine.license_id, Some("test-license-id".to_string()));
544        assert_eq!(machine.owner_id, Some("test-owner-id".to_string()));
545        assert_eq!(machine.group_id, Some("test-group-id".to_string()));
546        assert_eq!(machine.id, "test-machine-id");
547        assert_eq!(machine.fingerprint, "test-fingerprint");
548    }
549
550    #[test]
551    fn test_machine_without_relationships() {
552        // Test that all relationship IDs are None when no relationships exist
553        let machine_data = KeygenResponseData {
554            id: "test-machine-id".to_string(),
555            r#type: "machines".to_string(),
556            attributes: MachineAttributes {
557                fingerprint: "test-fingerprint".to_string(),
558                name: Some("Test Machine".to_string()),
559                platform: Some("linux".to_string()),
560                hostname: Some("test-host".to_string()),
561                ip: Some("192.168.1.1".to_string()),
562                cores: Some(8),
563                metadata: Some(HashMap::new()),
564                require_heartbeat: true,
565                heartbeat_status: "ALIVE".to_string(),
566                heartbeat_duration: Some(3600),
567                created: Utc::now(),
568                updated: Utc::now(),
569            },
570            relationships: KeygenRelationships {
571                policy: None,
572                account: None,
573                product: None,
574                group: None,
575                owner: None,
576                users: None,
577                machines: None,
578                environment: None,
579                license: None,
580                other: HashMap::new(),
581            },
582        };
583
584        let machine = Machine::from(machine_data);
585
586        assert_eq!(machine.account_id, None);
587        assert_eq!(machine.environment_id, None);
588        assert_eq!(machine.product_id, None);
589        assert_eq!(machine.license_id, None);
590        assert_eq!(machine.owner_id, None);
591        assert_eq!(machine.group_id, None);
592    }
593}