1use crate::certificate::CertificateFileResponse;
7use crate::client::{Client, ClientOptions, Response};
8use crate::config::get_config;
9use crate::config::KeygenConfig;
10use crate::errors::Error;
11use crate::insert_optional;
12use crate::machine_file::MachineFile;
13use crate::KeygenResponseData;
14use chrono::{DateTime, Utc};
15#[cfg(not(target_arch = "wasm32"))]
16use futures::future::{BoxFuture, FutureExt};
17use serde::{Deserialize, Serialize};
18use serde_json::{json, Value};
19use std::collections::HashMap;
20use std::sync::Arc;
21#[cfg(not(target_arch = "wasm32"))]
22use std::time::Duration;
23#[cfg(not(target_arch = "wasm32"))]
24use tokio::sync::mpsc;
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
29pub enum HeartbeatStatus {
30 Alive,
31 Dead,
32 NotStarted,
33 Resurrected,
34}
35
36impl HeartbeatStatus {
37 pub fn parse(s: &str) -> Option<Self> {
39 match s.to_uppercase().as_str() {
40 "ALIVE" => Some(Self::Alive),
41 "DEAD" => Some(Self::Dead),
42 "NOT_STARTED" => Some(Self::NotStarted),
43 "RESURRECTED" => Some(Self::Resurrected),
44 _ => None,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub(crate) struct MachineAttributes {
51 pub fingerprint: String,
52 pub name: Option<String>,
53 pub platform: Option<String>,
54 pub hostname: Option<String>,
55 pub ip: Option<String>,
56 pub cores: Option<i32>,
57 pub metadata: Option<HashMap<String, Value>>,
58 #[serde(rename = "requireHeartbeat")]
59 pub require_heartbeat: bool,
60 #[serde(rename = "heartbeatStatus")]
61 pub heartbeat_status: String,
62 #[serde(rename = "heartbeatDuration")]
63 pub heartbeat_duration: Option<i32>,
64 pub created: DateTime<Utc>,
65 pub updated: DateTime<Utc>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub(crate) struct MachineResponse {
70 pub data: KeygenResponseData<MachineAttributes>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub(crate) struct MachinesResponse {
75 pub data: Vec<KeygenResponseData<MachineAttributes>>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct Machine {
80 pub id: String,
81 pub fingerprint: String,
82 pub name: Option<String>,
83 pub platform: Option<String>,
84 pub hostname: Option<String>,
85 pub ip: Option<String>,
86 pub cores: Option<i32>,
87 pub metadata: Option<HashMap<String, Value>>,
88 #[serde(rename = "requireHeartbeat")]
89 pub require_heartbeat: bool,
90 #[serde(rename = "heartbeatStatus")]
91 pub heartbeat_status: String,
92 #[serde(rename = "heartbeatDuration")]
93 pub heartbeat_duration: Option<i32>,
94 pub created: DateTime<Utc>,
95 pub updated: DateTime<Utc>,
96 pub account_id: Option<String>,
97 pub environment_id: Option<String>,
98 pub product_id: Option<String>,
99 pub license_id: Option<String>,
100 pub owner_id: Option<String>,
101 pub group_id: Option<String>,
102 #[serde(skip)]
103 pub config: Option<Arc<KeygenConfig>>,
104}
105
106#[derive(Debug, Clone, Default)]
107pub struct MachineCheckoutOpts {
108 pub ttl: Option<i64>,
109 pub include: Option<Vec<String>>,
110}
111
112impl MachineCheckoutOpts {
113 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn with_ttl(ttl: i64) -> Self {
118 Self {
119 ttl: Some(ttl),
120 ..Self::default()
121 }
122 }
123
124 pub fn with_include(include: Vec<String>) -> Self {
125 Self {
126 include: Some(include),
127 ..Self::default()
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct MachineListFilters {
134 pub license: Option<String>,
135 pub user: Option<String>,
136 pub platform: Option<String>,
137 pub name: Option<String>,
138 pub fingerprint: Option<String>,
139 pub ip: Option<String>,
140 pub hostname: Option<String>,
141 pub product: Option<String>,
142 pub owner: Option<String>,
143 pub group: Option<String>,
144 pub policy: Option<String>,
145 pub key: Option<String>,
146 pub metadata: Option<HashMap<String, Value>>,
147 pub page_number: Option<i32>,
148 pub page_size: Option<i32>,
149 pub limit: Option<i32>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct MachineCreateRequest {
154 pub fingerprint: String,
155 pub name: Option<String>,
156 pub platform: Option<String>,
157 pub hostname: Option<String>,
158 pub ip: Option<String>,
159 pub cores: Option<i32>,
160 pub metadata: Option<HashMap<String, Value>>,
161 pub license_id: String,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct MachineUpdateRequest {
166 pub name: Option<String>,
167 pub platform: Option<String>,
168 pub hostname: Option<String>,
169 pub ip: Option<String>,
170 pub cores: Option<i32>,
171 pub metadata: Option<HashMap<String, Value>>,
172}
173
174impl Machine {
175 pub(crate) fn from(data: KeygenResponseData<MachineAttributes>) -> Machine {
176 Machine {
177 id: data.id,
178 fingerprint: data.attributes.fingerprint,
179 name: data.attributes.name,
180 platform: data.attributes.platform,
181 hostname: data.attributes.hostname,
182 ip: data.attributes.ip,
183 cores: data.attributes.cores,
184 metadata: data.attributes.metadata,
185 require_heartbeat: data.attributes.require_heartbeat,
186 heartbeat_status: data.attributes.heartbeat_status,
187 heartbeat_duration: data.attributes.heartbeat_duration,
188 created: data.attributes.created,
189 updated: data.attributes.updated,
190 account_id: data.relationships.account_id(),
191 environment_id: data.relationships.environment_id(),
192 product_id: data.relationships.product_id(),
193 license_id: data.relationships.license_id(),
194 owner_id: data.relationships.owner_id(),
195 group_id: data.relationships.group_id(),
196 config: None,
197 }
198 }
199
200 pub fn with_config(mut self, config: KeygenConfig) -> Self {
202 self.config = Some(Arc::new(config));
203 self
204 }
205
206 fn get_client(&self) -> Result<Client, Error> {
208 let config = if let Some(ref cfg) = self.config {
209 cfg.as_ref().clone()
210 } else {
211 get_config()?
212 };
213 Client::new(ClientOptions::from(config))
214 }
215
216 pub async fn deactivate(&self) -> Result<(), Error> {
217 let client = self.get_client()?;
218 let _response = client
219 .delete::<(), serde_json::Value>(&format!("machines/{}", self.id), None::<&()>)
220 .await?;
221 Ok(())
222 }
223
224 pub async fn checkout(&self, options: &MachineCheckoutOpts) -> Result<MachineFile, Error> {
225 let mut query = json!({
226 "encrypt": 1
227 });
228
229 if let Some(ttl) = options.ttl {
230 query["ttl"] = ttl.into();
231 }
232
233 if let Some(ref include) = options.include {
234 query["include"] = json!(include.join(","));
235 } else {
236 query["include"] = "license.entitlements".into();
237 }
238
239 let client = self.get_client()?;
240 let response = client
241 .post(
242 &format!("machines/{}/actions/check-out", self.id),
243 None::<&()>,
244 Some(&query),
245 )
246 .await?;
247
248 let machine_file_response: CertificateFileResponse = serde_json::from_value(response.body)?;
249 let machine_file = MachineFile::from(machine_file_response.data);
250 Ok(machine_file)
251 }
252
253 pub async fn ping(&self) -> Result<Machine, Error> {
254 let client = self.get_client()?;
255 let response: Response<MachineResponse> = client
256 .post(
257 &format!("machines/{}/actions/ping", self.id),
258 None::<&()>,
259 None::<&()>,
260 )
261 .await?;
262 let machine = Machine::from(response.body.data).with_config(
263 self.config
264 .as_ref()
265 .ok_or(Error::MissingConfiguration)?
266 .as_ref()
267 .clone(),
268 );
269 Ok(machine)
270 }
271
272 #[cfg(not(target_arch = "wasm32"))]
273 pub fn monitor(
274 self: Arc<Self>,
275 heartbeat_interval: Duration,
276 tx: Option<mpsc::Sender<Result<Machine, Error>>>,
277 mut cancel_rx: Option<mpsc::Receiver<()>>,
278 ) -> BoxFuture<'static, ()> {
279 async move {
280 async fn send(
281 tx: &Option<mpsc::Sender<Result<Machine, Error>>>,
282 result: Result<Machine, Error>,
283 ) {
284 if let Some(tx) = tx {
285 let _ = tx.send(result).await;
286 }
287 }
288
289 let mut interval = tokio::time::interval(heartbeat_interval);
290 interval.tick().await;
291
292 send(&tx, self.ping().await).await;
293
294 loop {
295 tokio::select! {
296 _ = interval.tick() => {
297 send(&tx, self.ping().await).await;
298 }
299 _ = async {
300 if let Some(ref mut rx) = cancel_rx {
301 rx.recv().await
302 } else {
303 std::future::pending::<Option<()>>().await
304 }
305 } => {
306 break;
307 }
308 }
309 }
310 }
311 .boxed()
312 }
313
314 #[cfg(feature = "token")]
316 pub async fn create(request: MachineCreateRequest) -> Result<Machine, Error> {
317 let config = get_config()?;
318 let client = Client::new(ClientOptions::from(config))?;
319 let mut attributes = serde_json::Map::new();
320 attributes.insert("fingerprint".to_string(), json!(request.fingerprint));
321
322 insert_optional(&mut attributes, "name", request.name)?;
323 insert_optional(&mut attributes, "platform", request.platform)?;
324 insert_optional(&mut attributes, "hostname", request.hostname)?;
325 insert_optional(&mut attributes, "ip", request.ip)?;
326 insert_optional(&mut attributes, "cores", request.cores)?;
327 insert_optional(&mut attributes, "metadata", request.metadata)?;
328
329 let body = json!({
330 "data": {
331 "type": "machines",
332 "attributes": attributes,
333 "relationships": {
334 "license": {
335 "data": {
336 "type": "licenses",
337 "id": request.license_id
338 }
339 }
340 }
341 }
342 });
343
344 let response = client.post("machines", Some(&body), None::<&()>).await?;
345 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
346 Ok(Machine::from(machine_response.data))
347 }
348
349 #[cfg(feature = "token")]
351 pub async fn list(filters: Option<MachineListFilters>) -> Result<Vec<Machine>, Error> {
352 let config = get_config()?;
353 let client = Client::new(ClientOptions::from(config))?;
354 let mut query_params = Vec::new();
355 if let Some(filters) = filters {
356 if let Some(license) = filters.license {
357 query_params.push(("license".to_string(), license));
358 }
359 if let Some(user) = filters.user {
360 query_params.push(("user".to_string(), user));
361 }
362 if let Some(platform) = filters.platform {
363 query_params.push(("platform".to_string(), platform));
364 }
365 if let Some(name) = filters.name {
366 query_params.push(("name".to_string(), name));
367 }
368 if let Some(fingerprint) = filters.fingerprint {
369 query_params.push(("fingerprint".to_string(), fingerprint));
370 }
371 if let Some(ip) = filters.ip {
372 query_params.push(("ip".to_string(), ip));
373 }
374 if let Some(hostname) = filters.hostname {
375 query_params.push(("hostname".to_string(), hostname));
376 }
377 if let Some(product) = filters.product {
378 query_params.push(("product".to_string(), product));
379 }
380 if let Some(owner) = filters.owner {
381 query_params.push(("owner".to_string(), owner));
382 }
383 if let Some(group) = filters.group {
384 query_params.push(("group".to_string(), group));
385 }
386 if let Some(policy) = filters.policy {
387 query_params.push(("policy".to_string(), policy));
388 }
389 if let Some(key) = filters.key {
390 query_params.push(("key".to_string(), key));
391 }
392 if let Some(metadata) = filters.metadata {
393 for (key, value) in metadata {
394 query_params.push((format!("metadata[{key}]"), value.to_string()));
395 }
396 }
397 if let Some(page_number) = filters.page_number {
399 query_params.push(("page[number]".to_string(), page_number.to_string()));
400 }
401 if let Some(page_size) = filters.page_size {
402 query_params.push(("page[size]".to_string(), page_size.to_string()));
403 }
404 if let Some(limit) = filters.limit {
405 query_params.push(("limit".to_string(), limit.to_string()));
406 }
407 }
408
409 let query = if query_params.is_empty() {
410 None
411 } else {
412 Some(
413 query_params
414 .into_iter()
415 .collect::<HashMap<String, String>>(),
416 )
417 };
418
419 let response = client.get("machines", query.as_ref()).await?;
420 let machines_response: MachinesResponse = serde_json::from_value(response.body)?;
421 Ok(machines_response
422 .data
423 .into_iter()
424 .map(Machine::from)
425 .collect())
426 }
427
428 #[cfg(feature = "token")]
430 pub async fn get(id: &str) -> Result<Machine, Error> {
431 let config = get_config()?;
432 let client = Client::new(ClientOptions::from(config))?;
433 let endpoint = format!("machines/{id}");
434 let response = client.get(&endpoint, None::<&()>).await?;
435 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
436 Ok(Machine::from(machine_response.data))
437 }
438
439 #[cfg(feature = "token")]
441 pub async fn update(&self, request: MachineUpdateRequest) -> Result<Machine, Error> {
442 let client = self.get_client()?;
443 let endpoint = format!("machines/{}", self.id);
444
445 let mut attributes = serde_json::Map::new();
446 insert_optional(&mut attributes, "name", request.name)?;
447 insert_optional(&mut attributes, "platform", request.platform)?;
448 insert_optional(&mut attributes, "hostname", request.hostname)?;
449 insert_optional(&mut attributes, "ip", request.ip)?;
450 insert_optional(&mut attributes, "cores", request.cores)?;
451 insert_optional(&mut attributes, "metadata", request.metadata)?;
452
453 let body = json!({
454 "data": {
455 "type": "machines",
456 "attributes": attributes
457 }
458 });
459
460 let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
461 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
462 Ok(Machine::from(machine_response.data))
463 }
464
465 #[cfg(feature = "token")]
467 pub async fn reset(&self) -> Result<Machine, Error> {
468 let client = self.get_client()?;
469 let endpoint = format!("machines/{}/actions/reset", self.id);
470 let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
471 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
472 Ok(Machine::from(machine_response.data))
473 }
474
475 #[cfg(feature = "token")]
477 pub async fn change_owner(&self, owner_id: &str) -> Result<Machine, Error> {
478 let client = self.get_client()?;
479 let endpoint = format!("machines/{}/owner", self.id);
480 let body = json!({
481 "data": {
482 "type": "users",
483 "id": owner_id
484 }
485 });
486 let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
487 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
488 Ok(Machine::from(machine_response.data))
489 }
490
491 #[cfg(feature = "token")]
493 pub async fn change_group(&self, group_id: &str) -> Result<Machine, Error> {
494 let client = self.get_client()?;
495 let endpoint = format!("machines/{}/group", self.id);
496 let body = json!({
497 "data": {
498 "type": "groups",
499 "id": group_id
500 }
501 });
502 let response = client.put(&endpoint, Some(&body), None::<&()>).await?;
503 let machine_response: MachineResponse = serde_json::from_value(response.body)?;
504 Ok(Machine::from(machine_response.data))
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::{
512 KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
513 };
514 use chrono::Utc;
515
516 #[test]
517 fn test_machine_relationships() {
518 let machine_data = KeygenResponseData {
520 id: "test-machine-id".to_string(),
521 r#type: "machines".to_string(),
522 attributes: MachineAttributes {
523 fingerprint: "test-fingerprint".to_string(),
524 name: Some("Test Machine".to_string()),
525 platform: Some("linux".to_string()),
526 hostname: Some("test-host".to_string()),
527 ip: Some("192.168.1.1".to_string()),
528 cores: Some(8),
529 metadata: Some(HashMap::new()),
530 require_heartbeat: true,
531 heartbeat_status: "ALIVE".to_string(),
532 heartbeat_duration: Some(3600),
533 created: Utc::now(),
534 updated: Utc::now(),
535 },
536 relationships: KeygenRelationships {
537 policy: None,
538 account: Some(KeygenRelationship {
539 data: Some(KeygenRelationshipData {
540 r#type: "accounts".to_string(),
541 id: "test-account-id".to_string(),
542 }),
543 links: None,
544 }),
545 product: Some(KeygenRelationship {
546 data: Some(KeygenRelationshipData {
547 r#type: "products".to_string(),
548 id: "test-product-id".to_string(),
549 }),
550 links: None,
551 }),
552 group: Some(KeygenRelationship {
553 data: Some(KeygenRelationshipData {
554 r#type: "groups".to_string(),
555 id: "test-group-id".to_string(),
556 }),
557 links: None,
558 }),
559 owner: Some(KeygenRelationship {
560 data: Some(KeygenRelationshipData {
561 r#type: "users".to_string(),
562 id: "test-owner-id".to_string(),
563 }),
564 links: None,
565 }),
566 users: None,
567 machines: None,
568 environment: Some(KeygenRelationship {
569 data: Some(KeygenRelationshipData {
570 r#type: "environments".to_string(),
571 id: "test-environment-id".to_string(),
572 }),
573 links: None,
574 }),
575 license: Some(KeygenRelationship {
576 data: Some(KeygenRelationshipData {
577 r#type: "licenses".to_string(),
578 id: "test-license-id".to_string(),
579 }),
580 links: None,
581 }),
582 release: None,
583 other: HashMap::new(),
584 },
585 };
586
587 let machine = Machine::from(machine_data);
588
589 assert_eq!(machine.account_id, Some("test-account-id".to_string()));
590 assert_eq!(
591 machine.environment_id,
592 Some("test-environment-id".to_string())
593 );
594 assert_eq!(machine.product_id, Some("test-product-id".to_string()));
595 assert_eq!(machine.license_id, Some("test-license-id".to_string()));
596 assert_eq!(machine.owner_id, Some("test-owner-id".to_string()));
597 assert_eq!(machine.group_id, Some("test-group-id".to_string()));
598 assert_eq!(machine.id, "test-machine-id");
599 assert_eq!(machine.fingerprint, "test-fingerprint");
600 }
601
602 #[test]
603 fn test_machine_without_relationships() {
604 let machine_data = KeygenResponseData {
606 id: "test-machine-id".to_string(),
607 r#type: "machines".to_string(),
608 attributes: MachineAttributes {
609 fingerprint: "test-fingerprint".to_string(),
610 name: Some("Test Machine".to_string()),
611 platform: Some("linux".to_string()),
612 hostname: Some("test-host".to_string()),
613 ip: Some("192.168.1.1".to_string()),
614 cores: Some(8),
615 metadata: Some(HashMap::new()),
616 require_heartbeat: true,
617 heartbeat_status: "ALIVE".to_string(),
618 heartbeat_duration: Some(3600),
619 created: Utc::now(),
620 updated: Utc::now(),
621 },
622 relationships: KeygenRelationships {
623 policy: None,
624 account: None,
625 product: None,
626 group: None,
627 owner: None,
628 users: None,
629 machines: None,
630 environment: None,
631 license: None,
632 release: None,
633 other: HashMap::new(),
634 },
635 };
636
637 let machine = Machine::from(machine_data);
638
639 assert_eq!(machine.account_id, None);
640 assert_eq!(machine.environment_id, None);
641 assert_eq!(machine.product_id, None);
642 assert_eq!(machine.license_id, None);
643 assert_eq!(machine.owner_id, None);
644 assert_eq!(machine.group_id, None);
645 }
646
647 #[test]
648 fn test_heartbeat_status_parse() {
649 assert_eq!(
650 HeartbeatStatus::parse("ALIVE"),
651 Some(HeartbeatStatus::Alive)
652 );
653 assert_eq!(HeartbeatStatus::parse("DEAD"), Some(HeartbeatStatus::Dead));
654 assert_eq!(
655 HeartbeatStatus::parse("NOT_STARTED"),
656 Some(HeartbeatStatus::NotStarted)
657 );
658 assert_eq!(
659 HeartbeatStatus::parse("RESURRECTED"),
660 Some(HeartbeatStatus::Resurrected)
661 );
662 assert_eq!(HeartbeatStatus::parse("UNKNOWN"), None);
663 }
664
665 #[test]
666 fn test_heartbeat_status_parse_case_insensitive() {
667 assert_eq!(
668 HeartbeatStatus::parse("alive"),
669 Some(HeartbeatStatus::Alive)
670 );
671 assert_eq!(HeartbeatStatus::parse("dead"), Some(HeartbeatStatus::Dead));
672 assert_eq!(
673 HeartbeatStatus::parse("not_started"),
674 Some(HeartbeatStatus::NotStarted)
675 );
676 assert_eq!(
677 HeartbeatStatus::parse("resurrected"),
678 Some(HeartbeatStatus::Resurrected)
679 );
680 }
681
682 #[test]
683 fn test_heartbeat_status_serialize() {
684 assert_eq!(
685 serde_json::to_string(&HeartbeatStatus::Alive).unwrap(),
686 "\"ALIVE\""
687 );
688 assert_eq!(
689 serde_json::to_string(&HeartbeatStatus::Dead).unwrap(),
690 "\"DEAD\""
691 );
692 assert_eq!(
693 serde_json::to_string(&HeartbeatStatus::NotStarted).unwrap(),
694 "\"NOT_STARTED\""
695 );
696 assert_eq!(
697 serde_json::to_string(&HeartbeatStatus::Resurrected).unwrap(),
698 "\"RESURRECTED\""
699 );
700 }
701
702 #[test]
703 fn test_heartbeat_status_deserialize() {
704 assert_eq!(
705 serde_json::from_str::<HeartbeatStatus>("\"ALIVE\"").unwrap(),
706 HeartbeatStatus::Alive
707 );
708 assert_eq!(
709 serde_json::from_str::<HeartbeatStatus>("\"DEAD\"").unwrap(),
710 HeartbeatStatus::Dead
711 );
712 assert_eq!(
713 serde_json::from_str::<HeartbeatStatus>("\"NOT_STARTED\"").unwrap(),
714 HeartbeatStatus::NotStarted
715 );
716 assert_eq!(
717 serde_json::from_str::<HeartbeatStatus>("\"RESURRECTED\"").unwrap(),
718 HeartbeatStatus::Resurrected
719 );
720 }
721}