1use crate::{
2 models::{
3 CreatableModel, CreateListenerList, InsertQueryBuilder, UpdatableModel, UpdateListenerList,
4 UpdateQueryBuilder,
5 },
6 prelude::*,
7};
8use garde::Validate;
9use rand::distr::SampleString;
10use serde::{Deserialize, Serialize};
11use sqlx::{Row, postgres::PgRow};
12use std::{
13 collections::{BTreeMap, HashMap},
14 sync::{Arc, LazyLock},
15};
16use utoipa::ToSchema;
17
18mod events;
19pub use events::NodeEvent;
20
21pub type GetNode = crate::extract::ConsumingExtension<Node>;
22
23#[derive(Serialize, Deserialize, Clone)]
24pub struct Node {
25 pub uuid: uuid::Uuid,
26 pub location: super::location::Location,
27 pub backup_configuration: Option<Fetchable<super::backup_configuration::BackupConfiguration>>,
28
29 pub name: compact_str::CompactString,
30 pub description: Option<compact_str::CompactString>,
31
32 pub deployment_enabled: bool,
33 pub maintenance_enabled: bool,
34
35 pub public_url: Option<reqwest::Url>,
36 pub url: reqwest::Url,
37 pub sftp_host: Option<compact_str::CompactString>,
38 pub sftp_port: i32,
39
40 pub memory: i64,
41 pub disk: i64,
42
43 pub token_id: compact_str::CompactString,
44 pub token: Vec<u8>,
45
46 pub created: chrono::NaiveDateTime,
47}
48
49impl BaseModel for Node {
50 const NAME: &'static str = "node";
51
52 #[inline]
53 fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
54 let prefix = prefix.unwrap_or_default();
55
56 let mut columns = BTreeMap::from([
57 ("nodes.uuid", compact_str::format_compact!("{prefix}uuid")),
58 (
59 "nodes.backup_configuration_uuid",
60 compact_str::format_compact!("{prefix}node_backup_configuration_uuid"),
61 ),
62 ("nodes.name", compact_str::format_compact!("{prefix}name")),
63 (
64 "nodes.description",
65 compact_str::format_compact!("{prefix}description"),
66 ),
67 (
68 "nodes.deployment_enabled",
69 compact_str::format_compact!("{prefix}deployment_enabled"),
70 ),
71 (
72 "nodes.maintenance_enabled",
73 compact_str::format_compact!("{prefix}maintenance_enabled"),
74 ),
75 (
76 "nodes.public_url",
77 compact_str::format_compact!("{prefix}public_url"),
78 ),
79 ("nodes.url", compact_str::format_compact!("{prefix}url")),
80 (
81 "nodes.sftp_host",
82 compact_str::format_compact!("{prefix}sftp_host"),
83 ),
84 (
85 "nodes.sftp_port",
86 compact_str::format_compact!("{prefix}sftp_port"),
87 ),
88 (
89 "nodes.memory",
90 compact_str::format_compact!("{prefix}memory"),
91 ),
92 ("nodes.disk", compact_str::format_compact!("{prefix}disk")),
93 (
94 "nodes.token_id",
95 compact_str::format_compact!("{prefix}token_id"),
96 ),
97 ("nodes.token", compact_str::format_compact!("{prefix}token")),
98 (
99 "nodes.created",
100 compact_str::format_compact!("{prefix}created"),
101 ),
102 ]);
103
104 columns.extend(super::location::Location::columns(Some("location_")));
105
106 columns
107 }
108
109 #[inline]
110 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
111 let prefix = prefix.unwrap_or_default();
112
113 Ok(Self {
114 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
115 location: super::location::Location::map(Some("location_"), row)?,
116 backup_configuration:
117 super::backup_configuration::BackupConfiguration::get_fetchable_from_row(
118 row,
119 compact_str::format_compact!("{prefix}node_backup_configuration_uuid"),
120 ),
121 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
122 description: row
123 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
124 deployment_enabled: row
125 .try_get(compact_str::format_compact!("{prefix}deployment_enabled").as_str())?,
126 maintenance_enabled: row
127 .try_get(compact_str::format_compact!("{prefix}maintenance_enabled").as_str())?,
128 public_url: row
129 .try_get::<Option<String>, _>(
130 compact_str::format_compact!("{prefix}public_url").as_str(),
131 )?
132 .try_map(|url| url.parse())
133 .map_err(anyhow::Error::new)?,
134 url: row
135 .try_get::<String, _>(compact_str::format_compact!("{prefix}url").as_str())?
136 .parse()
137 .map_err(anyhow::Error::new)?,
138 sftp_host: row.try_get(compact_str::format_compact!("{prefix}sftp_host").as_str())?,
139 sftp_port: row.try_get(compact_str::format_compact!("{prefix}sftp_port").as_str())?,
140 memory: row.try_get(compact_str::format_compact!("{prefix}memory").as_str())?,
141 disk: row.try_get(compact_str::format_compact!("{prefix}disk").as_str())?,
142 token_id: row.try_get(compact_str::format_compact!("{prefix}token_id").as_str())?,
143 token: row.try_get(compact_str::format_compact!("{prefix}token").as_str())?,
144 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
145 })
146 }
147}
148
149impl Node {
150 pub async fn by_token_id_token_cached(
151 database: &crate::database::Database,
152 token_id: &str,
153 token: &str,
154 ) -> Result<Option<Self>, anyhow::Error> {
155 database
156 .cache
157 .cached(&format!("node::token::{token_id}.{token}"), 10, || async {
158 let row = sqlx::query(&format!(
159 r#"
160 SELECT {}
161 FROM nodes
162 JOIN locations ON locations.uuid = nodes.location_uuid
163 WHERE nodes.token_id = $1
164 "#,
165 Self::columns_sql(None)
166 ))
167 .bind(token_id)
168 .fetch_optional(database.read())
169 .await?;
170
171 Ok::<_, anyhow::Error>(
172 if let Some(node) = row.try_map(|row| Self::map(None, &row))? {
173 if constant_time_eq::constant_time_eq(
174 database.decrypt(node.token.clone()).await?.as_bytes(),
175 token.as_bytes(),
176 ) {
177 Some(node)
178 } else {
179 None
180 }
181 } else {
182 None
183 },
184 )
185 })
186 .await
187 }
188
189 pub async fn by_location_uuid_with_pagination(
190 database: &crate::database::Database,
191 location_uuid: uuid::Uuid,
192 page: i64,
193 per_page: i64,
194 search: Option<&str>,
195 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
196 let offset = (page - 1) * per_page;
197
198 let rows = sqlx::query(&format!(
199 r#"
200 SELECT {}, COUNT(*) OVER() AS total_count
201 FROM nodes
202 JOIN locations ON locations.uuid = nodes.location_uuid
203 WHERE nodes.location_uuid = $1 AND ($2 IS NULL OR nodes.name ILIKE '%' || $2 || '%')
204 ORDER BY nodes.created
205 LIMIT $3 OFFSET $4
206 "#,
207 Self::columns_sql(None)
208 ))
209 .bind(location_uuid)
210 .bind(search)
211 .bind(per_page)
212 .bind(offset)
213 .fetch_all(database.read())
214 .await?;
215
216 Ok(super::Pagination {
217 total: rows
218 .first()
219 .map_or(Ok(0), |row| row.try_get("total_count"))?,
220 per_page,
221 page,
222 data: rows
223 .into_iter()
224 .map(|row| Self::map(None, &row))
225 .try_collect_vec()?,
226 })
227 }
228
229 pub async fn by_backup_configuration_uuid_with_pagination(
230 database: &crate::database::Database,
231 backup_configuration_uuid: uuid::Uuid,
232 page: i64,
233 per_page: i64,
234 search: Option<&str>,
235 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
236 let offset = (page - 1) * per_page;
237
238 let rows = sqlx::query(&format!(
239 r#"
240 SELECT {}, COUNT(*) OVER() AS total_count
241 FROM nodes
242 JOIN locations ON locations.uuid = nodes.location_uuid
243 WHERE nodes.backup_configuration_uuid = $1 AND ($2 IS NULL OR nodes.name ILIKE '%' || $2 || '%')
244 ORDER BY nodes.created
245 LIMIT $3 OFFSET $4
246 "#,
247 Self::columns_sql(None)
248 ))
249 .bind(backup_configuration_uuid)
250 .bind(search)
251 .bind(per_page)
252 .bind(offset)
253 .fetch_all(database.read())
254 .await?;
255
256 Ok(super::Pagination {
257 total: rows
258 .first()
259 .map_or(Ok(0), |row| row.try_get("total_count"))?,
260 per_page,
261 page,
262 data: rows
263 .into_iter()
264 .map(|row| Self::map(None, &row))
265 .try_collect_vec()?,
266 })
267 }
268
269 pub async fn all_with_pagination(
270 database: &crate::database::Database,
271 page: i64,
272 per_page: i64,
273 search: Option<&str>,
274 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
275 let offset = (page - 1) * per_page;
276
277 let rows = sqlx::query(&format!(
278 r#"
279 SELECT {}, COUNT(*) OVER() AS total_count
280 FROM nodes
281 JOIN locations ON locations.uuid = nodes.location_uuid
282 WHERE $1 IS NULL OR nodes.name ILIKE '%' || $1 || '%'
283 ORDER BY nodes.created
284 LIMIT $2 OFFSET $3
285 "#,
286 Self::columns_sql(None)
287 ))
288 .bind(search)
289 .bind(per_page)
290 .bind(offset)
291 .fetch_all(database.read())
292 .await?;
293
294 Ok(super::Pagination {
295 total: rows
296 .first()
297 .map_or(Ok(0), |row| row.try_get("total_count"))?,
298 per_page,
299 page,
300 data: rows
301 .into_iter()
302 .map(|row| Self::map(None, &row))
303 .try_collect_vec()?,
304 })
305 }
306
307 pub async fn count_by_location_uuid(
308 database: &crate::database::Database,
309 location_uuid: uuid::Uuid,
310 ) -> i64 {
311 sqlx::query_scalar(
312 r#"
313 SELECT COUNT(*)
314 FROM nodes
315 WHERE nodes.location_uuid = $1
316 "#,
317 )
318 .bind(location_uuid)
319 .fetch_one(database.read())
320 .await
321 .unwrap_or(0)
322 }
323
324 pub async fn fetch_configuration(
328 &self,
329 database: &crate::database::Database,
330 ) -> Result<wings_api::Config, anyhow::Error> {
331 database
332 .cache
333 .cached(
334 &format!("node::{}::configuration", self.uuid),
335 120,
336 || async {
337 Ok::<_, anyhow::Error>(
338 self.api_client(database).await?.get_system_config().await?,
339 )
340 },
341 )
342 .await
343 }
344
345 pub async fn fetch_server_resources(
349 &self,
350 database: &crate::database::Database,
351 ) -> Result<HashMap<uuid::Uuid, wings_api::ResourceUsage>, anyhow::Error> {
352 database
353 .cache
354 .cached(
355 &format!("node::{}::server_resources", self.uuid),
356 15,
357 || async {
358 let resources = self
359 .api_client(database)
360 .await?
361 .get_servers_utilization()
362 .await?;
363
364 Ok::<_, anyhow::Error>(resources.into_iter().collect())
365 },
366 )
367 .await
368 }
369
370 pub async fn reset_token(
371 &self,
372 state: &crate::State,
373 ) -> Result<(String, String), anyhow::Error> {
374 let token_id = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
375 let token = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 64);
376
377 sqlx::query(
378 r#"
379 UPDATE nodes
380 SET token_id = $2, token = $3
381 WHERE nodes.uuid = $1
382 "#,
383 )
384 .bind(self.uuid)
385 .bind(&token_id)
386 .bind(state.database.encrypt(token.clone()).await?)
387 .execute(state.database.write())
388 .await?;
389
390 Self::get_event_emitter().emit(
391 state.clone(),
392 NodeEvent::TokenReset {
393 node: Box::new(self.clone()),
394 token_id: token_id.clone(),
395 token: token.clone(),
396 },
397 );
398
399 Ok((token_id, token))
400 }
401
402 #[inline]
403 pub fn public_url(&self) -> reqwest::Url {
404 self.public_url.clone().unwrap_or(self.url.clone())
405 }
406
407 #[inline]
408 pub async fn api_client(
409 &self,
410 database: &crate::database::Database,
411 ) -> Result<wings_api::client::WingsClient, anyhow::Error> {
412 Ok(wings_api::client::WingsClient::new(
413 self.url.to_string(),
414 database.decrypt(self.token.to_vec()).await?.into(),
415 ))
416 }
417
418 #[inline]
419 pub fn create_jwt<T: Serialize>(
420 &self,
421 database: &crate::database::Database,
422 jwt: &crate::jwt::Jwt,
423 payload: &T,
424 ) -> Result<String, jwt::Error> {
425 jwt.create_custom(
426 database.blocking_decrypt(&self.token).unwrap().as_bytes(),
427 payload,
428 )
429 }
430
431 #[inline]
432 pub async fn into_admin_api_object(
433 self,
434 database: &crate::database::Database,
435 ) -> Result<AdminApiNode, anyhow::Error> {
436 let (location, backup_configuration) =
437 tokio::join!(self.location.into_admin_api_object(database), async {
438 if let Some(backup_configuration) = self.backup_configuration {
439 if let Ok(backup_configuration) =
440 backup_configuration.fetch_cached(database).await
441 {
442 backup_configuration
443 .into_admin_api_object(database)
444 .await
445 .ok()
446 } else {
447 None
448 }
449 } else {
450 None
451 }
452 });
453
454 Ok(AdminApiNode {
455 uuid: self.uuid,
456 location,
457 backup_configuration,
458 name: self.name,
459 description: self.description,
460 deployment_enabled: self.deployment_enabled,
461 maintenance_enabled: self.maintenance_enabled,
462 public_url: self.public_url.map(|url| url.to_string()),
463 url: self.url.to_string(),
464 sftp_host: self.sftp_host,
465 sftp_port: self.sftp_port,
466 memory: self.memory,
467 disk: self.disk,
468 token_id: self.token_id,
469 token: database.decrypt(self.token).await?,
470 created: self.created.and_utc(),
471 })
472 }
473}
474
475#[async_trait::async_trait]
476impl ByUuid for Node {
477 async fn by_uuid(
478 database: &crate::database::Database,
479 uuid: uuid::Uuid,
480 ) -> Result<Self, crate::database::DatabaseError> {
481 let row = sqlx::query(&format!(
482 r#"
483 SELECT {}, {}
484 FROM nodes
485 JOIN locations ON locations.uuid = nodes.location_uuid
486 WHERE nodes.uuid = $1
487 "#,
488 Self::columns_sql(None),
489 super::location::Location::columns_sql(Some("location_")),
490 ))
491 .bind(uuid)
492 .fetch_one(database.read())
493 .await?;
494
495 Self::map(None, &row)
496 }
497}
498
499#[derive(ToSchema, Deserialize, Validate)]
500pub struct CreateNodeOptions {
501 #[garde(skip)]
502 pub location_uuid: uuid::Uuid,
503 #[garde(skip)]
504 pub backup_configuration_uuid: Option<uuid::Uuid>,
505 #[garde(length(chars, min = 3, max = 255))]
506 #[schema(min_length = 3, max_length = 255)]
507 pub name: compact_str::CompactString,
508 #[garde(length(chars, min = 1, max = 1024))]
509 #[schema(min_length = 1, max_length = 1024)]
510 pub description: Option<compact_str::CompactString>,
511 #[garde(skip)]
512 pub deployment_enabled: bool,
513 #[garde(skip)]
514 pub maintenance_enabled: bool,
515 #[garde(length(chars, min = 3, max = 255), url)]
516 #[schema(min_length = 3, max_length = 255, format = "uri")]
517 pub public_url: Option<compact_str::CompactString>,
518 #[garde(length(chars, min = 3, max = 255), url)]
519 #[schema(min_length = 3, max_length = 255, format = "uri")]
520 pub url: compact_str::CompactString,
521 #[garde(length(chars, min = 3, max = 255))]
522 #[schema(min_length = 3, max_length = 255)]
523 pub sftp_host: Option<compact_str::CompactString>,
524 #[garde(range(min = 1))]
525 #[schema(minimum = 1)]
526 pub sftp_port: u16,
527 #[garde(range(min = 1))]
528 #[schema(minimum = 1)]
529 pub memory: i64,
530 #[garde(range(min = 1))]
531 #[schema(minimum = 1)]
532 pub disk: i64,
533}
534
535#[async_trait::async_trait]
536impl CreatableModel for Node {
537 type CreateOptions<'a> = CreateNodeOptions;
538 type CreateResult = Self;
539
540 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
541 static CREATE_LISTENERS: LazyLock<CreateListenerList<Node>> =
542 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
543
544 &CREATE_LISTENERS
545 }
546
547 async fn create(
548 state: &crate::State,
549 mut options: Self::CreateOptions<'_>,
550 ) -> Result<Self, crate::database::DatabaseError> {
551 options.validate()?;
552
553 let mut transaction = state.database.write().begin().await?;
554
555 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
556 super::backup_configuration::BackupConfiguration::by_uuid_optional(
557 &state.database,
558 *backup_configuration_uuid,
559 )
560 .await?
561 .ok_or(crate::database::InvalidRelationError(
562 "backup_configuration",
563 ))?;
564 }
565
566 let mut query_builder = InsertQueryBuilder::new("nodes");
567
568 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
569 .await?;
570
571 let token_id = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
572 let token = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 64);
573
574 query_builder
575 .set("location_uuid", options.location_uuid)
576 .set(
577 "backup_configuration_uuid",
578 options.backup_configuration_uuid,
579 )
580 .set("name", &options.name)
581 .set("description", &options.description)
582 .set("deployment_enabled", options.deployment_enabled)
583 .set("maintenance_enabled", options.maintenance_enabled)
584 .set("public_url", &options.public_url)
585 .set("url", &options.url)
586 .set("sftp_host", &options.sftp_host)
587 .set("sftp_port", options.sftp_port as i32)
588 .set("memory", options.memory)
589 .set("disk", options.disk)
590 .set("token_id", token_id.clone())
591 .set("token", state.database.encrypt(token.clone()).await?);
592
593 let row = query_builder
594 .returning("uuid")
595 .fetch_one(&mut *transaction)
596 .await?;
597 let uuid: uuid::Uuid = row.get("uuid");
598
599 transaction.commit().await?;
600
601 Self::by_uuid(&state.database, uuid).await
602 }
603}
604
605#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
606pub struct UpdateNodeOptions {
607 #[garde(skip)]
608 pub location_uuid: Option<uuid::Uuid>,
609 #[serde(
610 default,
611 skip_serializing_if = "Option::is_none",
612 with = "::serde_with::rust::double_option"
613 )]
614 #[garde(skip)]
615 pub backup_configuration_uuid: Option<Option<uuid::Uuid>>,
616 #[garde(length(chars, min = 3, max = 255))]
617 #[schema(min_length = 3, max_length = 255)]
618 pub name: Option<compact_str::CompactString>,
619 #[garde(length(chars, min = 1, max = 1024))]
620 #[schema(min_length = 1, max_length = 1024)]
621 #[serde(
622 default,
623 skip_serializing_if = "Option::is_none",
624 with = "::serde_with::rust::double_option"
625 )]
626 pub description: Option<Option<compact_str::CompactString>>,
627 #[garde(skip)]
628 pub deployment_enabled: Option<bool>,
629 #[garde(skip)]
630 pub maintenance_enabled: Option<bool>,
631 #[garde(length(chars, min = 3, max = 255), url)]
632 #[schema(min_length = 3, max_length = 255, format = "uri")]
633 #[serde(
634 default,
635 skip_serializing_if = "Option::is_none",
636 with = "::serde_with::rust::double_option"
637 )]
638 pub public_url: Option<Option<compact_str::CompactString>>,
639 #[garde(length(chars, min = 3, max = 255), url)]
640 #[schema(min_length = 3, max_length = 255, format = "uri")]
641 pub url: Option<compact_str::CompactString>,
642 #[garde(length(chars, min = 3, max = 255))]
643 #[schema(min_length = 3, max_length = 255)]
644 #[serde(
645 default,
646 skip_serializing_if = "Option::is_none",
647 with = "::serde_with::rust::double_option"
648 )]
649 pub sftp_host: Option<Option<compact_str::CompactString>>,
650 #[garde(range(min = 1))]
651 #[schema(minimum = 1)]
652 pub sftp_port: Option<u16>,
653 #[garde(range(min = 1))]
654 #[schema(minimum = 1)]
655 pub memory: Option<i64>,
656 #[garde(range(min = 1))]
657 #[schema(minimum = 1)]
658 pub disk: Option<i64>,
659}
660
661#[async_trait::async_trait]
662impl UpdatableModel for Node {
663 type UpdateOptions = UpdateNodeOptions;
664
665 fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
666 static UPDATE_LISTENERS: LazyLock<UpdateListenerList<Node>> =
667 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
668
669 &UPDATE_LISTENERS
670 }
671
672 async fn update(
673 &mut self,
674 state: &crate::State,
675 mut options: Self::UpdateOptions,
676 ) -> Result<(), crate::database::DatabaseError> {
677 options.validate()?;
678
679 let location = if let Some(location_uuid) = options.location_uuid {
680 Some(
681 super::location::Location::by_uuid_optional(&state.database, location_uuid)
682 .await?
683 .ok_or(crate::database::InvalidRelationError("location"))?,
684 )
685 } else {
686 None
687 };
688
689 let backup_configuration =
690 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
691 match backup_configuration_uuid {
692 Some(uuid) => {
693 super::backup_configuration::BackupConfiguration::by_uuid_optional(
694 &state.database,
695 *uuid,
696 )
697 .await?
698 .ok_or(crate::database::InvalidRelationError(
699 "backup_configuration",
700 ))?;
701
702 Some(Some(
703 super::backup_configuration::BackupConfiguration::get_fetchable(*uuid),
704 ))
705 }
706 None => Some(None),
707 }
708 } else {
709 None
710 };
711
712 let mut transaction = state.database.write().begin().await?;
713
714 let mut query_builder = UpdateQueryBuilder::new("nodes");
715
716 Self::run_update_handlers(
717 self,
718 &mut options,
719 &mut query_builder,
720 state,
721 &mut transaction,
722 )
723 .await?;
724
725 query_builder
726 .set("location_uuid", options.location_uuid.as_ref())
727 .set(
728 "backup_configuration_uuid",
729 options
730 .backup_configuration_uuid
731 .as_ref()
732 .map(|u| u.as_ref()),
733 )
734 .set("name", options.name.as_ref())
735 .set(
736 "description",
737 options.description.as_ref().map(|d| d.as_ref()),
738 )
739 .set("deployment_enabled", options.deployment_enabled)
740 .set("maintenance_enabled", options.maintenance_enabled)
741 .set(
742 "public_url",
743 options.public_url.as_ref().map(|u| u.as_ref()),
744 )
745 .set("url", options.url.as_ref())
746 .set("sftp_host", options.sftp_host.as_ref().map(|h| h.as_ref()))
747 .set("sftp_port", options.sftp_port.as_ref().map(|p| *p as i32))
748 .set("memory", options.memory.as_ref())
749 .set("disk", options.disk.as_ref())
750 .where_eq("uuid", self.uuid);
751
752 query_builder.execute(&mut *transaction).await?;
753
754 if let Some(location) = location {
755 self.location = location;
756 }
757 if let Some(backup_configuration) = backup_configuration {
758 self.backup_configuration = backup_configuration;
759 }
760 if let Some(name) = options.name {
761 self.name = name;
762 }
763 if let Some(description) = options.description {
764 self.description = description;
765 }
766 if let Some(deployment_enabled) = options.deployment_enabled {
767 self.deployment_enabled = deployment_enabled;
768 }
769 if let Some(maintenance_enabled) = options.maintenance_enabled {
770 self.maintenance_enabled = maintenance_enabled;
771 }
772 if let Some(public_url) = options.public_url {
773 self.public_url = public_url
774 .try_map(|url| url.parse())
775 .map_err(anyhow::Error::new)?;
776 }
777 if let Some(url) = options.url {
778 self.url = url.parse().map_err(anyhow::Error::new)?;
779 }
780 if let Some(sftp_host) = options.sftp_host {
781 self.sftp_host = sftp_host;
782 }
783 if let Some(sftp_port) = options.sftp_port {
784 self.sftp_port = sftp_port as i32;
785 }
786 if let Some(memory) = options.memory {
787 self.memory = memory;
788 }
789 if let Some(disk) = options.disk {
790 self.disk = disk;
791 }
792
793 transaction.commit().await?;
794
795 Ok(())
796 }
797}
798
799#[async_trait::async_trait]
800impl DeletableModel for Node {
801 type DeleteOptions = ();
802
803 fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
804 static DELETE_LISTENERS: LazyLock<DeleteListenerList<Node>> =
805 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
806
807 &DELETE_LISTENERS
808 }
809
810 async fn delete(
811 &self,
812 state: &crate::State,
813 options: Self::DeleteOptions,
814 ) -> Result<(), anyhow::Error> {
815 let mut transaction = state.database.write().begin().await?;
816
817 self.run_delete_handlers(&options, state, &mut transaction)
818 .await?;
819
820 sqlx::query(
821 r#"
822 DELETE FROM nodes
823 WHERE nodes.uuid = $1
824 "#,
825 )
826 .bind(self.uuid)
827 .execute(&mut *transaction)
828 .await?;
829
830 transaction.commit().await?;
831
832 Ok(())
833 }
834}
835
836#[derive(ToSchema, Serialize)]
837#[schema(title = "Node")]
838pub struct AdminApiNode {
839 pub uuid: uuid::Uuid,
840 pub location: super::location::AdminApiLocation,
841 pub backup_configuration: Option<super::backup_configuration::AdminApiBackupConfiguration>,
842
843 pub name: compact_str::CompactString,
844 pub description: Option<compact_str::CompactString>,
845
846 pub deployment_enabled: bool,
847 pub maintenance_enabled: bool,
848
849 #[schema(format = "uri")]
850 pub public_url: Option<String>,
851 #[schema(format = "uri")]
852 pub url: String,
853 pub sftp_host: Option<compact_str::CompactString>,
854 pub sftp_port: i32,
855
856 pub memory: i64,
857 pub disk: i64,
858
859 pub token_id: compact_str::CompactString,
860 pub token: compact_str::CompactString,
861
862 pub created: chrono::DateTime<chrono::Utc>,
863}