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