1use crate::prelude::*;
2use compact_str::ToCompactString;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, postgres::PgRow};
5use std::{
6 collections::{BTreeMap, HashMap},
7 sync::LazyLock,
8};
9#[derive(Serialize, Deserialize, Clone)]
10pub struct NodeAllocation {
11 pub uuid: uuid::Uuid,
12 pub server: Option<Fetchable<super::server::Server>>,
13
14 pub ip: sqlx::types::ipnetwork::IpNetwork,
15 pub ip_alias: Option<compact_str::CompactString>,
16 pub port: i32,
17
18 pub created: chrono::NaiveDateTime,
19
20 extension_data: super::ModelExtensionData,
21}
22
23impl BaseModel for NodeAllocation {
24 const NAME: &'static str = "node_allocation";
25
26 fn get_extension_list() -> &'static super::ModelExtensionList {
27 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
28 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
29
30 &EXTENSIONS
31 }
32
33 fn get_extension_data(&self) -> &super::ModelExtensionData {
34 &self.extension_data
35 }
36
37 #[inline]
38 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
39 let prefix = prefix.unwrap_or_default();
40
41 BTreeMap::from([
42 (
43 "node_allocations.uuid",
44 compact_str::format_compact!("{prefix}uuid"),
45 ),
46 (
47 "node_allocations.ip",
48 compact_str::format_compact!("{prefix}ip"),
49 ),
50 (
51 "node_allocations.ip_alias",
52 compact_str::format_compact!("{prefix}ip_alias"),
53 ),
54 (
55 "node_allocations.port",
56 compact_str::format_compact!("{prefix}port"),
57 ),
58 (
59 "node_allocations.created",
60 compact_str::format_compact!("{prefix}created"),
61 ),
62 ])
63 }
64
65 #[inline]
66 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
67 let prefix = prefix.unwrap_or_default();
68
69 Ok(Self {
70 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
71 server: if let Ok(server_uuid) = row.try_get::<uuid::Uuid, _>("server_uuid") {
72 Some(super::server::Server::get_fetchable(server_uuid))
73 } else {
74 None
75 },
76 ip: row.try_get(compact_str::format_compact!("{prefix}ip").as_str())?,
77 ip_alias: row.try_get(compact_str::format_compact!("{prefix}ip_alias").as_str())?,
78 port: row.try_get(compact_str::format_compact!("{prefix}port").as_str())?,
79 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
80 extension_data: Self::map_extensions(prefix, row)?,
81 })
82 }
83}
84
85impl NodeAllocation {
86 pub async fn create(
87 database: &crate::database::Database,
88 node_uuid: uuid::Uuid,
89 ip: &sqlx::types::ipnetwork::IpNetwork,
90 ip_alias: Option<&str>,
91 port: i32,
92 ) -> Result<(), crate::database::DatabaseError> {
93 sqlx::query(
94 r#"
95 INSERT INTO node_allocations (node_uuid, ip, ip_alias, port)
96 VALUES ($1, $2, $3, $4)
97 "#,
98 )
99 .bind(node_uuid)
100 .bind(ip)
101 .bind(ip_alias)
102 .bind(port)
103 .execute(database.write())
104 .await?;
105
106 Ok(())
107 }
108
109 pub async fn get_random(
110 database: &crate::database::Database,
111 node_uuid: uuid::Uuid,
112 start_port: u16,
113 end_port: u16,
114 amount: i64,
115 exclude: &[uuid::Uuid],
116 ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
117 let rows = sqlx::query(
118 r#"
119 WITH eligible_ips AS (
120 SELECT node_allocations.ip
121 FROM node_allocations
122 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
123 WHERE
124 node_allocations.node_uuid = $1
125 AND node_allocations.port BETWEEN $2 AND $3
126 AND server_allocations.uuid IS NULL
127 AND NOT (node_allocations.uuid = ANY($5))
128 GROUP BY node_allocations.ip
129 HAVING COUNT(*) >= $4
130 ),
131 random_ip AS (
132 SELECT ip FROM eligible_ips ORDER BY RANDOM() LIMIT 1
133 )
134 SELECT node_allocations.uuid
135 FROM node_allocations
136 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
137 WHERE
138 node_allocations.node_uuid = $1
139 AND node_allocations.port BETWEEN $2 AND $3
140 AND server_allocations.uuid IS NULL
141 AND NOT (node_allocations.uuid = ANY($5))
142 AND node_allocations.ip = (SELECT ip FROM random_ip)
143 ORDER BY RANDOM()
144 LIMIT $4
145 "#,
146 )
147 .bind(node_uuid)
148 .bind(start_port as i32)
149 .bind(end_port as i32)
150 .bind(amount)
151 .bind(exclude)
152 .fetch_all(database.write())
153 .await?;
154
155 if rows.len() != amount as usize {
156 return Err(anyhow::anyhow!("only found {} available allocations", rows.len()).into());
157 }
158
159 Ok(rows
160 .into_iter()
161 .map(|row| row.get::<uuid::Uuid, _>("uuid"))
162 .collect())
163 }
164
165 pub async fn get_random_ip(
166 database: &crate::database::Database,
167 node_uuid: uuid::Uuid,
168 ip: &sqlx::types::ipnetwork::IpNetwork,
169 start_port: u16,
170 end_port: u16,
171 amount: i64,
172 exclude: &[uuid::Uuid],
173 ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
174 let rows = sqlx::query(
175 r#"
176 SELECT node_allocations.uuid
177 FROM node_allocations
178 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
179 WHERE
180 node_allocations.node_uuid = $1
181 AND node_allocations.ip = $2
182 AND node_allocations.port BETWEEN $3 AND $4
183 AND server_allocations.uuid IS NULL
184 AND NOT (node_allocations.uuid = ANY($6))
185 ORDER BY RANDOM()
186 LIMIT $5
187 "#,
188 )
189 .bind(node_uuid)
190 .bind(ip)
191 .bind(start_port as i32)
192 .bind(end_port as i32)
193 .bind(amount)
194 .bind(exclude)
195 .fetch_all(database.write())
196 .await?;
197
198 if rows.len() != amount as usize {
199 return Err(anyhow::anyhow!(
200 "only found {} available allocations on this IP",
201 rows.len()
202 )
203 .into());
204 }
205
206 Ok(rows
207 .into_iter()
208 .map(|row| row.get::<uuid::Uuid, _>("uuid"))
209 .collect())
210 }
211
212 pub async fn get_random_dedicated(
213 database: &crate::database::Database,
214 node_uuid: uuid::Uuid,
215 start_port: u16,
216 end_port: u16,
217 amount: i64,
218 ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
219 let rows = sqlx::query(
220 r#"
221 WITH eligible_ips AS (
222 SELECT node_allocations.ip
223 FROM node_allocations
224 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
225 WHERE node_allocations.node_uuid = $1
226 GROUP BY node_allocations.ip
227 HAVING
228 COUNT(server_allocations.uuid) = 0
229 AND SUM(CASE WHEN node_allocations.port BETWEEN $2 AND $3 THEN 1 ELSE 0 END) >= $4
230 ),
231 random_ip AS (
232 SELECT ip FROM eligible_ips ORDER BY RANDOM() LIMIT 1
233 )
234 SELECT node_allocations.uuid
235 FROM node_allocations
236 WHERE
237 node_allocations.node_uuid = $1
238 AND node_allocations.port BETWEEN $2 AND $3
239 AND node_allocations.ip = (SELECT ip FROM random_ip)
240 ORDER BY RANDOM()
241 LIMIT $4
242 "#,
243 )
244 .bind(node_uuid)
245 .bind(start_port as i32)
246 .bind(end_port as i32)
247 .bind(amount)
248 .fetch_all(database.write())
249 .await?;
250
251 if rows.len() != amount as usize {
252 return Err(anyhow::anyhow!(
253 "only found {} available dedicated allocations",
254 rows.len()
255 )
256 .into());
257 }
258
259 Ok(rows
260 .into_iter()
261 .map(|row| row.get::<uuid::Uuid, _>("uuid"))
262 .collect())
263 }
264
265 pub async fn get_preserved(
266 database: &crate::database::Database,
267 node_uuid: uuid::Uuid,
268 ports: &[i32],
269 ) -> Result<
270 Option<(sqlx::types::ipnetwork::IpNetwork, Vec<(uuid::Uuid, i32)>)>,
271 crate::database::DatabaseError,
272 > {
273 let rows = sqlx::query(
274 r#"
275 WITH best_ip AS (
276 SELECT node_allocations.ip
277 FROM node_allocations
278 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
279 WHERE
280 node_allocations.node_uuid = $1
281 AND node_allocations.port = ANY($2)
282 AND server_allocations.uuid IS NULL
283 GROUP BY node_allocations.ip
284 ORDER BY COUNT(DISTINCT node_allocations.port) DESC, RANDOM()
285 LIMIT 1
286 )
287 SELECT node_allocations.uuid, node_allocations.ip, node_allocations.port
288 FROM node_allocations
289 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
290 WHERE
291 node_allocations.node_uuid = $1
292 AND node_allocations.port = ANY($2)
293 AND server_allocations.uuid IS NULL
294 AND node_allocations.ip = (SELECT ip FROM best_ip)
295 "#,
296 )
297 .bind(node_uuid)
298 .bind(ports)
299 .fetch_all(database.write())
300 .await?;
301
302 let Some(first) = rows.first() else {
303 return Ok(None);
304 };
305
306 let ip = first.get::<sqlx::types::ipnetwork::IpNetwork, _>("ip");
307
308 Ok(Some((
309 ip,
310 rows.into_iter()
311 .map(|row| (row.get::<uuid::Uuid, _>("uuid"), row.get::<i32, _>("port")))
312 .collect(),
313 )))
314 }
315
316 pub async fn by_node_uuid_ip_port_unused(
317 database: &crate::database::Database,
318 node_uuid: uuid::Uuid,
319 ip: &sqlx::types::ipnetwork::IpNetwork,
320 port: i32,
321 ) -> Result<Option<Self>, crate::database::DatabaseError> {
322 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
323 r#"
324 SELECT {}
325 FROM node_allocations
326 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
327 WHERE node_allocations.node_uuid = $1 AND node_allocations.ip = $2 AND node_allocations.port = $3 AND server_allocations.uuid IS NULL
328 "#,
329 Self::columns_sql(None)
330 )))
331 .bind(node_uuid)
332 .bind(ip)
333 .bind(port)
334 .fetch_optional(database.read())
335 .await?;
336
337 row.try_map(|row| Self::map(None, &row))
338 }
339
340 pub async fn get_from_deployment<'a>(
341 database: &crate::database::Database,
342 deployment: &'a super::egg_configuration::EggConfigAllocationsDeployment,
343 node_uuid: uuid::Uuid,
344 variables: &mut HashMap<&'a str, compact_str::CompactString>,
345 ) -> Result<(Option<uuid::Uuid>, Vec<uuid::Uuid>), crate::database::DatabaseError> {
346 let mut primary = None;
347 let mut additional = Vec::new();
348
349 const MAX_ITER: usize = 100;
350
351 macro_rules! is_unused {
352 ($uuid:expr) => {
353 primary.as_ref().map_or(true, |p| p.uuid != $uuid) && !additional.contains(&$uuid)
354 };
355 }
356
357 macro_rules! get_random {
358 ($start_port:expr, $end_port:expr) => {{
359 let mut exclude = additional.clone();
360 if let Some(primary) = &primary {
361 exclude.push(primary.uuid);
362 Self::get_random_ip(
363 database,
364 node_uuid,
365 &primary.ip,
366 $start_port,
367 $end_port,
368 1,
369 &exclude,
370 )
371 .await?
372 } else {
373 Self::get_random(database, node_uuid, $start_port, $end_port, 1, &exclude)
374 .await?
375 }
376 }};
377 }
378
379 let mut success = false;
380
381 'primary: for i in 0..MAX_ITER {
382 if i != 0 && deployment.primary.is_none() {
383 break;
384 }
385
386 additional.clear();
387 variables.clear();
388
389 if let Some(primary_allocation) = &deployment.primary {
390 let random = if deployment.dedicated {
391 Self::get_random_dedicated(
392 database,
393 node_uuid,
394 primary_allocation.start_port,
395 primary_allocation.end_port,
396 1,
397 )
398 .await?
399 } else {
400 Self::get_random(
401 database,
402 node_uuid,
403 primary_allocation.start_port,
404 primary_allocation.end_port,
405 1,
406 &[],
407 )
408 .await?
409 };
410
411 let Some(allocation) = random.into_iter().next() else {
412 return Err(anyhow::anyhow!("no available primary allocation found").into());
413 };
414 let allocation =
415 match Self::by_node_uuid_uuid(database, node_uuid, allocation).await? {
416 Some(allocation) => allocation,
417 None => {
418 return Err(
419 anyhow::anyhow!("allocated primary allocation not found").into()
420 );
421 }
422 };
423
424 if let Some(variable_name) = &primary_allocation.assign_to_variable {
425 variables.insert(variable_name, allocation.port.to_compact_string());
426 }
427
428 primary = Some(allocation);
429 }
430
431 for additional_allocation in &deployment.additional {
432 match additional_allocation.mode {
433 super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::Random => {
434 let random = get_random!(1, u16::MAX);
435
436 let Some(allocation) = random.into_iter().next() else {
437 return Err(anyhow::anyhow!("no available additional allocation found").into());
438 };
439 additional.push(allocation);
440
441 if let Some(variable_name) = &additional_allocation.assign_to_variable {
442 let allocation = match Self::by_node_uuid_uuid(database, node_uuid, allocation).await? {
443 Some(allocation) => allocation,
444 None => {
445 return Err(
446 anyhow::anyhow!("allocated additional allocation not found").into()
447 );
448 }
449 };
450
451 variables.insert(variable_name, allocation.port.to_compact_string());
452 }
453 },
454 super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::Range {
455 start_port,
456 end_port,
457 } => {
458 let random = get_random!(start_port, end_port);
459
460 let Some(allocation) = random.into_iter().next() else {
461 return Err(anyhow::anyhow!("no available additional allocation found").into());
462 };
463 additional.push(allocation);
464
465 if let Some(variable_name) = &additional_allocation.assign_to_variable {
466 let allocation = match Self::by_node_uuid_uuid(database, node_uuid, allocation).await? {
467 Some(allocation) => allocation,
468 None => {
469 return Err(
470 anyhow::anyhow!("allocated additional allocation not found").into()
471 );
472 }
473 };
474
475 variables.insert(variable_name, allocation.port.to_compact_string());
476 }
477 }
478 super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::AddPrimary { value } => {
479 let primary = match &primary {
480 Some(primary) => primary,
481 None => {
482 return Err(anyhow::anyhow!("primary allocation is required for `add_primary` mode").into());
483 }
484 };
485
486 let allocation_port = primary.port + value as i32;
487
488 let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
489 Some(allocation) => allocation,
490 None => continue 'primary,
491 };
492 if !is_unused!(allocation.uuid) {
493 return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
494 }
495 additional.push(allocation.uuid);
496
497 if let Some(variable_name) = &additional_allocation.assign_to_variable {
498 variables.insert(variable_name, allocation.port.to_compact_string());
499 }
500 }
501 super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::SubtractPrimary { value } => {
502 let primary = match &primary {
503 Some(primary) => primary,
504 None => {
505 return Err(anyhow::anyhow!("primary allocation is required for `subtract_primary` mode").into());
506 }
507 };
508
509 let allocation_port = primary.port - value as i32;
510
511 let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
512 Some(allocation) => allocation,
513 None => continue 'primary,
514 };
515 if !is_unused!(allocation.uuid) {
516 return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
517 }
518 additional.push(allocation.uuid);
519
520 if let Some(variable_name) = &additional_allocation.assign_to_variable {
521 variables.insert(variable_name, allocation.port.to_compact_string());
522 }
523 }
524 super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::MultiplyPrimary { value } => {
525 let primary = match &primary {
526 Some(primary) => primary,
527 None => {
528 return Err(anyhow::anyhow!("primary allocation is required for `multiply_primary` mode").into());
529 }
530 };
531
532 let allocation_port = (primary.port as f64 * value) as i32;
533
534 let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
535 Some(allocation) => allocation,
536 None => continue 'primary,
537 };
538 if !is_unused!(allocation.uuid) {
539 return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
540 }
541 additional.push(allocation.uuid);
542
543 if let Some(variable_name) = &additional_allocation.assign_to_variable {
544 variables.insert(variable_name, allocation.port.to_compact_string());
545 }
546 }
547 super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::DividePrimary { value } => {
548 let primary = match &primary {
549 Some(primary) => primary,
550 None => {
551 return Err(anyhow::anyhow!("primary allocation is required for `divide_primary` mode").into());
552 }
553 };
554
555 let allocation_port = (primary.port as f64 / value) as i32;
556
557 let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
558 Some(allocation) => allocation,
559 None => continue 'primary,
560 };
561 if !is_unused!(allocation.uuid) {
562 return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
563 }
564 additional.push(allocation.uuid);
565
566 if let Some(variable_name) = &additional_allocation.assign_to_variable {
567 variables.insert(variable_name, allocation.port.to_compact_string());
568 }
569 }
570 }
571 }
572
573 success = true;
574 break;
575 }
576
577 if !success {
578 return Err(anyhow::anyhow!(
579 "could not satisfy all additional allocation rules after {MAX_ITER} attempts"
580 )
581 .into());
582 }
583
584 Ok((primary.map(|p| p.uuid), additional))
585 }
586
587 pub async fn by_node_uuid_uuid(
588 database: &crate::database::Database,
589 node_uuid: uuid::Uuid,
590 uuid: uuid::Uuid,
591 ) -> Result<Option<Self>, crate::database::DatabaseError> {
592 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
593 r#"
594 SELECT {}
595 FROM node_allocations
596 WHERE node_allocations.node_uuid = $1 AND node_allocations.uuid = $2
597 "#,
598 Self::columns_sql(None)
599 )))
600 .bind(node_uuid)
601 .bind(uuid)
602 .fetch_optional(database.read())
603 .await?;
604
605 row.try_map(|row| Self::map(None, &row))
606 }
607
608 pub async fn available_by_node_uuid_with_pagination(
609 database: &crate::database::Database,
610 node_uuid: uuid::Uuid,
611 page: i64,
612 per_page: i64,
613 search: Option<&str>,
614 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
615 let offset = (page - 1) * per_page;
616
617 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
618 r#"
619 SELECT {}, COUNT(*) OVER() AS total_count
620 FROM node_allocations
621 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
622 WHERE node_allocations.node_uuid = $1 AND server_allocations.uuid IS NULL
623 AND (
624 $2 IS NULL
625 OR host(node_allocations.ip) || ':' || node_allocations.port ILIKE '%' || $2 || '%'
626 OR (node_allocations.ip_alias IS NOT NULL AND node_allocations.ip_alias || ':' || node_allocations.port ILIKE '%' || $2 || '%')
627 )
628 ORDER BY node_allocations.ip, node_allocations.port
629 LIMIT $3 OFFSET $4
630 "#,
631 Self::columns_sql(None)
632 )))
633 .bind(node_uuid)
634 .bind(search)
635 .bind(per_page)
636 .bind(offset)
637 .fetch_all(database.read())
638 .await?;
639
640 Ok(super::Pagination {
641 total: rows
642 .first()
643 .map_or(Ok(0), |row| row.try_get("total_count"))?,
644 per_page,
645 page,
646 data: rows
647 .into_iter()
648 .map(|row| Self::map(None, &row))
649 .try_collect_vec()?,
650 })
651 }
652
653 pub async fn by_node_uuid_with_pagination(
654 database: &crate::database::Database,
655 node_uuid: uuid::Uuid,
656 page: i64,
657 per_page: i64,
658 search: Option<&str>,
659 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
660 let offset = (page - 1) * per_page;
661
662 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
663 r#"
664 SELECT {}, server_allocations.server_uuid, COUNT(*) OVER() AS total_count
665 FROM node_allocations
666 LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
667 WHERE node_allocations.node_uuid = $1
668 AND (
669 $2 IS NULL OR host(node_allocations.ip) || ':' || node_allocations.port ILIKE '%' || $2 || '%'
670 OR (node_allocations.ip_alias IS NOT NULL AND node_allocations.ip_alias || ':' || node_allocations.port ILIKE '%' || $2 || '%')
671 OR server_allocations.notes ILIKE '%' || $2 || '%'
672 )
673 ORDER BY node_allocations.ip, node_allocations.port
674 LIMIT $3 OFFSET $4
675 "#,
676 Self::columns_sql(None)
677 )))
678 .bind(node_uuid)
679 .bind(search)
680 .bind(per_page)
681 .bind(offset)
682 .fetch_all(database.read())
683 .await?;
684
685 Ok(super::Pagination {
686 total: rows
687 .first()
688 .map_or(Ok(0), |row| row.try_get("total_count"))?,
689 per_page,
690 page,
691 data: rows
692 .into_iter()
693 .map(|row| Self::map(None, &row))
694 .try_collect_vec()?,
695 })
696 }
697
698 pub async fn delete_by_uuids(
699 database: &crate::database::Database,
700 node_uuid: uuid::Uuid,
701 uuids: &[uuid::Uuid],
702 ) -> Result<u64, crate::database::DatabaseError> {
703 let deleted = sqlx::query(
704 r#"
705 DELETE FROM node_allocations
706 WHERE node_allocations.node_uuid = $1 AND node_allocations.uuid = ANY($2)
707 "#,
708 )
709 .bind(node_uuid)
710 .bind(uuids)
711 .execute(database.write())
712 .await?
713 .rows_affected();
714
715 Ok(deleted)
716 }
717}
718
719#[async_trait::async_trait]
720impl IntoAdminApiObject for NodeAllocation {
721 type AdminApiObject = AdminApiNodeAllocation;
722 type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
723
724 async fn into_admin_api_object<'a>(
725 self,
726 state: &crate::State,
727 storage_url_retriever: Self::ExtraArgs<'a>,
728 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
729 let api_object = AdminApiNodeAllocation::init_hooks(&self, state).await?;
730
731 let server = match self.server {
732 Some(fetchable) => Some(
733 fetchable
734 .fetch_cached(&state.database)
735 .await?
736 .into_admin_api_object(state, storage_url_retriever)
737 .await?,
738 ),
739 None => None,
740 };
741
742 let api_object = finish_extendible!(
743 AdminApiNodeAllocation {
744 uuid: self.uuid,
745 server,
746 ip: self.ip.ip().to_compact_string(),
747 ip_alias: self.ip_alias,
748 port: self.port,
749 created: self.created.and_utc(),
750 },
751 api_object,
752 state
753 )?;
754
755 Ok(api_object)
756 }
757}
758
759#[schema_extension_derive::extendible]
760#[init_args(NodeAllocation, crate::State)]
761#[hook_args(crate::State)]
762#[derive(ToSchema, Serialize)]
763#[schema(title = "NodeAllocation")]
764pub struct AdminApiNodeAllocation {
765 pub uuid: uuid::Uuid,
766 pub server: Option<super::server::AdminApiServer>,
767
768 pub ip: compact_str::CompactString,
769 pub ip_alias: Option<compact_str::CompactString>,
770 pub port: i32,
771
772 pub created: chrono::DateTime<chrono::Utc>,
773}