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