1use crate::{
2 State,
3 models::{InsertQueryBuilder, UpdateQueryBuilder},
4 prelude::*,
5 response::DisplayError,
6};
7use compact_str::ToCompactString;
8use garde::Validate;
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11use sqlx::{Row, postgres::PgRow, prelude::Type};
12use std::{
13 collections::{BTreeMap, HashMap, HashSet},
14 sync::{Arc, LazyLock},
15};
16use utoipa::ToSchema;
17
18mod events;
19pub use events::ServerEvent;
20
21pub mod firewall;
22
23pub type GetServer = crate::extract::ConsumingExtension<Server>;
24pub type GetServerActivityLogger = crate::extract::ConsumingExtension<ServerActivityLogger>;
25
26fn ignore_match_path(path: &std::path::Path) -> std::path::PathBuf {
30 let path = crate::cap::CapFilesystem::resolve_path(path);
31
32 match path.strip_prefix("/") {
33 Ok(relative) => relative.to_path_buf(),
34 Err(_) => path,
35 }
36}
37
38fn is_path_ignored(
39 list: &crate::ignore_list::IgnoreList,
40 path: impl AsRef<std::path::Path>,
41 is_dir: bool,
42) -> bool {
43 list.is_ignored(&ignore_match_path(path.as_ref()), is_dir)
44}
45
46fn is_path_ignored_subtree(
47 list: &crate::ignore_list::IgnoreList,
48 path: impl AsRef<std::path::Path>,
49 is_dir: bool,
50) -> bool {
51 list.is_ignored_subtree(&ignore_match_path(path.as_ref()), is_dir)
52}
53
54#[derive(Clone)]
55pub struct ServerActivityLogger {
56 pub state: State,
57 pub server_uuid: uuid::Uuid,
58 pub user_uuid: uuid::Uuid,
59 pub impersonator_uuid: Option<uuid::Uuid>,
60 pub user_admin: bool,
61 pub user_owner: bool,
62 pub user_subuser: bool,
63 pub api_key_uuid: Option<uuid::Uuid>,
64 pub ip: std::net::IpAddr,
65}
66
67impl ServerActivityLogger {
68 pub async fn log(&self, event: impl Into<compact_str::CompactString>, data: serde_json::Value) {
69 let settings = match self.state.settings.get().await {
70 Ok(settings) => settings,
71 Err(_) => return,
72 };
73
74 if !settings.activity.server_log_admin_activity
75 && self.user_admin
76 && !self.user_owner
77 && !self.user_subuser
78 {
79 return;
80 }
81 drop(settings);
82
83 let options = super::server_activity::CreateServerActivityOptions {
84 server_uuid: self.server_uuid,
85 user_uuid: Some(self.user_uuid),
86 impersonator_uuid: self.impersonator_uuid,
87 api_key_uuid: self.api_key_uuid,
88 schedule_uuid: None,
89 event: event.into(),
90 ip: Some(self.ip.into()),
91 data,
92 created: None,
93 };
94 if let Err(err) = super::server_activity::ServerActivity::create(&self.state, options).await
95 {
96 tracing::warn!(
97 user = %self.user_uuid,
98 "failed to log server activity: {:#?}",
99 err
100 );
101 }
102 }
103}
104
105#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
106#[serde(rename_all = "snake_case")]
107#[sqlx(type_name = "server_status", rename_all = "SCREAMING_SNAKE_CASE")]
108pub enum ServerStatus {
109 Installing,
110 InstallFailed,
111 RestoringBackup,
112 BackupRestoreFailed,
113}
114
115#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
116#[serde(rename_all = "snake_case")]
117#[sqlx(
118 type_name = "server_auto_start_behavior",
119 rename_all = "SCREAMING_SNAKE_CASE"
120)]
121pub enum ServerAutoStartBehavior {
122 Always,
123 UnlessStopped,
124 Never,
125}
126
127impl From<ServerAutoStartBehavior> for wings_api::ServerAutoStartBehavior {
128 fn from(value: ServerAutoStartBehavior) -> Self {
129 match value {
130 ServerAutoStartBehavior::Always => Self::Always,
131 ServerAutoStartBehavior::UnlessStopped => Self::UnlessStopped,
132 ServerAutoStartBehavior::Never => Self::Never,
133 }
134 }
135}
136
137pub const MAX_TRANSFER_MULTIPLEX_CHANNELS: u64 = 16;
138
139#[derive(Clone, Default)]
140pub struct ServerInstallOptions {
141 pub truncate_directory: bool,
142 pub installation_script: Option<wings_api::InstallationScript>,
143 pub start_on_completion: bool,
144}
145
146pub struct ServerTransferOptions {
147 pub destination_node: super::node::Node,
148
149 pub allocation_uuid: Option<uuid::Uuid>,
150 pub allocation_uuids: Vec<uuid::Uuid>,
151
152 pub backups: Vec<uuid::Uuid>,
153 pub delete_source_backups: bool,
154 pub archive_format: wings_api::TransferArchiveFormat,
155 pub compression_level: Option<wings_api::CompressionLevel>,
156 pub multiplex_channels: u64,
157}
158
159#[derive(Serialize, Deserialize, Clone)]
160pub struct Server {
161 pub uuid: uuid::Uuid,
162 pub uuid_short: i32,
163 pub external_id: Option<compact_str::CompactString>,
164 pub allocation: Option<super::server_allocation::ServerAllocation>,
165 pub destination_allocation_uuid: Option<uuid::Uuid>,
166 pub node: Fetchable<super::node::Node>,
167 pub destination_node: Option<Fetchable<super::node::Node>>,
168 pub owner: super::user::User,
169 pub egg: Box<super::nest_egg::NestEgg>,
170 pub nest: Box<super::nest::Nest>,
171 pub backup_configuration: Option<Fetchable<super::backup_configuration::BackupConfiguration>>,
172
173 pub status: Option<ServerStatus>,
174 pub suspended: bool,
175
176 pub name: compact_str::CompactString,
177 pub description: Option<compact_str::CompactString>,
178
179 pub memory: i64,
180 pub memory_overhead: i64,
181 pub swap: i64,
182 pub disk: i64,
183 pub bandwidth_upload: i64,
184 pub bandwidth_download: i64,
185 pub io_weight: Option<i16>,
186 pub cpu: i32,
187 pub pinned_cpus: Vec<i16>,
188
189 pub startup: compact_str::CompactString,
190 pub image: compact_str::CompactString,
191 pub labels: IndexMap<compact_str::CompactString, compact_str::CompactString>,
192 pub auto_kill: wings_api::ServerConfigurationAutoKill,
193 pub auto_start_behavior: ServerAutoStartBehavior,
194 pub timezone: Option<compact_str::CompactString>,
195 pub trash: Option<TrashSettings>,
196
197 pub hugepages_passthrough_enabled: bool,
198 pub kvm_passthrough_enabled: bool,
199
200 pub allocation_limit: i32,
201 pub database_limit: i32,
202 pub backup_limit: i32,
203 pub schedule_limit: i32,
204
205 pub subuser_permissions: Option<Arc<Vec<compact_str::CompactString>>>,
206 pub subuser_ignored_files: Option<Vec<compact_str::CompactString>>,
207 #[serde(skip_serializing, skip_deserializing)]
208 subuser_ignored_files_list: Option<Box<crate::ignore_list::IgnoreList>>,
209
210 pub created: chrono::NaiveDateTime,
211
212 extension_data: super::ModelExtensionData,
213}
214
215impl BaseModel for Server {
216 const NAME: &'static str = "server";
217
218 fn get_extension_list() -> &'static super::ModelExtensionList {
219 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
220 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
221
222 &EXTENSIONS
223 }
224
225 fn get_extension_data(&self) -> &super::ModelExtensionData {
226 &self.extension_data
227 }
228
229 #[inline]
230 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
231 let prefix = prefix.unwrap_or_default();
232
233 let mut columns = BTreeMap::from([
234 ("servers.uuid", compact_str::format_compact!("{prefix}uuid")),
235 (
236 "servers.uuid_short",
237 compact_str::format_compact!("{prefix}uuid_short"),
238 ),
239 (
240 "servers.external_id",
241 compact_str::format_compact!("{prefix}external_id"),
242 ),
243 (
244 "servers.destination_allocation_uuid",
245 compact_str::format_compact!("{prefix}destination_allocation_uuid"),
246 ),
247 (
248 "servers.node_uuid",
249 compact_str::format_compact!("{prefix}node_uuid"),
250 ),
251 (
252 "servers.destination_node_uuid",
253 compact_str::format_compact!("{prefix}destination_node_uuid"),
254 ),
255 (
256 "servers.backup_configuration_uuid",
257 compact_str::format_compact!("{prefix}backup_configuration_uuid"),
258 ),
259 (
260 "servers.status",
261 compact_str::format_compact!("{prefix}status"),
262 ),
263 (
264 "servers.suspended",
265 compact_str::format_compact!("{prefix}suspended"),
266 ),
267 ("servers.name", compact_str::format_compact!("{prefix}name")),
268 (
269 "servers.description",
270 compact_str::format_compact!("{prefix}description"),
271 ),
272 (
273 "servers.memory",
274 compact_str::format_compact!("{prefix}memory"),
275 ),
276 (
277 "servers.memory_overhead",
278 compact_str::format_compact!("{prefix}memory_overhead"),
279 ),
280 ("servers.swap", compact_str::format_compact!("{prefix}swap")),
281 ("servers.disk", compact_str::format_compact!("{prefix}disk")),
282 (
283 "servers.bandwidth_upload",
284 compact_str::format_compact!("{prefix}bandwidth_upload"),
285 ),
286 (
287 "servers.bandwidth_download",
288 compact_str::format_compact!("{prefix}bandwidth_download"),
289 ),
290 (
291 "servers.io_weight",
292 compact_str::format_compact!("{prefix}io_weight"),
293 ),
294 ("servers.cpu", compact_str::format_compact!("{prefix}cpu")),
295 (
296 "servers.pinned_cpus",
297 compact_str::format_compact!("{prefix}pinned_cpus"),
298 ),
299 (
300 "servers.startup",
301 compact_str::format_compact!("{prefix}startup"),
302 ),
303 (
304 "servers.image",
305 compact_str::format_compact!("{prefix}image"),
306 ),
307 (
308 "servers.labels",
309 compact_str::format_compact!("{prefix}labels"),
310 ),
311 (
312 "servers.auto_kill",
313 compact_str::format_compact!("{prefix}auto_kill"),
314 ),
315 (
316 "servers.auto_start_behavior",
317 compact_str::format_compact!("{prefix}auto_start_behavior"),
318 ),
319 (
320 "servers.timezone",
321 compact_str::format_compact!("{prefix}timezone"),
322 ),
323 (
324 "servers.trash",
325 compact_str::format_compact!("{prefix}trash"),
326 ),
327 (
328 "servers.hugepages_passthrough_enabled",
329 compact_str::format_compact!("{prefix}hugepages_passthrough_enabled"),
330 ),
331 (
332 "servers.kvm_passthrough_enabled",
333 compact_str::format_compact!("{prefix}kvm_passthrough_enabled"),
334 ),
335 (
336 "servers.allocation_limit",
337 compact_str::format_compact!("{prefix}allocation_limit"),
338 ),
339 (
340 "servers.database_limit",
341 compact_str::format_compact!("{prefix}database_limit"),
342 ),
343 (
344 "servers.backup_limit",
345 compact_str::format_compact!("{prefix}backup_limit"),
346 ),
347 (
348 "servers.schedule_limit",
349 compact_str::format_compact!("{prefix}schedule_limit"),
350 ),
351 (
352 "servers.created",
353 compact_str::format_compact!("{prefix}created"),
354 ),
355 ]);
356
357 columns.extend(super::server_allocation::ServerAllocation::base_columns(
358 Some("allocation_"),
359 ));
360 columns.extend(super::user::User::base_columns(Some("owner_")));
361 columns.extend(super::nest_egg::NestEgg::base_columns(Some("egg_")));
362 columns.extend(super::nest::Nest::base_columns(Some("nest_")));
363
364 columns
365 }
366
367 #[inline]
368 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
369 let prefix = prefix.unwrap_or_default();
370
371 Ok(Self {
372 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
373 uuid_short: row.try_get(compact_str::format_compact!("{prefix}uuid_short").as_str())?,
374 external_id: row
375 .try_get(compact_str::format_compact!("{prefix}external_id").as_str())?,
376 allocation: if row
377 .try_get::<uuid::Uuid, _>(
378 compact_str::format_compact!("{prefix}allocation_uuid").as_str(),
379 )
380 .is_ok()
381 {
382 Some(super::server_allocation::ServerAllocation::map(
383 Some("allocation_"),
384 row,
385 )?)
386 } else {
387 None
388 },
389 destination_allocation_uuid: row
390 .try_get::<uuid::Uuid, _>(
391 compact_str::format_compact!("{prefix}destination_allocation_uuid").as_str(),
392 )
393 .ok(),
394 node: super::node::Node::get_fetchable(
395 row.try_get(compact_str::format_compact!("{prefix}node_uuid").as_str())?,
396 ),
397 destination_node: super::node::Node::get_fetchable_from_row(
398 row,
399 compact_str::format_compact!("{prefix}destination_node_uuid"),
400 ),
401 owner: super::user::User::map(Some("owner_"), row)?,
402 egg: Box::new(super::nest_egg::NestEgg::map(Some("egg_"), row)?),
403 nest: Box::new(super::nest::Nest::map(Some("nest_"), row)?),
404 backup_configuration:
405 super::backup_configuration::BackupConfiguration::get_fetchable_from_row(
406 row,
407 compact_str::format_compact!("{prefix}backup_configuration_uuid"),
408 ),
409 status: row.try_get(compact_str::format_compact!("{prefix}status").as_str())?,
410 suspended: row.try_get(compact_str::format_compact!("{prefix}suspended").as_str())?,
411 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
412 description: row
413 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
414 memory: row.try_get(compact_str::format_compact!("{prefix}memory").as_str())?,
415 memory_overhead: row
416 .try_get(compact_str::format_compact!("{prefix}memory_overhead").as_str())?,
417 swap: row.try_get(compact_str::format_compact!("{prefix}swap").as_str())?,
418 disk: row.try_get(compact_str::format_compact!("{prefix}disk").as_str())?,
419 bandwidth_upload: row
420 .try_get(compact_str::format_compact!("{prefix}bandwidth_upload").as_str())?,
421 bandwidth_download: row
422 .try_get(compact_str::format_compact!("{prefix}bandwidth_download").as_str())?,
423 io_weight: row.try_get(compact_str::format_compact!("{prefix}io_weight").as_str())?,
424 cpu: row.try_get(compact_str::format_compact!("{prefix}cpu").as_str())?,
425 pinned_cpus: row
426 .try_get(compact_str::format_compact!("{prefix}pinned_cpus").as_str())?,
427 startup: row.try_get(compact_str::format_compact!("{prefix}startup").as_str())?,
428 image: row.try_get(compact_str::format_compact!("{prefix}image").as_str())?,
429 labels: serde_json::from_value(row.try_get::<serde_json::Value, _>(
430 compact_str::format_compact!("{prefix}labels").as_str(),
431 )?)?,
432 auto_kill: serde_json::from_value(row.try_get::<serde_json::Value, _>(
433 compact_str::format_compact!("{prefix}auto_kill").as_str(),
434 )?)?,
435 auto_start_behavior: row
436 .try_get(compact_str::format_compact!("{prefix}auto_start_behavior").as_str())?,
437 timezone: row.try_get(compact_str::format_compact!("{prefix}timezone").as_str())?,
438 trash: row
439 .try_get::<Option<serde_json::Value>, _>(
440 compact_str::format_compact!("{prefix}trash").as_str(),
441 )?
442 .map(serde_json::from_value)
443 .transpose()?,
444 hugepages_passthrough_enabled: row.try_get(
445 compact_str::format_compact!("{prefix}hugepages_passthrough_enabled").as_str(),
446 )?,
447 kvm_passthrough_enabled: row.try_get(
448 compact_str::format_compact!("{prefix}kvm_passthrough_enabled").as_str(),
449 )?,
450 allocation_limit: row
451 .try_get(compact_str::format_compact!("{prefix}allocation_limit").as_str())?,
452 database_limit: row
453 .try_get(compact_str::format_compact!("{prefix}database_limit").as_str())?,
454 backup_limit: row
455 .try_get(compact_str::format_compact!("{prefix}backup_limit").as_str())?,
456 schedule_limit: row
457 .try_get(compact_str::format_compact!("{prefix}schedule_limit").as_str())?,
458 subuser_permissions: row
459 .try_get::<Vec<compact_str::CompactString>, _>("permissions")
460 .map(Arc::new)
461 .ok(),
462 subuser_ignored_files: row
463 .try_get::<Vec<compact_str::CompactString>, _>("ignored_files")
464 .ok(),
465 subuser_ignored_files_list: None,
466 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
467 extension_data: Self::map_extensions(prefix, row)?,
468 })
469 }
470
471 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
472 vec![compact_str::format_compact!(
473 "{}::{}",
474 Self::NAME,
475 self.uuid
476 )]
477 }
478}
479
480#[async_trait::async_trait]
481impl ResolvableModel for Server {
482 type Fingerprint = i32;
483
484 fn uuid(&self) -> uuid::Uuid {
485 self.uuid
486 }
487
488 fn fingerprint(&self) -> Self::Fingerprint {
489 self.uuid_short
490 }
491
492 async fn resolve(
493 database: &crate::database::Database,
494 identifier: &str,
495 ) -> Result<Option<Self>, anyhow::Error> {
496 let Ok(uuid_short) = u32::from_str_radix(identifier, 16) else {
497 return Ok(None);
498 };
499
500 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
501 r#"
502 SELECT {}
503 FROM servers
504 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
505 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
506 JOIN users ON users.uuid = servers.owner_uuid
507 LEFT JOIN roles ON roles.uuid = users.role_uuid
508 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
509 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
510 WHERE servers.uuid_short = $1
511 "#,
512 Self::columns_sql(None)
513 )))
514 .bind(uuid_short as i32)
515 .fetch_optional(database.read())
516 .await?;
517
518 Ok(row.try_map(|row| Self::map(None, &row))?)
519 }
520}
521
522impl Server {
523 pub fn unavailable_reason(&self) -> Option<&'static str> {
526 if self.suspended {
527 Some("server is suspended")
528 } else if self.destination_node.is_some() {
529 Some("server is being transferred")
530 } else {
531 match self.status? {
532 ServerStatus::Installing => Some("server is currently installing"),
533 ServerStatus::InstallFailed => Some("server has failed its installation process"),
534 ServerStatus::RestoringBackup => Some("server is restoring from a backup"),
535 ServerStatus::BackupRestoreFailed => Some("server has failed to restore a backup"),
536 }
537 }
538 }
539
540 pub async fn by_node_uuid_uuid(
541 database: &crate::database::Database,
542 node_uuid: uuid::Uuid,
543 uuid: uuid::Uuid,
544 ) -> Result<Option<Self>, crate::database::DatabaseError> {
545 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
546 r#"
547 SELECT {}
548 FROM servers
549 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
550 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
551 JOIN users ON users.uuid = servers.owner_uuid
552 LEFT JOIN roles ON roles.uuid = users.role_uuid
553 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
554 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
555 WHERE (servers.node_uuid = $1 OR servers.destination_node_uuid = $1) AND servers.uuid = $2
556 "#,
557 Self::columns_sql(None)
558 )))
559 .bind(node_uuid)
560 .bind(uuid)
561 .fetch_optional(database.read())
562 .await?;
563
564 row.try_map(|row| Self::map(None, &row))
565 }
566
567 pub async fn by_external_id(
568 database: &crate::database::Database,
569 external_id: &str,
570 ) -> Result<Option<Self>, crate::database::DatabaseError> {
571 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
572 r#"
573 SELECT {}
574 FROM servers
575 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
576 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
577 JOIN users ON users.uuid = servers.owner_uuid
578 LEFT JOIN roles ON roles.uuid = users.role_uuid
579 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
580 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
581 WHERE servers.external_id = $1
582 "#,
583 Self::columns_sql(None)
584 )))
585 .bind(external_id)
586 .fetch_optional(database.read())
587 .await?;
588
589 row.try_map(|row| Self::map(None, &row))
590 }
591
592 pub async fn by_identifier(
593 database: &crate::database::Database,
594 identifier: &str,
595 ) -> Result<Option<Self>, crate::database::DatabaseError> {
596 let query = format!(
597 r#"
598 SELECT {}
599 FROM servers
600 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
601 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
602 JOIN users ON users.uuid = servers.owner_uuid
603 LEFT JOIN roles ON roles.uuid = users.role_uuid
604 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
605 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
606 WHERE servers.{} = $1
607 "#,
608 Self::columns_sql(None),
609 match identifier.len() {
610 8 => "uuid_short",
611 36 => "uuid",
612 _ => return Ok(None),
613 }
614 );
615
616 let mut row = sqlx::query(sqlx::AssertSqlSafe(query));
617 row = match identifier.len() {
618 8 => row.bind(u32::from_str_radix(identifier, 16).map_err(anyhow::Error::new)? as i32),
619 36 => row.bind(uuid::Uuid::parse_str(identifier).map_err(anyhow::Error::new)?),
620 _ => return Ok(None),
621 };
622 let row = row.fetch_optional(database.read()).await?;
623
624 row.try_map(|row| Self::map(None, &row))
625 }
626
627 pub async fn by_user_identifier(
631 database: &crate::database::Database,
632 permissions: &super::user::PermissionManager,
633 identifier: &str,
634 ) -> Result<Option<Self>, anyhow::Error> {
635 let server = match identifier.len() {
636 8 => Self::resolve_cached(database, identifier).await?,
637 36 => match uuid::Uuid::parse_str(identifier) {
638 Ok(uuid) => Self::by_uuid_optional_cached(database, uuid).await?,
639 Err(_) => None,
640 },
641 _ => None,
642 };
643
644 let Some(mut server) = server else {
645 return Ok(None);
646 };
647
648 if server.owner.uuid == permissions.user_uuid() {
649 return Ok(Some(server));
650 }
651
652 if let Some((subuser_permissions, ignored_files)) =
653 super::server_subuser::ServerSubuser::permissions_by_server_uuid_user_uuid_cached(
654 database,
655 server.uuid,
656 permissions.user_uuid(),
657 )
658 .await?
659 {
660 server.subuser_permissions = Some(Arc::new(subuser_permissions));
661 server.subuser_ignored_files = Some(ignored_files);
662
663 return Ok(Some(server));
664 }
665
666 Ok(permissions
667 .has_admin_permission("servers.read")
668 .is_ok()
669 .then_some(server))
670 }
671
672 pub async fn by_user_uuids(
673 database: &crate::database::Database,
674 permissions: &super::user::PermissionManager,
675 uuids: &[uuid::Uuid],
676 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
677 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
678 r#"
679 SELECT {}, server_subusers.permissions, server_subusers.ignored_files
680 FROM servers
681 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
682 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
683 JOIN users ON users.uuid = servers.owner_uuid
684 LEFT JOIN roles ON roles.uuid = users.role_uuid
685 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
686 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
687 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
688 WHERE servers.uuid = ANY($3) AND (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1 OR $2)
689 ORDER BY array_position($3, servers.uuid)
690 "#,
691 Self::columns_sql(None)
692 )))
693 .bind(permissions.user_uuid())
694 .bind(permissions.has_admin_permission("servers.read").is_ok())
695 .bind(uuids)
696 .fetch_all(database.read())
697 .await?;
698
699 rows.into_iter()
700 .map(|row| Self::map(None, &row))
701 .try_collect_vec()
702 }
703
704 pub async fn by_owner_uuid_with_pagination(
705 database: &crate::database::Database,
706 owner_uuid: uuid::Uuid,
707 page: i64,
708 per_page: i64,
709 search: Option<&str>,
710 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
711 super::PaginatedQueryBuilder::new(
712 Self::columns_sql(None),
713 format!(
714 r#"
715 FROM servers
716 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
717 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
718 JOIN users ON users.uuid = servers.owner_uuid
719 LEFT JOIN roles ON roles.uuid = users.role_uuid
720 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
721 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
722 WHERE servers.owner_uuid = $1 AND {search}
723 "#,
724 search = super::search_sql(2, &["servers.name"], &["servers.uuid"]),
725 ),
726 "servers.created, servers.uuid",
727 )
728 .bind(owner_uuid)
729 .bind(search)
730 .fetch(database, page, per_page)
731 .await
732 }
733
734 pub async fn by_user_uuid_server_order_with_pagination(
735 database: &crate::database::Database,
736 permissions: &super::user::PermissionManager,
737 server_order: &[uuid::Uuid],
738 page: i64,
739 per_page: i64,
740 search: Option<&str>,
741 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
742 super::PaginatedQueryBuilder::new(
743 Self::columns_sql(None),
744 format!(
745 r#"
746 FROM servers
747 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
748 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
749 JOIN users ON users.uuid = servers.owner_uuid
750 LEFT JOIN roles ON roles.uuid = users.role_uuid
751 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
752 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
753 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
754 WHERE servers.uuid = ANY($2)
755 AND (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1 OR $4)
756 AND {search}
757 "#,
758 search = super::search_sql(3, &["servers.name", "users.username", "users.email"], &["servers.uuid"]),
759 ),
760 "array_position($2, servers.uuid), servers.created",
761 )
762 .bind(permissions.user_uuid())
763 .bind(server_order)
764 .bind(search)
765 .bind(permissions.has_admin_permission("servers.read").is_ok())
766 .fetch(database, page, per_page)
767 .await
768 }
769
770 pub async fn by_user_uuid_with_pagination(
771 database: &crate::database::Database,
772 user_uuid: uuid::Uuid,
773 page: i64,
774 per_page: i64,
775 search: Option<&str>,
776 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
777 super::PaginatedQueryBuilder::new(
778 format!("{}, server_subusers.permissions, server_subusers.ignored_files", Self::columns_sql(None)),
779 format!(
780 r#"
781 FROM servers
782 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
783 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
784 JOIN users ON users.uuid = servers.owner_uuid
785 LEFT JOIN roles ON roles.uuid = users.role_uuid
786 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
787 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
788 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
789 WHERE
790 servers.uuid IN (
791 SELECT owned.uuid FROM servers owned WHERE owned.owner_uuid = $1
792 UNION ALL
793 SELECT server_subusers.server_uuid FROM server_subusers WHERE server_subusers.user_uuid = $1
794 )
795 AND {search}
796 "#,
797 search = super::search_sql(2, &["servers.name", "users.username", "users.email"], &["servers.uuid"]),
798 ),
799 "servers.created, servers.uuid",
800 )
801 .bind(user_uuid)
802 .bind(search)
803 .fetch(database, page, per_page)
804 .await
805 }
806
807 pub async fn all_uuids_by_node_uuid_user_uuid(
808 database: &crate::database::Database,
809 node_uuid: uuid::Uuid,
810 user_uuid: uuid::Uuid,
811 ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
812 let rows = sqlx::query(
813 r#"
814 SELECT servers.uuid
815 FROM servers
816 WHERE servers.node_uuid = $1 AND servers.uuid IN (
817 SELECT owned.uuid FROM servers owned WHERE owned.owner_uuid = $2
818 UNION ALL
819 SELECT server_subusers.server_uuid FROM server_subusers WHERE server_subusers.user_uuid = $2
820 )
821 ORDER BY servers.created, servers.uuid
822 "#
823 )
824 .bind(node_uuid)
825 .bind(user_uuid)
826 .fetch_all(database.read())
827 .await?;
828
829 Ok(rows
830 .into_iter()
831 .map(|row| row.get::<uuid::Uuid, _>("uuid"))
832 .collect())
833 }
834
835 pub async fn by_not_user_uuid_with_pagination(
836 database: &crate::database::Database,
837 user_uuid: uuid::Uuid,
838 page: i64,
839 per_page: i64,
840 search: Option<&str>,
841 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
842 super::PaginatedQueryBuilder::new(
843 format!("{}, server_subusers.permissions, server_subusers.ignored_files", Self::columns_sql(None)),
844 format!(
845 r#"
846 FROM servers
847 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
848 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
849 JOIN users ON users.uuid = servers.owner_uuid
850 LEFT JOIN roles ON roles.uuid = users.role_uuid
851 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
852 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
853 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
854 WHERE
855 servers.owner_uuid != $1 AND (server_subusers.user_uuid IS NULL OR server_subusers.user_uuid != $1)
856 AND {search}
857 "#,
858 search = super::search_sql(2, &["servers.name", "users.username", "users.email"], &["servers.uuid"]),
859 ),
860 "servers.created, servers.uuid",
861 )
862 .bind(user_uuid)
863 .bind(search)
864 .fetch(database, page, per_page)
865 .await
866 }
867
868 pub async fn tunnel_available_by_user_uuid_with_pagination(
871 database: &crate::database::Database,
872 user_uuid: uuid::Uuid,
873 server_uuid: uuid::Uuid,
874 page: i64,
875 per_page: i64,
876 search: Option<&str>,
877 exclude: &HashSet<(uuid::Uuid, uuid::Uuid)>,
878 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
879 let (excluded_egg_uuids, excluded_node_uuids): (Vec<_>, Vec<_>) =
880 exclude.iter().copied().unzip();
881
882 super::PaginatedQueryBuilder::new(
883 format!("{}, server_subusers.permissions, server_subusers.ignored_files", Self::columns_sql(None)),
884 format!(
885 r#"
886 FROM server_tunnels
887 JOIN servers ON servers.uuid = server_tunnels.server_uuid
888 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
889 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
890 JOIN users ON users.uuid = servers.owner_uuid
891 LEFT JOIN roles ON roles.uuid = users.role_uuid
892 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
893 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
894 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
895 WHERE
896 servers.uuid IN (
897 SELECT owned.uuid FROM servers owned WHERE owned.owner_uuid = $1
898 UNION ALL
899 SELECT server_subusers.server_uuid FROM server_subusers WHERE server_subusers.user_uuid = $1
900 )
901 AND servers.uuid != $2
902 AND NOT servers.suspended
903 AND servers.destination_node_uuid IS NULL
904 AND servers.status IS NULL
905 AND (servers.egg_uuid, servers.node_uuid) NOT IN (SELECT * FROM UNNEST($4::uuid[], $5::uuid[]))
906 AND {search}
907 "#,
908 search = super::search_sql(3, &["servers.name", "users.username", "users.email"], &["servers.uuid"]),
909 ),
910 "servers.created, servers.uuid",
911 )
912 .bind(user_uuid)
913 .bind(server_uuid)
914 .bind(search)
915 .bind(excluded_egg_uuids)
916 .bind(excluded_node_uuids)
917 .fetch(database, page, per_page)
918 .await
919 }
920
921 pub async fn tunnel_available_by_not_user_uuid_with_pagination(
922 database: &crate::database::Database,
923 user_uuid: uuid::Uuid,
924 server_uuid: uuid::Uuid,
925 page: i64,
926 per_page: i64,
927 search: Option<&str>,
928 exclude: &HashSet<(uuid::Uuid, uuid::Uuid)>,
929 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
930 let (excluded_egg_uuids, excluded_node_uuids): (Vec<_>, Vec<_>) =
931 exclude.iter().copied().unzip();
932
933 super::PaginatedQueryBuilder::new(
934 format!("{}, server_subusers.permissions, server_subusers.ignored_files", Self::columns_sql(None)),
935 format!(
936 r#"
937 FROM server_tunnels
938 JOIN servers ON servers.uuid = server_tunnels.server_uuid
939 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
940 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
941 JOIN users ON users.uuid = servers.owner_uuid
942 LEFT JOIN roles ON roles.uuid = users.role_uuid
943 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
944 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
945 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
946 WHERE
947 servers.owner_uuid != $1 AND (server_subusers.user_uuid IS NULL OR server_subusers.user_uuid != $1)
948 AND servers.uuid != $2
949 AND NOT servers.suspended
950 AND servers.destination_node_uuid IS NULL
951 AND servers.status IS NULL
952 AND (servers.egg_uuid, servers.node_uuid) NOT IN (SELECT * FROM UNNEST($4::uuid[], $5::uuid[]))
953 AND {search}
954 "#,
955 search = super::search_sql(3, &["servers.name", "users.username", "users.email"], &["servers.uuid"]),
956 ),
957 "servers.created, servers.uuid",
958 )
959 .bind(user_uuid)
960 .bind(server_uuid)
961 .bind(search)
962 .bind(excluded_egg_uuids)
963 .bind(excluded_node_uuids)
964 .fetch(database, page, per_page)
965 .await
966 }
967
968 pub async fn all_with_firewall_by_egg_node_pairs(
969 database: &crate::database::Database,
970 pairs: &HashSet<(uuid::Uuid, uuid::Uuid)>,
971 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
972 let (egg_uuids, node_uuids): (Vec<_>, Vec<_>) = pairs.iter().copied().unzip();
973
974 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
975 r#"
976 SELECT {}
977 FROM server_firewalls
978 JOIN servers ON servers.uuid = server_firewalls.server_uuid
979 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
980 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
981 JOIN users ON users.uuid = servers.owner_uuid
982 LEFT JOIN roles ON roles.uuid = users.role_uuid
983 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
984 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
985 WHERE (servers.egg_uuid, servers.node_uuid) IN (SELECT * FROM UNNEST($1::uuid[], $2::uuid[]))
986 "#,
987 Self::columns_sql(None)
988 )))
989 .bind(egg_uuids)
990 .bind(node_uuids)
991 .fetch_all(database.write())
992 .await?;
993
994 rows.into_iter()
995 .map(|row| Self::map(None, &row))
996 .try_collect_vec()
997 }
998
999 pub async fn by_node_uuid_with_pagination(
1000 database: &crate::database::Database,
1001 node_uuid: uuid::Uuid,
1002 page: i64,
1003 per_page: i64,
1004 search: Option<&str>,
1005 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1006 super::PaginatedQueryBuilder::new(
1007 Self::columns_sql(None),
1008 format!(
1009 r#"
1010 FROM servers
1011 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1012 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1013 JOIN users ON users.uuid = servers.owner_uuid
1014 LEFT JOIN roles ON roles.uuid = users.role_uuid
1015 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1016 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1017 WHERE servers.node_uuid = $1 AND {search}
1018 "#,
1019 search = super::search_sql(2, &["servers.name"], &["servers.uuid"]),
1020 ),
1021 "servers.created, servers.uuid",
1022 )
1023 .bind(node_uuid)
1024 .bind(search)
1025 .fetch(database, page, per_page)
1026 .await
1027 }
1028
1029 pub async fn by_node_uuid_transferring_with_pagination(
1030 database: &crate::database::Database,
1031 node_uuid: uuid::Uuid,
1032 page: i64,
1033 per_page: i64,
1034 search: Option<&str>,
1035 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1036 super::PaginatedQueryBuilder::new(
1037 Self::columns_sql(None),
1038 format!(
1039 r#"
1040 FROM servers
1041 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1042 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1043 JOIN users ON users.uuid = servers.owner_uuid
1044 LEFT JOIN roles ON roles.uuid = users.role_uuid
1045 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1046 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1047 WHERE servers.node_uuid = $1 AND servers.destination_node_uuid IS NOT NULL
1048 AND {search}
1049 "#,
1050 search = super::search_sql(2, &["servers.name"], &["servers.uuid"]),
1051 ),
1052 "servers.created, servers.uuid",
1053 )
1054 .bind(node_uuid)
1055 .bind(search)
1056 .fetch(database, page, per_page)
1057 .await
1058 }
1059
1060 pub async fn by_egg_uuid_with_pagination(
1061 database: &crate::database::Database,
1062 egg_uuid: uuid::Uuid,
1063 page: i64,
1064 per_page: i64,
1065 search: Option<&str>,
1066 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1067 super::PaginatedQueryBuilder::new(
1068 Self::columns_sql(None),
1069 format!(
1070 r#"
1071 FROM servers
1072 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1073 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1074 JOIN users ON users.uuid = servers.owner_uuid
1075 LEFT JOIN roles ON roles.uuid = users.role_uuid
1076 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1077 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1078 WHERE servers.egg_uuid = $1 AND {search}
1079 "#,
1080 search = super::search_sql(2, &["servers.name"], &["servers.uuid"]),
1081 ),
1082 "servers.created, servers.uuid",
1083 )
1084 .bind(egg_uuid)
1085 .bind(search)
1086 .fetch(database, page, per_page)
1087 .await
1088 }
1089
1090 pub async fn by_backup_configuration_uuid_with_pagination(
1091 database: &crate::database::Database,
1092 backup_configuration_uuid: uuid::Uuid,
1093 page: i64,
1094 per_page: i64,
1095 search: Option<&str>,
1096 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1097 super::PaginatedQueryBuilder::new(
1098 Self::columns_sql(None),
1099 format!(
1100 r#"
1101 FROM servers
1102 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1103 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1104 JOIN users ON users.uuid = servers.owner_uuid
1105 LEFT JOIN roles ON roles.uuid = users.role_uuid
1106 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1107 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1108 WHERE servers.backup_configuration_uuid = $1 AND {search}
1109 "#,
1110 search = super::search_sql(2, &["servers.name"], &["servers.uuid"]),
1111 ),
1112 "servers.created, servers.uuid",
1113 )
1114 .bind(backup_configuration_uuid)
1115 .bind(search)
1116 .fetch(database, page, per_page)
1117 .await
1118 }
1119
1120 pub async fn all_with_pagination(
1121 database: &crate::database::Database,
1122 page: i64,
1123 per_page: i64,
1124 search: Option<&str>,
1125 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1126 super::PaginatedQueryBuilder::new(
1127 Self::columns_sql(None),
1128 format!(
1129 r#"
1130 FROM servers
1131 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1132 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1133 JOIN users ON users.uuid = servers.owner_uuid
1134 LEFT JOIN roles ON roles.uuid = users.role_uuid
1135 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1136 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1137 WHERE {search}
1138 "#,
1139 search = super::search_sql(1, &["servers.name"], &["servers.uuid"]),
1140 ),
1141 "servers.created, servers.uuid",
1142 )
1143 .bind(search)
1144 .fetch(database, page, per_page)
1145 .await
1146 }
1147
1148 pub async fn count_by_user_uuid(
1149 database: &crate::database::Database,
1150 user_uuid: uuid::Uuid,
1151 ) -> Result<i64, sqlx::Error> {
1152 sqlx::query_scalar(
1153 r#"
1154 SELECT COUNT(*)
1155 FROM servers
1156 WHERE servers.owner_uuid = $1
1157 "#,
1158 )
1159 .bind(user_uuid)
1160 .fetch_one(database.read())
1161 .await
1162 }
1163
1164 pub async fn count_by_node_uuid(
1165 database: &crate::database::Database,
1166 node_uuid: uuid::Uuid,
1167 ) -> Result<i64, sqlx::Error> {
1168 sqlx::query_scalar(
1169 r#"
1170 SELECT COUNT(*)
1171 FROM servers
1172 WHERE servers.node_uuid = $1
1173 "#,
1174 )
1175 .bind(node_uuid)
1176 .fetch_one(database.read())
1177 .await
1178 }
1179
1180 pub async fn count_by_egg_uuid(
1181 database: &crate::database::Database,
1182 egg_uuid: uuid::Uuid,
1183 ) -> Result<i64, sqlx::Error> {
1184 sqlx::query_scalar(
1185 r#"
1186 SELECT COUNT(*)
1187 FROM servers
1188 WHERE servers.egg_uuid = $1
1189 "#,
1190 )
1191 .bind(egg_uuid)
1192 .fetch_one(database.read())
1193 .await
1194 }
1195
1196 pub async fn fetch_status(
1198 &self,
1199 database: &crate::database::Database,
1200 ) -> Result<Option<ServerStatus>, crate::database::DatabaseError> {
1201 let status = sqlx::query_scalar(
1202 r#"
1203 SELECT status
1204 FROM servers
1205 WHERE servers.uuid = $1
1206 "#,
1207 )
1208 .bind(self.uuid)
1209 .fetch_one(database.read())
1210 .await?;
1211
1212 Ok(status)
1213 }
1214
1215 pub async fn try_set_status_by_uuid(
1218 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
1219 uuid: uuid::Uuid,
1220 from: Option<ServerStatus>,
1221 to: Option<ServerStatus>,
1222 ) -> Result<bool, sqlx::Error> {
1223 let rows_affected = sqlx::query!(
1224 "UPDATE servers
1225 SET status = $2
1226 WHERE servers.uuid = $1 AND servers.status IS NOT DISTINCT FROM $3",
1227 uuid,
1228 to as Option<ServerStatus>,
1229 from as Option<ServerStatus>
1230 )
1231 .execute(executor)
1232 .await?
1233 .rows_affected();
1234
1235 Ok(rows_affected > 0)
1236 }
1237
1238 pub async fn set_status(
1240 &mut self,
1241 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
1242 status: Option<ServerStatus>,
1243 ) -> Result<(), sqlx::Error> {
1244 sqlx::query!(
1245 "UPDATE servers
1246 SET status = $2
1247 WHERE servers.uuid = $1",
1248 self.uuid,
1249 status as Option<ServerStatus>
1250 )
1251 .execute(executor)
1252 .await?;
1253
1254 self.status = status;
1255
1256 Ok(())
1257 }
1258
1259 pub async fn try_set_status(
1262 &mut self,
1263 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
1264 from: Option<ServerStatus>,
1265 to: Option<ServerStatus>,
1266 ) -> Result<bool, sqlx::Error> {
1267 if !Self::try_set_status_by_uuid(executor, self.uuid, from, to).await? {
1268 return Ok(false);
1269 }
1270
1271 self.status = to;
1272
1273 Ok(true)
1274 }
1275
1276 pub async fn egg_configuration(
1277 &self,
1278 database: &crate::database::Database,
1279 ) -> Result<super::egg_configuration::MergedEggConfiguration, anyhow::Error> {
1280 let node = self.node.fetch_cached(database).await?;
1281
1282 super::egg_configuration::EggConfiguration::merged_for_target(
1283 database,
1284 &super::egg_configuration::EggConfigurationTarget::new(&self.egg, &node),
1285 )
1286 .await
1287 }
1288
1289 pub async fn sync(self, state: &crate::State) -> Result<(), anyhow::Error> {
1291 self.node
1292 .fetch_cached(&state.database)
1293 .await?
1294 .api_client(&state.database)
1295 .await?
1296 .post_servers_server_sync(
1297 self.uuid,
1298 &wings_api::servers_server_sync::post::RequestBody {
1299 server: serde_json::to_value(self.into_remote_api_object(state).await?)?,
1300 },
1301 )
1302 .await?;
1303
1304 Ok(())
1305 }
1306
1307 pub async fn batch_sync(self, state: &crate::State) {
1311 state
1312 .database
1313 .batch_action("sync_server", self.uuid, {
1314 let state = state.clone();
1315
1316 async move { self.sync(&state).await }
1317 })
1318 .await;
1319 }
1320
1321 pub async fn install(
1325 &self,
1326 state: &crate::State,
1327 truncate_directory: bool,
1328 installation_script: Option<wings_api::InstallationScript>,
1329 ) -> Result<(), anyhow::Error> {
1330 self.install_with_options(
1331 state,
1332 ServerInstallOptions {
1333 truncate_directory,
1334 installation_script,
1335 ..Default::default()
1336 },
1337 )
1338 .await
1339 }
1340
1341 pub async fn install_with_options(
1343 &self,
1344 state: &crate::State,
1345 options: ServerInstallOptions,
1346 ) -> Result<(), anyhow::Error> {
1347 let ServerInstallOptions {
1348 truncate_directory,
1349 installation_script,
1350 start_on_completion,
1351 } = options;
1352
1353 let mut transaction = state.database.write().begin().await?;
1354
1355 if !Self::try_set_status_by_uuid(
1356 &mut *transaction,
1357 self.uuid,
1358 None,
1359 Some(ServerStatus::Installing),
1360 )
1361 .await?
1362 {
1363 transaction.rollback().await?;
1364
1365 return Err(DisplayError::new(
1366 "server is already installing or in an invalid state for reinstalling",
1367 )
1368 .into());
1369 }
1370
1371 match self
1372 .node
1373 .fetch_cached(&state.database)
1374 .await?
1375 .api_client(&state.database)
1376 .await?
1377 .post_servers_server_reinstall_with(
1378 self.uuid,
1379 &wings_api::servers_server_reinstall::post::RequestBody {
1380 truncate_directory,
1381 installation_script: Some(
1382 if let Some(installation_script) = &installation_script {
1383 installation_script.clone()
1384 } else {
1385 wings_api::InstallationScript {
1386 container_image: self.egg.config_script.container.clone(),
1387 entrypoint: self.egg.config_script.entrypoint.clone(),
1388 script: self.egg.config_script.content.to_compact_string(),
1389 environment: Default::default(),
1390 }
1391 },
1392 ),
1393 },
1394 &wings_api::servers_server_reinstall::post::Extra {
1395 start_on_completion,
1396 ..Default::default()
1397 },
1398 )
1399 .await
1400 {
1401 Ok(_) => {}
1402 Err(err) => {
1403 transaction.rollback().await?;
1404
1405 return Err(err.into());
1406 }
1407 };
1408
1409 transaction.commit().await?;
1410
1411 Self::get_event_emitter().emit(
1412 state.clone(),
1413 events::ServerEvent::InstallStarted {
1414 server: Box::new(self.clone()),
1415 installation_script: Box::new(
1416 if let Some(installation_script) = installation_script {
1417 installation_script
1418 } else {
1419 wings_api::InstallationScript {
1420 container_image: self.egg.config_script.container.clone(),
1421 entrypoint: self.egg.config_script.entrypoint.clone(),
1422 script: self.egg.config_script.content.to_compact_string(),
1423 environment: Default::default(),
1424 }
1425 },
1426 ),
1427 },
1428 );
1429
1430 Ok(())
1431 }
1432
1433 pub async fn transfer(
1437 self,
1438 state: &crate::State,
1439 options: ServerTransferOptions,
1440 ) -> Result<(), anyhow::Error> {
1441 if self.destination_node.is_some() {
1442 return Err(DisplayError::new("server is already being transferred")
1443 .with_status(axum::http::StatusCode::CONFLICT)
1444 .into());
1445 }
1446
1447 if matches!(
1448 self.status,
1449 Some(ServerStatus::Installing) | Some(ServerStatus::RestoringBackup)
1450 ) {
1451 return Err(DisplayError::new(
1452 "server is installing or restoring a backup and cannot be transferred",
1453 )
1454 .with_status(axum::http::StatusCode::CONFLICT)
1455 .into());
1456 }
1457
1458 if self.node.uuid == options.destination_node.uuid {
1459 return Err(DisplayError::new(
1460 "destination node must be different from the current node",
1461 )
1462 .with_status(axum::http::StatusCode::CONFLICT)
1463 .into());
1464 }
1465
1466 if options.destination_node.is_all_in_one_node() {
1467 return Err(DisplayError::new("cannot transfer to an all-in-one node")
1468 .with_status(axum::http::StatusCode::CONFLICT)
1469 .into());
1470 }
1471
1472 if options.multiplex_channels > MAX_TRANSFER_MULTIPLEX_CHANNELS {
1473 return Err(DisplayError::new(format!(
1474 "multiplex channels cannot exceed {MAX_TRANSFER_MULTIPLEX_CHANNELS}"
1475 ))
1476 .with_status(axum::http::StatusCode::BAD_REQUEST)
1477 .into());
1478 }
1479
1480 let mut requested_allocations = options.allocation_uuids.clone();
1481 requested_allocations.extend(options.allocation_uuid);
1482 requested_allocations.sort_unstable();
1483 requested_allocations.dedup();
1484
1485 if !requested_allocations.is_empty() {
1486 let owned = sqlx::query!(
1487 "SELECT COUNT(*) AS count FROM node_allocations
1488 WHERE node_allocations.uuid = ANY($1) AND node_allocations.node_uuid = $2",
1489 &requested_allocations,
1490 options.destination_node.uuid
1491 )
1492 .fetch_one(state.database.read())
1493 .await?
1494 .count
1495 .unwrap_or(0);
1496
1497 if owned != requested_allocations.len() as i64 {
1498 return Err(DisplayError::new(
1499 "all allocations must belong to the destination node",
1500 )
1501 .with_status(axum::http::StatusCode::BAD_REQUEST)
1502 .into());
1503 }
1504 }
1505
1506 let mut transaction = state.database.write().begin().await?;
1507
1508 let destination_allocation_uuid = if let Some(allocation_uuid) = options.allocation_uuid {
1509 match sqlx::query!(
1510 "INSERT INTO server_allocations (server_uuid, allocation_uuid)
1511 VALUES ($1, $2)
1512 ON CONFLICT DO NOTHING
1513 RETURNING uuid",
1514 self.uuid,
1515 allocation_uuid
1516 )
1517 .fetch_optional(&mut *transaction)
1518 .await?
1519 {
1520 Some(row) => Some(row.uuid),
1521 None => {
1522 return Err(DisplayError::new(
1523 "the primary allocation is already assigned to a server",
1524 )
1525 .with_status(axum::http::StatusCode::CONFLICT)
1526 .into());
1527 }
1528 }
1529 } else {
1530 None
1531 };
1532
1533 sqlx::query!(
1534 "UPDATE servers
1535 SET destination_node_uuid = $2, destination_allocation_uuid = $3
1536 WHERE servers.uuid = $1",
1537 self.uuid,
1538 options.destination_node.uuid,
1539 destination_allocation_uuid
1540 )
1541 .execute(&mut *transaction)
1542 .await?;
1543
1544 if !options.allocation_uuids.is_empty() {
1545 sqlx::query!(
1546 "INSERT INTO server_allocations (server_uuid, allocation_uuid)
1547 SELECT $1, UNNEST($2::uuid[])
1548 ON CONFLICT DO NOTHING",
1549 self.uuid,
1550 &options.allocation_uuids
1551 )
1552 .execute(&mut *transaction)
1553 .await?;
1554 }
1555
1556 let token = options
1557 .destination_node
1558 .create_jwt(
1559 &state.database,
1560 &state.jwt,
1561 &crate::jwt::BasePayload {
1562 scope: "transfer".into(),
1563 issuer: "panel".into(),
1564 subject: Some(self.uuid.to_compact_string()),
1565 audience: Vec::new(),
1566 expiration_time: Some(chrono::Utc::now().timestamp() + 600),
1567 not_before: None,
1568 issued_at: Some(chrono::Utc::now().timestamp()),
1569 jwt_id: self.node.uuid.to_compact_string(),
1570 },
1571 )
1572 .await?;
1573
1574 let url = options.destination_node.url("/api/transfers");
1575
1576 match self
1579 .node
1580 .fetch_cached(&state.database)
1581 .await?
1582 .api_client(&state.database)
1583 .await?
1584 .post_servers_server_transfer(
1585 self.uuid,
1586 &wings_api::servers_server_transfer::post::RequestBody {
1587 url: url.to_compact_string(),
1588 token: format!("Bearer {token}").into(),
1589 backups: options.backups,
1590 delete_backups: options.delete_source_backups,
1591 archive_format: options.archive_format,
1592 compression_level: options.compression_level,
1593 multiplex_streams: options.multiplex_channels,
1594 },
1595 )
1596 .await
1597 {
1598 Ok(_) => {}
1599 Err(err) => {
1600 transaction.rollback().await?;
1601
1602 return Err(err.into());
1603 }
1604 }
1605
1606 transaction.commit().await?;
1607
1608 Self::invalidate_cached(&state.database, self.uuid).await;
1609
1610 Server::get_event_emitter().emit(
1611 state.clone(),
1612 ServerEvent::TransferStarted {
1613 server: Box::new(self),
1614 destination_node: Box::new(options.destination_node),
1615 destination_allocation: destination_allocation_uuid,
1616 destination_allocations: options.allocation_uuids,
1617 },
1618 );
1619
1620 Ok(())
1621 }
1622
1623 fn scope_allows_console_streams(scope: Option<&[compact_str::CompactString]>) -> bool {
1624 scope.is_none_or(|scope| scope.iter().any(|p| p == "control.read-console"))
1625 }
1626
1627 fn push_granted_permissions<'a>(
1628 permissions: &mut Vec<&'a str>,
1629 granted: impl Iterator<Item = &'a compact_str::CompactString>,
1630 scope: Option<&[compact_str::CompactString]>,
1631 settings: &crate::settings::AppSettings,
1632 ) {
1633 for permission in granted {
1634 if scope.is_some_and(|scope| !scope.contains(permission))
1635 || permissions.contains(&permission.as_str())
1636 {
1637 continue;
1638 }
1639
1640 if permission == "control.read-console" {
1641 if settings.server.allow_viewing_installation_logs {
1642 permissions.push("admin.websocket.install");
1643 }
1644 if settings.server.allow_viewing_transfer_progress {
1645 permissions.push("admin.websocket.transfer");
1646 }
1647 }
1648
1649 permissions.push(permission.as_str());
1650 }
1651 }
1652
1653 pub fn wings_permissions<'a>(
1654 &'a self,
1655 settings: &crate::settings::AppSettings,
1656 user: &'a super::user::User,
1657 scope: &'a crate::models::user::CredentialScope,
1658 ) -> Vec<&'a str> {
1659 let scope = scope.server_permissions();
1660 let mut permissions = vec!["websocket.connect", "meta.calagopus"];
1661
1662 if user.admin {
1663 permissions.reserve(scope.map_or(1, |s| s.len()) + 3);
1664
1665 crate::utils::push_scope_or_star(&mut permissions, scope);
1666
1667 if Self::scope_allows_console_streams(scope) {
1668 permissions.push("admin.websocket.errors");
1669 permissions.push("admin.websocket.install");
1670 permissions.push("admin.websocket.transfer");
1671 }
1672
1673 return permissions;
1674 }
1675
1676 if self.owner.uuid == user.uuid {
1677 permissions.reserve(scope.map_or(1, |s| s.len()) + 2);
1678
1679 if Self::scope_allows_console_streams(scope) {
1680 if settings.server.allow_viewing_installation_logs {
1681 permissions.push("admin.websocket.install");
1682 }
1683 if settings.server.allow_viewing_transfer_progress {
1684 permissions.push("admin.websocket.transfer");
1685 }
1686 }
1687
1688 crate::utils::push_scope_or_star(&mut permissions, scope);
1689
1690 return permissions;
1691 }
1692
1693 let role_permissions = user
1698 .role
1699 .as_ref()
1700 .map_or(&[][..], |role| role.server_permissions.as_slice());
1701 let subuser_permissions = self
1702 .subuser_permissions
1703 .iter()
1704 .flat_map(|permissions| permissions.iter());
1705
1706 permissions.reserve(
1707 role_permissions.len() + self.subuser_permissions.as_ref().map_or(0, |p| p.len()),
1708 );
1709 Self::push_granted_permissions(
1710 &mut permissions,
1711 role_permissions.iter().chain(subuser_permissions),
1712 scope,
1713 settings,
1714 );
1715
1716 permissions
1717 }
1718
1719 pub async fn feature_limits(
1721 &self,
1722 state: &crate::State,
1723 ) -> Result<ApiServerFeatureLimits, anyhow::Error> {
1724 let feature_limits = ApiServerFeatureLimits::init_hooks(self, state).await?;
1725
1726 let feature_limits = finish_extendible!(
1727 ApiServerFeatureLimits {
1728 allocations: self.allocation_limit,
1729 databases: self.database_limit,
1730 backups: self.backup_limit,
1731 schedules: self.schedule_limit,
1732 },
1733 feature_limits,
1734 state
1735 )?;
1736
1737 Ok(feature_limits)
1738 }
1739
1740 pub async fn backup_configuration(
1741 &self,
1742 database: &crate::database::Database,
1743 ) -> Option<super::backup_configuration::BackupConfiguration> {
1744 if let Some(backup_configuration) = &self.backup_configuration
1745 && let Ok(backup_configuration) = backup_configuration.fetch_cached(database).await
1746 {
1747 return Some(backup_configuration);
1748 }
1749
1750 let node = self.node.fetch_cached(database).await.ok()?;
1751
1752 if let Some(backup_configuration) = node.backup_configuration
1753 && let Ok(backup_configuration) = backup_configuration.fetch_cached(database).await
1754 {
1755 return Some(backup_configuration);
1756 }
1757
1758 if let Some(backup_configuration) = node.location.backup_configuration
1759 && let Ok(backup_configuration) = backup_configuration.fetch_cached(database).await
1760 {
1761 return Some(backup_configuration);
1762 }
1763
1764 None
1765 }
1766
1767 fn subuser_ignored_files_list(&mut self) -> Option<Option<&crate::ignore_list::IgnoreList>> {
1771 let ignored_files = self.subuser_ignored_files.as_ref()?;
1772
1773 if self.subuser_ignored_files_list.is_none() {
1774 match crate::ignore_list::IgnoreList::try_from_lines(ignored_files) {
1775 Ok(list) => self.subuser_ignored_files_list = Some(Box::new(list)),
1776 Err(err) => {
1777 tracing::error!(
1778 server = %self.uuid,
1779 "failed to compile subuser ignored files, denying access: {:#?}",
1780 err
1781 );
1782
1783 return Some(None);
1784 }
1785 }
1786 }
1787
1788 Some(self.subuser_ignored_files_list.as_deref())
1789 }
1790
1791 pub fn is_ignored(&mut self, path: impl AsRef<std::path::Path>, is_dir: bool) -> bool {
1793 match self.subuser_ignored_files_list() {
1794 Some(Some(list)) => is_path_ignored(list, path, is_dir),
1795 Some(None) => true,
1796 None => false,
1797 }
1798 }
1799
1800 pub fn is_ignored_subtree(&mut self, path: impl AsRef<std::path::Path>) -> bool {
1804 match self.subuser_ignored_files_list() {
1805 Some(Some(list)) => is_path_ignored_subtree(list, path, true),
1806 Some(None) => true,
1807 None => false,
1808 }
1809 }
1810
1811 pub fn is_ignored_either(&mut self, path: impl AsRef<std::path::Path>) -> bool {
1812 let path = path.as_ref();
1813
1814 self.is_ignored(path, false) || self.is_ignored(path, true)
1815 }
1816
1817 pub fn effective_trash(&self, settings: &crate::settings::AppSettings) -> TrashPolicy {
1818 self.trash.unwrap_or(settings.server.trash).into()
1819 }
1820
1821 pub fn api_trash(&self, settings: &crate::settings::AppSettings) -> ApiServerTrash {
1822 let TrashPolicy {
1823 retention,
1824 usage_threshold,
1825 } = self.effective_trash(settings);
1826
1827 ApiServerTrash {
1828 retention,
1829 usage_threshold,
1830 custom: self.trash.is_some(),
1831 defaults: settings.server.trash,
1832 }
1833 }
1834
1835 #[inline]
1836 pub async fn into_remote_api_object(
1837 self,
1838 state: &crate::State,
1839 ) -> Result<RemoteApiServer, anyhow::Error> {
1840 let trash = self
1841 .trash
1842 .unwrap_or(state.settings.get().await?.server.trash);
1843 let firewall_enabled = self
1844 .egg_configuration(&state.database)
1845 .await?
1846 .firewall_enabled();
1847
1848 let (variables, backups, schedules, mounts, devices, allocations, firewall_rules) = tokio::try_join!(
1849 sqlx::query!(
1850 "SELECT nest_egg_variables.env_variable, COALESCE(server_variables.value, nest_egg_variables.default_value) AS value
1851 FROM nest_egg_variables
1852 LEFT JOIN server_variables ON server_variables.variable_uuid = nest_egg_variables.uuid AND server_variables.server_uuid = $1
1853 WHERE nest_egg_variables.egg_uuid = $2",
1854 self.uuid,
1855 self.egg.uuid
1856 )
1857 .fetch_all(state.database.read()),
1858 sqlx::query!(
1859 "SELECT server_backups.uuid
1860 FROM server_backups
1861 WHERE server_backups.server_uuid = $1",
1862 self.uuid
1863 )
1864 .fetch_all(state.database.read()),
1865 sqlx::query!(
1866 "SELECT server_schedules.uuid, server_schedules.triggers, server_schedules.condition
1867 FROM server_schedules
1868 WHERE server_schedules.server_uuid = $1 AND server_schedules.enabled",
1869 self.uuid
1870 )
1871 .fetch_all(state.database.read()),
1872 sqlx::query!(
1873 "SELECT mounts.source, mounts.target, mounts.read_only
1874 FROM server_mounts
1875 JOIN mounts ON mounts.uuid = server_mounts.mount_uuid
1876 WHERE server_mounts.server_uuid = $1",
1877 self.uuid
1878 )
1879 .fetch_all(state.database.read()),
1880 sqlx::query(
1881 "SELECT devices.source, devices.target, devices.permissions
1882 FROM server_devices
1883 JOIN devices ON devices.uuid = server_devices.device_uuid
1884 WHERE server_devices.server_uuid = $1",
1885 )
1886 .bind(self.uuid)
1887 .fetch_all(state.database.read()),
1888 sqlx::query!(
1889 "SELECT node_allocations.ip, node_allocations.port
1890 FROM server_allocations
1891 JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1892 WHERE server_allocations.server_uuid = $1",
1893 self.uuid
1894 )
1895 .fetch_all(state.database.read()),
1896 firewall::fetch_raw_rules(&state.database, self.uuid),
1897 )?;
1898
1899 let mut futures = Vec::new();
1900 futures.reserve_exact(schedules.len());
1901
1902 for schedule in &schedules {
1903 futures.push(
1904 sqlx::query!(
1905 "SELECT server_schedule_steps.uuid, server_schedule_steps.schedule_uuid, server_schedule_steps.action
1906 FROM server_schedule_steps
1907 WHERE server_schedule_steps.schedule_uuid = $1
1908 ORDER BY server_schedule_steps.order_, server_schedule_steps.created, server_schedule_steps.uuid",
1909 schedule.uuid
1910 )
1911 .fetch_all(state.database.read()),
1912 );
1913 }
1914
1915 let results = futures_util::future::try_join_all(futures).await?;
1916 let mut schedule_steps = HashMap::new();
1917 schedule_steps.reserve(schedules.len());
1918
1919 for (i, steps) in results.into_iter().enumerate() {
1920 schedule_steps.insert(schedules[i].uuid, steps);
1921 }
1922
1923 Ok(RemoteApiServer {
1924 settings: wings_api::ServerConfiguration {
1925 uuid: self.uuid,
1926 start_on_completion: None,
1927 meta: wings_api::ServerConfigurationMeta {
1928 name: self.name,
1929 description: self.description.unwrap_or_default(),
1930 },
1931 suspended: self.suspended,
1932 invocation: self.startup,
1933 entrypoint: None,
1934 skip_egg_scripts: false,
1935 environment: variables
1936 .into_iter()
1937 .map(|v| {
1938 (
1939 v.env_variable.into(),
1940 serde_json::Value::String(v.value.unwrap_or_default()),
1941 )
1942 })
1943 .collect(),
1944 labels: self.labels,
1945 backups: backups.into_iter().map(|b| b.uuid).collect(),
1946 schedules: schedules
1947 .into_iter()
1948 .map(|s| {
1949 Ok::<_, serde_json::Error>(wings_api::Schedule {
1950 uuid: s.uuid,
1951 triggers: s.triggers,
1952 condition: s.condition,
1953 actions: schedule_steps
1954 .remove(&s.uuid)
1955 .unwrap_or_default()
1956 .into_iter()
1957 .map(|step| {
1958 serde_json::to_value(wings_api::ScheduleAction {
1959 uuid: step.uuid,
1960 inner: serde_json::from_value(step.action)?,
1961 })
1962 })
1963 .try_collect_vec()?,
1964 })
1965 })
1966 .try_collect_vec()?,
1967 allocations: wings_api::ServerConfigurationAllocations {
1968 force_outgoing_ip: self.egg.force_outgoing_ip,
1969 default: self.allocation.map(|a| {
1970 wings_api::ServerConfigurationAllocationsDefault {
1971 ip: compact_str::format_compact!("{}", a.allocation.ip.ip()),
1972 port: a.allocation.port as u32,
1973 }
1974 }),
1975 mappings: {
1976 let mut mappings = IndexMap::new();
1977 for allocation in allocations {
1978 mappings
1979 .entry(compact_str::format_compact!("{}", allocation.ip.ip()))
1980 .or_insert_with(Vec::new)
1981 .push(allocation.port as u32);
1982 }
1983
1984 mappings
1985 },
1986 },
1987 build: wings_api::ServerConfigurationBuild {
1988 bandwidth: wings_api::BandwidthLimits {
1989 upload: self.bandwidth_upload as u64,
1990 download: self.bandwidth_download as u64,
1991 },
1992 memory_limit: self.memory,
1993 overhead_memory: self.memory_overhead,
1994 swap: self.swap,
1995 io_weight: self.io_weight.map(|w| w as u32),
1996 cpu_limit: self.cpu as i64,
1997 disk_space: self.disk as u64,
1998 threads: {
1999 let mut threads = compact_str::CompactString::default();
2000 for cpu in &self.pinned_cpus {
2001 if !threads.is_empty() {
2002 threads.push(',');
2003 }
2004 threads.push_str(&cpu.to_string());
2005 }
2006
2007 if threads.is_empty() {
2008 None
2009 } else {
2010 Some(threads)
2011 }
2012 },
2013 oom_disabled: false,
2014 },
2015 mounts: mounts
2016 .into_iter()
2017 .map(|m| wings_api::Mount {
2018 source: m.source.into(),
2019 target: m.target.into(),
2020 read_only: m.read_only,
2021 })
2022 .collect(),
2023 devices: devices
2024 .into_iter()
2025 .map(|d| {
2026 Ok::<_, sqlx::Error>(wings_api::Device {
2027 source: d.try_get("source")?,
2028 target: d.try_get("target")?,
2029 permissions: d.try_get("permissions")?,
2030 })
2031 })
2032 .try_collect_vec()?,
2033 firewall: if firewall_enabled {
2034 firewall::decode_rules(firewall_rules)?
2035 .into_iter()
2036 .map(Into::into)
2037 .collect()
2038 } else {
2039 Vec::new()
2040 },
2041 egg: wings_api::ServerConfigurationEgg {
2042 id: self.egg.uuid,
2043 file_denylist: self.egg.file_denylist,
2044 },
2045 container: wings_api::ServerConfigurationContainer {
2046 image: self.image,
2047 timezone: self.timezone,
2048 hugepages_passthrough_enabled: self.hugepages_passthrough_enabled,
2049 kvm_passthrough_enabled: self.kvm_passthrough_enabled,
2050 seccomp: wings_api::ServerConfigurationContainerSeccomp {
2051 remove_allowed: vec![],
2052 },
2053 },
2054 auto_kill: self.auto_kill,
2055 auto_start_behavior: self.auto_start_behavior.into(),
2056 trash: Some(wings_api::ServerConfigurationTrash {
2057 retention: trash.retention,
2058 usage_threshold: Some(trash.usage_threshold),
2059 }),
2060 features: wings_api::ServerConfigurationFeatures {
2061 startup_cpu_boost: None,
2062 runtime_cpu_boost: None,
2063 },
2064 },
2065 process_configuration: super::nest_egg::ProcessConfiguration {
2066 startup: self.egg.config_startup,
2067 stop: self.egg.config_stop,
2068 configs: self.egg.config_files,
2069 },
2070 })
2071 }
2072}
2073
2074#[async_trait::async_trait]
2075impl super::IntoAdminApiObject for Server {
2076 type AdminApiObject = AdminApiServer;
2077 type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
2078
2079 async fn into_admin_api_object<'a>(
2080 self,
2081 state: &crate::State,
2082 storage_url_retriever: Self::ExtraArgs<'a>,
2083 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
2084 let feature_limits = ApiServerFeatureLimits::init_hooks(&self, state).await?;
2085 let api_object = AdminApiServer::init_hooks(&self, state).await?;
2086
2087 let allocation_uuid = self.allocation.as_ref().map(|a| a.uuid);
2088 let allocation = match self.allocation {
2089 Some(a) => Some(a.into_api_object(state, allocation_uuid).await?),
2090 None => None,
2091 };
2092
2093 let feature_limits = finish_extendible!(
2094 ApiServerFeatureLimits {
2095 allocations: self.allocation_limit,
2096 databases: self.database_limit,
2097 backups: self.backup_limit,
2098 schedules: self.schedule_limit,
2099 },
2100 feature_limits,
2101 state
2102 )?;
2103
2104 let (node, backup_configuration, egg) = tokio::join!(
2105 async {
2106 match self.node.fetch_cached(&state.database).await {
2107 Ok(node) => Ok(node.into_admin_api_object(state, ()).await?),
2108 Err(err) => Err(err),
2109 }
2110 },
2111 async {
2112 if let Some(backup_configuration) = self.backup_configuration {
2113 if let Ok(backup_configuration) =
2114 backup_configuration.fetch_cached(&state.database).await
2115 {
2116 backup_configuration
2117 .into_admin_api_object(state, ())
2118 .await
2119 .ok()
2120 } else {
2121 None
2122 }
2123 } else {
2124 None
2125 }
2126 },
2127 self.egg.into_admin_api_object(state, ())
2128 );
2129
2130 let api_object = finish_extendible!(
2131 AdminApiServer {
2132 uuid: self.uuid,
2133 uuid_short: format!("{:08x}", self.uuid_short).into(),
2134 external_id: self.external_id,
2135 allocation,
2136 node: node?,
2137 owner: self
2138 .owner
2139 .into_admin_api_object(state, storage_url_retriever)
2140 .await?,
2141 egg: egg?,
2142 nest: self.nest.into_admin_api_object(state, ()).await?,
2143 backup_configuration,
2144 status: self.status,
2145 is_suspended: self.suspended,
2146 is_transferring: self.destination_node.is_some(),
2147 name: self.name,
2148 description: self.description,
2149 limits: AdminApiServerLimits {
2150 cpu: self.cpu,
2151 memory: self.memory,
2152 memory_overhead: self.memory_overhead,
2153 swap: self.swap,
2154 disk: self.disk,
2155 io_weight: self.io_weight,
2156 bandwidth: Some(ApiServerBandwidth {
2157 upload: self.bandwidth_upload as u64,
2158 download: self.bandwidth_download as u64,
2159 }),
2160 },
2161 pinned_cpus: self.pinned_cpus,
2162 feature_limits,
2163 startup: self.startup,
2164 image: self.image,
2165 labels: self.labels,
2166 auto_kill: self.auto_kill,
2167 auto_start_behavior: self.auto_start_behavior,
2168 timezone: self.timezone,
2169 trash: self.trash,
2170 trash_defaults: state.settings.get().await?.server.trash,
2171 hugepages_passthrough_enabled: self.hugepages_passthrough_enabled,
2172 kvm_passthrough_enabled: self.kvm_passthrough_enabled,
2173 created: self.created.and_utc(),
2174 },
2175 api_object,
2176 state
2177 )?;
2178
2179 Ok(api_object)
2180 }
2181}
2182
2183#[async_trait::async_trait]
2184impl super::IntoApiObject for Server {
2185 type ApiObject = ApiServer;
2186 type ExtraArgs<'a> = &'a super::user::User;
2187
2188 async fn into_api_object<'a>(
2189 self,
2190 state: &crate::State,
2191 user: Self::ExtraArgs<'a>,
2192 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
2193 let feature_limits = ApiServerFeatureLimits::init_hooks(&self, state).await?;
2194 let api_object = ApiServer::init_hooks(&self, state).await?;
2195 let trash = self.api_trash(&*state.settings.get().await?);
2196
2197 let allocation_uuid = self.allocation.as_ref().map(|a| a.uuid);
2198 let allocation = match self.allocation {
2199 Some(a) => Some(a.into_api_object(state, allocation_uuid).await?),
2200 None => None,
2201 };
2202
2203 let node = self.node.fetch_cached(&state.database).await?;
2204 let egg_configuration = super::egg_configuration::EggConfiguration::merged_for_target(
2205 &state.database,
2206 &super::egg_configuration::EggConfigurationTarget::new(&self.egg, &node),
2207 )
2208 .await?;
2209
2210 let feature_limits = finish_extendible!(
2211 ApiServerFeatureLimits {
2212 allocations: self.allocation_limit,
2213 databases: self.database_limit,
2214 backups: self.backup_limit,
2215 schedules: self.schedule_limit,
2216 },
2217 feature_limits,
2218 state
2219 )?;
2220
2221 let api_object = finish_extendible!(
2222 ApiServer {
2223 uuid: self.uuid,
2224 uuid_short: format!("{:08x}", self.uuid_short).into(),
2225 allocation,
2226 egg: self.egg.into_api_object(state, ()).await?,
2227 egg_configuration: egg_configuration.into_api_object(state, ()).await?,
2228 permissions: if user.admin {
2229 vec!["*".into()]
2230 } else {
2231 self.subuser_permissions
2232 .map_or_else(|| vec!["*".into()], |p| p.to_vec())
2233 },
2234 ignored_files: self.subuser_ignored_files.unwrap_or_default(),
2235 location_uuid: node.location.uuid,
2236 location_name: node.location.name,
2237 location_flag: node.location.flag,
2238 node_uuid: node.uuid,
2239 node_name: node.name,
2240 node_maintenance_enabled: node.maintenance_enabled,
2241 sftp_host: node.sftp_host.unwrap_or_else(|| {
2242 node.public_url
2243 .unwrap_or(node.url)
2244 .host_str()
2245 .unwrap_or("unknown.sftp.host")
2246 .into()
2247 }),
2248 sftp_port: node.sftp_port,
2249 status: self.status,
2250 is_suspended: self.suspended,
2251 is_owner: self.owner.uuid == user.uuid,
2252 is_transferring: self.destination_node.is_some(),
2253 name: self.name,
2254 description: self.description,
2255 limits: ApiServerLimits {
2256 cpu: self.cpu,
2257 memory: self.memory,
2258 swap: self.swap,
2259 disk: self.disk,
2260 bandwidth: ApiServerBandwidth {
2261 upload: self.bandwidth_upload as u64,
2262 download: self.bandwidth_download as u64,
2263 },
2264 },
2265 feature_limits,
2266 startup: self.startup,
2267 image: self.image,
2268 auto_kill: self.auto_kill,
2269 auto_start_behavior: self.auto_start_behavior,
2270 timezone: self.timezone,
2271 trash,
2272 created: self.created.and_utc(),
2273 },
2274 api_object,
2275 state
2276 )?;
2277
2278 Ok(api_object)
2279 }
2280}
2281
2282#[async_trait::async_trait]
2283impl ByUuid for Server {
2284 async fn by_uuid(
2285 database: &crate::database::Database,
2286 uuid: uuid::Uuid,
2287 ) -> Result<Self, crate::database::DatabaseError> {
2288 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
2289 r#"
2290 SELECT {}
2291 FROM servers
2292 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
2293 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
2294 JOIN users ON users.uuid = servers.owner_uuid
2295 LEFT JOIN roles ON roles.uuid = users.role_uuid
2296 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
2297 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
2298 WHERE servers.uuid = $1
2299 "#,
2300 Self::columns_sql(None)
2301 )))
2302 .bind(uuid)
2303 .fetch_one(database.read())
2304 .await?;
2305
2306 Self::map(None, &row)
2307 }
2308
2309 async fn by_uuid_with_transaction(
2310 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2311 uuid: uuid::Uuid,
2312 ) -> Result<Self, crate::database::DatabaseError> {
2313 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
2314 r#"
2315 SELECT {}
2316 FROM servers
2317 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
2318 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
2319 JOIN users ON users.uuid = servers.owner_uuid
2320 LEFT JOIN roles ON roles.uuid = users.role_uuid
2321 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
2322 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
2323 WHERE servers.uuid = $1
2324 "#,
2325 Self::columns_sql(None)
2326 )))
2327 .bind(uuid)
2328 .fetch_one(&mut **transaction)
2329 .await?;
2330
2331 Self::map(None, &row)
2332 }
2333}
2334
2335#[derive(ToSchema, Validate, Deserialize)]
2336pub struct CreateServerOptions {
2337 #[garde(skip)]
2338 pub node_uuid: uuid::Uuid,
2339 #[garde(skip)]
2340 pub owner_uuid: uuid::Uuid,
2341 #[garde(skip)]
2342 pub egg_uuid: uuid::Uuid,
2343 #[garde(skip)]
2344 pub backup_configuration_uuid: Option<uuid::Uuid>,
2345
2346 #[garde(skip)]
2347 pub allocation_uuid: Option<uuid::Uuid>,
2348 #[garde(skip)]
2349 pub allocation_uuids: Vec<uuid::Uuid>,
2350
2351 #[garde(skip)]
2352 pub start_on_completion: bool,
2353 #[garde(skip)]
2354 pub skip_installer: bool,
2355
2356 #[garde(length(chars, min = 1, max = 255))]
2357 #[schema(min_length = 1, max_length = 255)]
2358 pub external_id: Option<compact_str::CompactString>,
2359 #[garde(length(chars, min = 1, max = 255))]
2360 #[schema(min_length = 1, max_length = 255)]
2361 pub name: compact_str::CompactString,
2362 #[garde(length(chars, min = 1, max = 1024))]
2363 #[schema(min_length = 1, max_length = 1024)]
2364 pub description: Option<compact_str::CompactString>,
2365
2366 #[garde(dive)]
2367 pub limits: AdminApiServerLimits,
2368 #[garde(inner(range(min = 0)))]
2369 pub pinned_cpus: Vec<i16>,
2370
2371 #[garde(length(chars, min = 1, max = 8192))]
2372 #[schema(min_length = 1, max_length = 8192)]
2373 pub startup: compact_str::CompactString,
2374 #[garde(length(chars, min = 2, max = 255))]
2375 #[schema(min_length = 2, max_length = 255)]
2376 pub image: compact_str::CompactString,
2377 #[garde(custom(validate_labels))]
2378 #[serde(default)]
2379 pub labels: IndexMap<compact_str::CompactString, compact_str::CompactString>,
2380 #[garde(skip)]
2381 #[schema(value_type = Option<String>)]
2382 pub timezone: Option<chrono_tz::Tz>,
2383 #[garde(dive)]
2384 #[schema(inline)]
2385 pub trash: Option<TrashSettings>,
2386
2387 #[garde(skip)]
2388 pub hugepages_passthrough_enabled: bool,
2389 #[garde(skip)]
2390 pub kvm_passthrough_enabled: bool,
2391
2392 #[garde(dive)]
2393 pub feature_limits: ApiServerFeatureLimits,
2394 #[garde(skip)]
2395 pub variables: HashMap<uuid::Uuid, compact_str::CompactString>,
2396}
2397
2398#[async_trait::async_trait]
2399impl CreatableModel for Server {
2400 type CreateOptions<'a> = CreateServerOptions;
2401 type CreateResult = Self;
2402
2403 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
2404 static CREATE_LISTENERS: LazyLock<CreateListenerList<Server>> =
2405 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
2406
2407 &CREATE_LISTENERS
2408 }
2409
2410 async fn create_with_transaction(
2411 _state: &crate::State,
2412 _options: Self::CreateOptions<'_>,
2413 _transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2414 ) -> Result<Self, crate::database::DatabaseError> {
2415 Err(anyhow::anyhow!("create_with_transaction is not supported for Server").into())
2416 }
2417
2418 async fn create(
2419 state: &crate::State,
2420 mut options: Self::CreateOptions<'_>,
2421 ) -> Result<Self, crate::database::DatabaseError> {
2422 options.validate()?;
2423
2424 let node = super::node::Node::by_uuid_optional(&state.database, options.node_uuid)
2425 .await?
2426 .ok_or(crate::database::InvalidRelationError("node"))?;
2427
2428 if options
2429 .limits
2430 .bandwidth
2431 .is_some_and(|limits| limits.upload != 0 || limits.download != 0)
2432 {
2433 node.ensure_bandwidth_ready(&state.database).await?;
2434 }
2435
2436 super::user::User::by_uuid_optional(&state.database, options.owner_uuid)
2437 .await?
2438 .ok_or(crate::database::InvalidRelationError("owner"))?;
2439
2440 super::nest_egg::NestEgg::by_uuid_optional(&state.database, options.egg_uuid)
2441 .await?
2442 .ok_or(crate::database::InvalidRelationError("egg"))?;
2443
2444 if let Some(backup_configuration_uuid) = options.backup_configuration_uuid {
2445 super::backup_configuration::BackupConfiguration::by_uuid_optional(
2446 &state.database,
2447 backup_configuration_uuid,
2448 )
2449 .await?
2450 .ok_or(crate::database::InvalidRelationError(
2451 "backup_configuration",
2452 ))?;
2453 }
2454
2455 let mut transaction = state.database.write().begin().await?;
2456 let mut attempts = 0;
2457
2458 loop {
2459 let server_uuid = uuid::Uuid::new_v4();
2460 let uuid_short = server_uuid.as_fields().0 as i32;
2461
2462 let mut query_builder = InsertQueryBuilder::new("servers");
2463
2464 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
2465 .await?;
2466
2467 query_builder
2468 .set("uuid", server_uuid)
2469 .set("uuid_short", uuid_short)
2470 .set("external_id", &options.external_id)
2471 .set("node_uuid", options.node_uuid)
2472 .set("owner_uuid", options.owner_uuid)
2473 .set("egg_uuid", options.egg_uuid)
2474 .set(
2475 "backup_configuration_uuid",
2476 options.backup_configuration_uuid,
2477 )
2478 .set("name", &options.name)
2479 .set("description", &options.description)
2480 .set(
2481 "status",
2482 if options.skip_installer {
2483 None::<ServerStatus>
2484 } else {
2485 Some(ServerStatus::Installing)
2486 },
2487 )
2488 .set("memory", options.limits.memory)
2489 .set("memory_overhead", options.limits.memory_overhead)
2490 .set(
2491 "bandwidth_upload",
2492 options.limits.bandwidth.unwrap_or_default().upload as i64,
2493 )
2494 .set(
2495 "bandwidth_download",
2496 options.limits.bandwidth.unwrap_or_default().download as i64,
2497 )
2498 .set("swap", options.limits.swap)
2499 .set("disk", options.limits.disk)
2500 .set("io_weight", options.limits.io_weight)
2501 .set("cpu", options.limits.cpu)
2502 .set("pinned_cpus", &options.pinned_cpus)
2503 .set("startup", &options.startup)
2504 .set("image", &options.image)
2505 .set("labels", OrderedJson(&options.labels))
2506 .set("timezone", options.timezone.as_ref().map(|t| t.name()))
2507 .set(
2508 "trash",
2509 options
2510 .trash
2511 .as_ref()
2512 .map(serde_json::to_value)
2513 .transpose()?,
2514 )
2515 .set(
2516 "hugepages_passthrough_enabled",
2517 options.hugepages_passthrough_enabled,
2518 )
2519 .set("kvm_passthrough_enabled", options.kvm_passthrough_enabled)
2520 .set("allocation_limit", options.feature_limits.allocations)
2521 .set("database_limit", options.feature_limits.databases)
2522 .set("backup_limit", options.feature_limits.backups)
2523 .set("schedule_limit", options.feature_limits.schedules);
2524
2525 match query_builder
2526 .returning("uuid")
2527 .fetch_one(&mut *transaction)
2528 .await
2529 {
2530 Ok(_) => {
2531 let allocation_uuid: Option<uuid::Uuid> =
2532 if let Some(allocation_uuid) = options.allocation_uuid {
2533 let row = sqlx::query(
2534 r#"
2535 INSERT INTO server_allocations (server_uuid, allocation_uuid)
2536 VALUES ($1, $2)
2537 RETURNING uuid
2538 "#,
2539 )
2540 .bind(server_uuid)
2541 .bind(allocation_uuid)
2542 .fetch_one(&mut *transaction)
2543 .await?;
2544
2545 Some(row.get("uuid"))
2546 } else {
2547 None
2548 };
2549
2550 for allocation_uuid in &options.allocation_uuids {
2551 sqlx::query(
2552 r#"
2553 INSERT INTO server_allocations (server_uuid, allocation_uuid)
2554 VALUES ($1, $2)
2555 "#,
2556 )
2557 .bind(server_uuid)
2558 .bind(allocation_uuid)
2559 .execute(&mut *transaction)
2560 .await?;
2561 }
2562
2563 sqlx::query(
2564 r#"
2565 UPDATE servers
2566 SET allocation_uuid = $1
2567 WHERE servers.uuid = $2
2568 "#,
2569 )
2570 .bind(allocation_uuid)
2571 .bind(server_uuid)
2572 .execute(&mut *transaction)
2573 .await?;
2574
2575 for (variable_uuid, value) in &options.variables {
2576 sqlx::query(
2577 r#"
2578 INSERT INTO server_variables (server_uuid, variable_uuid, value)
2579 VALUES ($1, $2, $3)
2580 "#,
2581 )
2582 .bind(server_uuid)
2583 .bind(variable_uuid)
2584 .bind(value.as_str())
2585 .execute(&mut *transaction)
2586 .await?;
2587 }
2588
2589 let mut result =
2590 Self::by_uuid_with_transaction(&mut transaction, server_uuid).await?;
2591
2592 Self::run_after_create_handlers(&mut result, &options, state, &mut transaction)
2593 .await?;
2594
2595 transaction.commit().await?;
2596
2597 if let Err(err) = node
2598 .api_client(&state.database)
2599 .await?
2600 .post_servers(&wings_api::servers::post::RequestBody {
2601 uuid: server_uuid,
2602 start_on_completion: options.start_on_completion,
2603 skip_scripts: options.skip_installer,
2604 })
2605 .await
2606 {
2607 tracing::error!(server = %server_uuid, node = %node.uuid, "failed to create server: {:?}", err);
2608
2609 sqlx::query!("DELETE FROM servers WHERE servers.uuid = $1", server_uuid)
2610 .execute(state.database.write())
2611 .await?;
2612
2613 return Err(err.into());
2614 }
2615
2616 return Ok(result);
2617 }
2618 Err(_) if attempts < 3 => {
2619 attempts += 1;
2620 transaction.rollback().await?;
2621 transaction = state.database.write().begin().await?;
2622
2623 continue;
2624 }
2625 Err(err) => {
2626 transaction.rollback().await?;
2627 return Err(err.into());
2628 }
2629 }
2630 }
2631 }
2632}
2633
2634fn validate_auto_kill(
2635 value: &Option<wings_api::ServerConfigurationAutoKill>,
2636 _context: &(),
2637) -> garde::Result {
2638 match value {
2639 Some(auto_kill) if !(1..=3600).contains(&auto_kill.seconds) => Err(garde::Error::new(
2640 "auto kill seconds must be between 1 and 3600",
2641 )),
2642 _ => Ok(()),
2643 }
2644}
2645
2646const RESERVED_LABELS: [&str; 2] = ["Service", "ContainerType"];
2649const MAX_LABELS: usize = 64;
2650
2651pub fn validate_labels(
2652 labels: &IndexMap<compact_str::CompactString, compact_str::CompactString>,
2653 _context: &(),
2654) -> garde::Result {
2655 if labels.len() > MAX_LABELS {
2656 return Err(garde::Error::new(compact_str::format_compact!(
2657 "a server cannot have more than {MAX_LABELS} labels"
2658 )));
2659 }
2660
2661 for (key, value) in labels {
2662 if key.trim().is_empty() {
2663 return Err(garde::Error::new("label keys cannot be empty"));
2664 }
2665
2666 if RESERVED_LABELS.contains(&key.as_str()) {
2667 return Err(garde::Error::new(compact_str::format_compact!(
2668 "label {key} is reserved by wings"
2669 )));
2670 }
2671
2672 if key.chars().count() > 255 || value.chars().count() > 255 {
2673 return Err(garde::Error::new(compact_str::format_compact!(
2674 "label {key} and its value must each be at most 255 characters"
2675 )));
2676 }
2677
2678 if key.chars().chain(value.chars()).any(char::is_control) {
2679 return Err(garde::Error::new(compact_str::format_compact!(
2680 "label {key} cannot contain control characters"
2681 )));
2682 }
2683 }
2684
2685 Ok(())
2686}
2687
2688#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
2689pub struct UpdateServerOptions {
2690 #[garde(skip)]
2691 pub owner_uuid: Option<uuid::Uuid>,
2692 #[garde(skip)]
2693 pub egg_uuid: Option<uuid::Uuid>,
2694 #[garde(skip)]
2695 #[serde(
2696 default,
2697 skip_serializing_if = "Option::is_none",
2698 with = "::serde_with::rust::double_option"
2699 )]
2700 pub backup_configuration_uuid: Option<Option<uuid::Uuid>>,
2701
2702 #[garde(skip)]
2703 pub suspended: Option<bool>,
2704
2705 #[garde(length(chars, min = 1, max = 255))]
2706 #[schema(min_length = 1, max_length = 255)]
2707 #[serde(
2708 default,
2709 skip_serializing_if = "Option::is_none",
2710 with = "::serde_with::rust::double_option"
2711 )]
2712 pub external_id: Option<Option<compact_str::CompactString>>,
2713 #[garde(length(chars, min = 1, max = 255))]
2714 #[schema(min_length = 1, max_length = 255)]
2715 pub name: Option<compact_str::CompactString>,
2716 #[garde(length(chars, min = 1, max = 1024))]
2717 #[schema(min_length = 1, max_length = 1024)]
2718 #[serde(
2719 default,
2720 skip_serializing_if = "Option::is_none",
2721 with = "::serde_with::rust::double_option"
2722 )]
2723 pub description: Option<Option<compact_str::CompactString>>,
2724
2725 #[garde(dive)]
2726 pub limits: Option<AdminApiServerLimits>,
2727 #[garde(inner(inner(range(min = 0))))]
2728 pub pinned_cpus: Option<Vec<i16>>,
2729
2730 #[garde(length(chars, min = 1, max = 8192))]
2731 #[schema(min_length = 1, max_length = 8192)]
2732 pub startup: Option<compact_str::CompactString>,
2733 #[garde(length(chars, min = 2, max = 255))]
2734 #[schema(min_length = 2, max_length = 255)]
2735 pub image: Option<compact_str::CompactString>,
2736 #[garde(inner(custom(validate_labels)))]
2737 pub labels: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
2738 #[garde(custom(validate_auto_kill))]
2739 #[schema(inline)]
2740 pub auto_kill: Option<wings_api::ServerConfigurationAutoKill>,
2741 #[garde(skip)]
2742 pub auto_start_behavior: Option<ServerAutoStartBehavior>,
2743 #[garde(skip)]
2744 #[schema(value_type = Option<Option<String>>)]
2745 #[serde(
2746 default,
2747 skip_serializing_if = "Option::is_none",
2748 with = "::serde_with::rust::double_option"
2749 )]
2750 pub timezone: Option<Option<chrono_tz::Tz>>,
2751 #[garde(dive)]
2752 #[schema(inline)]
2753 #[serde(
2754 default,
2755 skip_serializing_if = "Option::is_none",
2756 with = "::serde_with::rust::double_option"
2757 )]
2758 pub trash: Option<Option<TrashSettings>>,
2759
2760 #[garde(skip)]
2761 pub hugepages_passthrough_enabled: Option<bool>,
2762 #[garde(skip)]
2763 pub kvm_passthrough_enabled: Option<bool>,
2764
2765 #[garde(dive)]
2766 pub feature_limits: Option<ApiServerFeatureLimits>,
2767}
2768
2769#[async_trait::async_trait]
2770impl UpdatableModel for Server {
2771 type UpdateOptions = UpdateServerOptions;
2772
2773 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
2774 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Server>> =
2775 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
2776
2777 &UPDATE_LISTENERS
2778 }
2779
2780 async fn update(
2781 &mut self,
2782 state: &crate::State,
2783 options: Self::UpdateOptions,
2784 ) -> Result<(), crate::database::DatabaseError> {
2785 let previous_owner = options
2786 .owner_uuid
2787 .is_some_and(|owner_uuid| owner_uuid != self.owner.uuid)
2788 .then_some(self.owner.uuid);
2789
2790 let mut transaction = state.database.write().begin().await?;
2791
2792 if let Err(err) = self
2793 .update_with_transaction(state, options, &mut transaction)
2794 .await
2795 {
2796 transaction.rollback().await?;
2797 return Err(err);
2798 }
2799
2800 transaction.commit().await?;
2801
2802 super::invalidate_cache_keys(&state.database, &self.cache_invalidation_keys()).await;
2803
2804 if let Some(previous_owner) = previous_owner {
2805 match self.node.fetch_cached(&state.database).await {
2806 Ok(node) => node.invalidate_permissions(
2807 &state.database,
2808 super::node::PermissionsInvalidation {
2809 users: vec![previous_owner, self.owner.uuid],
2810 ..Default::default()
2811 },
2812 ),
2813 Err(err) => {
2814 tracing::error!(server = %self.uuid, "failed to invalidate permissions in wings: {:?}", err);
2815 }
2816 }
2817 }
2818
2819 Ok(())
2820 }
2821
2822 async fn update_with_transaction(
2823 &mut self,
2824 state: &crate::State,
2825 mut options: Self::UpdateOptions,
2826 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2827 ) -> Result<(), crate::database::DatabaseError> {
2828 options.validate()?;
2829
2830 let owner = if let Some(owner_uuid) = options.owner_uuid {
2831 Some(
2832 super::user::User::by_uuid_optional(&state.database, owner_uuid)
2833 .await?
2834 .ok_or(crate::database::InvalidRelationError("owner"))?,
2835 )
2836 } else {
2837 None
2838 };
2839
2840 let egg = if let Some(egg_uuid) = options.egg_uuid {
2841 Some(
2842 super::nest_egg::NestEgg::by_uuid_optional(&state.database, egg_uuid)
2843 .await?
2844 .ok_or(crate::database::InvalidRelationError("egg"))?,
2845 )
2846 } else {
2847 None
2848 };
2849
2850 let backup_configuration =
2851 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
2852 match backup_configuration_uuid {
2853 Some(uuid) => {
2854 super::backup_configuration::BackupConfiguration::by_uuid_optional(
2855 &state.database,
2856 *uuid,
2857 )
2858 .await?
2859 .ok_or(crate::database::InvalidRelationError(
2860 "backup_configuration",
2861 ))?;
2862
2863 Some(Some(
2864 super::backup_configuration::BackupConfiguration::get_fetchable(*uuid),
2865 ))
2866 }
2867 None => Some(None),
2868 }
2869 } else {
2870 None
2871 };
2872
2873 let mut query_builder = UpdateQueryBuilder::new("servers");
2874
2875 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
2876 .await?;
2877
2878 if options
2879 .limits
2880 .and_then(|limits| limits.bandwidth)
2881 .is_some_and(|limits| {
2882 (limits.upload != 0 || limits.download != 0)
2883 && (limits.upload as i64 != self.bandwidth_upload
2884 || limits.download as i64 != self.bandwidth_download)
2885 })
2886 {
2887 self.node
2888 .fetch_cached(&state.database)
2889 .await?
2890 .ensure_bandwidth_ready(&state.database)
2891 .await?;
2892 }
2893
2894 query_builder
2895 .set("owner_uuid", options.owner_uuid.as_ref())
2896 .set("egg_uuid", options.egg_uuid.as_ref())
2897 .set(
2898 "backup_configuration_uuid",
2899 options
2900 .backup_configuration_uuid
2901 .as_ref()
2902 .map(|u| u.as_ref()),
2903 )
2904 .set("suspended", options.suspended)
2905 .set(
2906 "external_id",
2907 options.external_id.as_ref().map(|e| e.as_ref()),
2908 )
2909 .set("name", options.name.as_ref())
2910 .set(
2911 "description",
2912 options.description.as_ref().map(|d| d.as_ref()),
2913 )
2914 .set("pinned_cpus", options.pinned_cpus.as_ref())
2915 .set("startup", options.startup.as_ref())
2916 .set("image", options.image.as_ref())
2917 .set("labels", options.labels.as_ref().map(OrderedJson))
2918 .set(
2919 "auto_kill",
2920 options
2921 .auto_kill
2922 .as_ref()
2923 .map(serde_json::to_value)
2924 .transpose()?,
2925 )
2926 .set("auto_start_behavior", options.auto_start_behavior)
2927 .set(
2928 "timezone",
2929 options
2930 .timezone
2931 .as_ref()
2932 .map(|t| t.as_ref().map(|t| t.name())),
2933 )
2934 .set(
2935 "trash",
2936 options
2937 .trash
2938 .as_ref()
2939 .map(|t| t.as_ref().map(serde_json::to_value).transpose())
2940 .transpose()?,
2941 )
2942 .set(
2943 "hugepages_passthrough_enabled",
2944 options.hugepages_passthrough_enabled,
2945 )
2946 .set("kvm_passthrough_enabled", options.kvm_passthrough_enabled);
2947
2948 if let Some(limits) = &options.limits {
2949 query_builder
2950 .set("cpu", Some(limits.cpu))
2951 .set("memory", Some(limits.memory))
2952 .set("memory_overhead", Some(limits.memory_overhead))
2953 .set("swap", Some(limits.swap))
2954 .set("disk", Some(limits.disk))
2955 .set("io_weight", Some(limits.io_weight));
2956
2957 if let Some(bandwidth) = limits.bandwidth {
2958 query_builder
2959 .set("bandwidth_upload", Some(bandwidth.upload as i64))
2960 .set("bandwidth_download", Some(bandwidth.download as i64));
2961 }
2962 }
2963
2964 if let Some(feature_limits) = &options.feature_limits {
2965 query_builder
2966 .set("allocation_limit", Some(feature_limits.allocations))
2967 .set("database_limit", Some(feature_limits.databases))
2968 .set("backup_limit", Some(feature_limits.backups))
2969 .set("schedule_limit", Some(feature_limits.schedules));
2970 }
2971
2972 query_builder.where_eq("uuid", self.uuid);
2973
2974 query_builder.execute(&mut **transaction).await?;
2975
2976 if egg.as_ref().is_some_and(|egg| egg.uuid != self.egg.uuid) {
2977 crate::tunnel::bump_epoch_if_server_on_mesh(transaction, self.uuid).await?;
2978 }
2979
2980 if let Some(owner) = owner {
2981 self.owner = owner;
2982 }
2983 if let Some(egg) = egg {
2984 *self.egg = egg;
2985 }
2986 if let Some(backup_configuration) = backup_configuration {
2987 self.backup_configuration = backup_configuration;
2988 }
2989 if let Some(suspended) = options.suspended {
2990 self.suspended = suspended;
2991 }
2992 if let Some(external_id) = options.external_id {
2993 self.external_id = external_id;
2994 }
2995 if let Some(name) = options.name {
2996 self.name = name;
2997 }
2998 if let Some(description) = options.description {
2999 self.description = description;
3000 }
3001 if let Some(limits) = options.limits {
3002 self.cpu = limits.cpu;
3003 self.memory = limits.memory;
3004 self.memory_overhead = limits.memory_overhead;
3005 self.swap = limits.swap;
3006 self.disk = limits.disk;
3007 self.io_weight = limits.io_weight;
3008 if let Some(bandwidth) = limits.bandwidth {
3009 self.bandwidth_upload = bandwidth.upload as i64;
3010 self.bandwidth_download = bandwidth.download as i64;
3011 }
3012 }
3013 if let Some(pinned_cpus) = options.pinned_cpus {
3014 self.pinned_cpus = pinned_cpus;
3015 }
3016 if let Some(startup) = options.startup {
3017 self.startup = startup;
3018 }
3019 if let Some(image) = options.image {
3020 self.image = image;
3021 }
3022 if let Some(labels) = options.labels {
3023 self.labels = labels;
3024 }
3025 if let Some(auto_kill) = options.auto_kill {
3026 self.auto_kill = auto_kill;
3027 }
3028 if let Some(auto_start_behavior) = options.auto_start_behavior {
3029 self.auto_start_behavior = auto_start_behavior;
3030 }
3031 if let Some(timezone) = options.timezone {
3032 self.timezone = timezone.map(|t| t.name().into());
3033 }
3034 if let Some(trash) = options.trash {
3035 self.trash = trash;
3036 }
3037 if let Some(hugepages_passthrough_enabled) = options.hugepages_passthrough_enabled {
3038 self.hugepages_passthrough_enabled = hugepages_passthrough_enabled;
3039 }
3040 if let Some(kvm_passthrough_enabled) = options.kvm_passthrough_enabled {
3041 self.kvm_passthrough_enabled = kvm_passthrough_enabled;
3042 }
3043 if let Some(feature_limits) = options.feature_limits {
3044 self.allocation_limit = feature_limits.allocations;
3045 self.database_limit = feature_limits.databases;
3046 self.backup_limit = feature_limits.backups;
3047 self.schedule_limit = feature_limits.schedules;
3048 }
3049
3050 self.run_after_update_handlers(state, transaction).await?;
3051
3052 Ok(())
3053 }
3054}
3055
3056#[derive(Clone, Default)]
3057pub struct DeleteServerOptions {
3058 pub force: bool,
3059}
3060
3061#[async_trait::async_trait]
3062impl DeletableModel for Server {
3063 type DeleteOptions = DeleteServerOptions;
3064
3065 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
3066 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Server>> =
3067 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
3068
3069 &DELETE_LISTENERS
3070 }
3071
3072 async fn delete_with_transaction(
3073 &self,
3074 _state: &crate::State,
3075 _options: Self::DeleteOptions,
3076 _transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
3077 ) -> Result<(), anyhow::Error> {
3078 Err(anyhow::anyhow!(
3079 "delete_with_transaction is not supported for Server"
3080 ))
3081 }
3082
3083 async fn delete(
3084 &self,
3085 state: &crate::State,
3086 options: Self::DeleteOptions,
3087 ) -> Result<(), anyhow::Error> {
3088 let node = self.node.fetch_cached(&state.database).await?;
3089 let databases =
3090 super::server_database::ServerDatabase::all_by_server_uuid(&state.database, self.uuid)
3091 .await?;
3092 let database_instances =
3093 super::server_database_instance::ServerDatabaseInstance::all_by_server_uuid(
3094 &state.database,
3095 self.uuid,
3096 )
3097 .await?;
3098
3099 let mut transaction = state.database.write().begin().await?;
3100 self.run_delete_handlers(&options, state, &mut transaction)
3101 .await?;
3102
3103 let state = state.clone();
3104 let server_uuid = self.uuid;
3105
3106 tokio::spawn(async move {
3107 for db in databases {
3108 match db.delete(&state, super::server_database::DeleteServerDatabaseOptions { force: options.force }).await {
3109 Ok(_) => {}
3110 Err(err) => {
3111 tracing::error!(server = %server_uuid, "failed to delete database: {:?}", err);
3112
3113 if !options.force {
3114 return Err(err);
3115 }
3116 }
3117 }
3118 }
3119
3120 for database_instance in database_instances {
3121 match database_instance.delete(&state, super::server_database_instance::DeleteServerDatabaseInstanceOptions { force: options.force }).await {
3122 Ok(_) => {}
3123 Err(err) => {
3124 tracing::error!(server = %server_uuid, "failed to delete database instance: {:?}", err);
3125
3126 if !options.force {
3127 return Err(err);
3128 }
3129 }
3130 }
3131 }
3132
3133 let on_mesh =
3134 crate::tunnel::bump_epoch_if_server_on_mesh(&mut transaction, server_uuid).await?;
3135
3136 sqlx::query!("DELETE FROM servers WHERE servers.uuid = $1", server_uuid)
3137 .execute(&mut *transaction)
3138 .await?;
3139
3140 match node
3141 .api_client(&state.database)
3142 .await?
3143 .delete_servers_server(server_uuid)
3144 .await
3145 {
3146 Ok(_) => {
3147 transaction.commit().await?;
3148
3149 Self::invalidate_cached(&state.database, server_uuid).await;
3150
3151 if on_mesh {
3152 crate::tunnel::poke_nodes(&state.database).await;
3153 }
3154
3155 Ok(())
3156 }
3157 Err(err) => {
3158 tracing::error!(server = %server_uuid, node = %node.uuid, "failed to delete server: {:?}", err);
3159
3160 if options.force {
3161 transaction.commit().await?;
3162
3163 Self::invalidate_cached(&state.database, server_uuid).await;
3164
3165 if on_mesh {
3166 crate::tunnel::poke_nodes(&state.database).await;
3167 }
3168
3169 Ok(())
3170 } else {
3171 transaction.rollback().await?;
3172 Err(err.into())
3173 }
3174 }
3175 }
3176 }).await?
3177 }
3178}
3179
3180#[derive(ToSchema, Serialize)]
3181#[schema(title = "RemoteServer")]
3182pub struct RemoteApiServer {
3183 settings: wings_api::ServerConfiguration,
3184 process_configuration: super::nest_egg::ProcessConfiguration,
3185}
3186
3187#[derive(ToSchema, Serialize, Deserialize, Clone, Copy, Default)]
3188pub struct ApiServerBandwidth {
3189 pub upload: u64,
3190 pub download: u64,
3191}
3192
3193fn validate_bandwidth(value: &Option<ApiServerBandwidth>, _context: &()) -> garde::Result {
3194 if let Some(limits) = value {
3195 for rate in [limits.upload, limits.download] {
3196 if rate != 0 && !(8..=100_000_000_000).contains(&rate) {
3197 return Err(garde::Error::new(
3198 "bandwidth must be zero or between 8 and 100000000000 bits/s",
3199 ));
3200 }
3201 }
3202 }
3203
3204 Ok(())
3205}
3206
3207#[derive(ToSchema, Validate, Serialize, Deserialize, Clone, Copy)]
3208pub struct AdminApiServerLimits {
3209 #[garde(range(min = 0))]
3210 #[schema(minimum = 0)]
3211 pub cpu: i32,
3212 #[garde(range(min = 0))]
3213 #[schema(minimum = 0)]
3214 pub memory: i64,
3215 #[garde(range(min = 0))]
3216 #[schema(minimum = 0)]
3217 pub memory_overhead: i64,
3218 #[garde(range(min = -1))]
3219 #[schema(minimum = -1)]
3220 pub swap: i64,
3221 #[garde(range(min = 0))]
3222 #[schema(minimum = 0)]
3223 pub disk: i64,
3224 #[garde(range(min = 0, max = 1000))]
3225 #[schema(minimum = 0, maximum = 1000)]
3226 pub io_weight: Option<i16>,
3227 #[serde(default, skip_serializing_if = "Option::is_none")]
3228 #[garde(custom(validate_bandwidth))]
3229 pub bandwidth: Option<ApiServerBandwidth>,
3230}
3231
3232#[derive(ToSchema, Validate, Serialize, Deserialize, Clone, Copy)]
3235pub struct TrashSettings {
3236 #[garde(range(min = 0, max = 365 * 24 * 60 * 60))]
3237 #[schema(minimum = 0, maximum = 31536000)]
3238 pub retention: u64,
3239 #[garde(range(min = 0, max = 100))]
3240 #[schema(minimum = 0, maximum = 100)]
3241 pub usage_threshold: u64,
3242}
3243
3244pub struct TrashPolicy {
3245 pub retention: Option<u64>,
3246 pub usage_threshold: Option<u64>,
3247}
3248
3249impl From<TrashSettings> for TrashPolicy {
3250 fn from(settings: TrashSettings) -> Self {
3251 Self {
3252 retention: (settings.retention > 0).then_some(settings.retention),
3253 usage_threshold: (settings.usage_threshold > 0).then_some(settings.usage_threshold),
3254 }
3255 }
3256}
3257
3258#[derive(ToSchema, Serialize, Clone, Copy)]
3259pub struct ApiServerTrash {
3260 pub retention: Option<u64>,
3261 pub usage_threshold: Option<u64>,
3262 pub custom: bool,
3263 #[schema(inline)]
3264 pub defaults: TrashSettings,
3265}
3266
3267#[derive(ToSchema, Validate, Serialize, Deserialize, Clone, Copy)]
3268pub struct ApiServerLimits {
3269 #[garde(range(min = 0))]
3270 #[schema(minimum = 0)]
3271 pub cpu: i32,
3272 #[garde(range(min = 0))]
3273 #[schema(minimum = 0)]
3274 pub memory: i64,
3275 #[garde(range(min = -1))]
3276 #[schema(minimum = -1)]
3277 pub swap: i64,
3278 #[garde(range(min = 0))]
3279 #[schema(minimum = 0)]
3280 pub disk: i64,
3281 #[garde(skip)]
3282 #[serde(default)]
3283 pub bandwidth: ApiServerBandwidth,
3284}
3285
3286#[schema_extension_derive::extendible]
3287#[init_args(Server, crate::State)]
3288#[hook_args(crate::State)]
3289#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
3290pub struct ApiServerFeatureLimits {
3291 #[garde(range(min = 0))]
3292 #[schema(minimum = 0)]
3293 pub allocations: i32,
3294 #[garde(range(min = 0))]
3295 #[schema(minimum = 0)]
3296 pub databases: i32,
3297 #[garde(range(min = 0))]
3298 #[schema(minimum = 0)]
3299 pub backups: i32,
3300 #[garde(range(min = 0))]
3301 #[schema(minimum = 0)]
3302 pub schedules: i32,
3303}
3304
3305#[schema_extension_derive::extendible]
3306#[init_args(Server, crate::State)]
3307#[hook_args(crate::State)]
3308#[derive(ToSchema, Serialize)]
3309#[schema(title = "AdminServer")]
3310pub struct AdminApiServer {
3311 pub uuid: uuid::Uuid,
3312 pub uuid_short: compact_str::CompactString,
3313 pub external_id: Option<compact_str::CompactString>,
3314 pub allocation: Option<super::server_allocation::ApiServerAllocation>,
3315 pub node: super::node::AdminApiNode,
3316 pub owner: super::user::AdminApiUser,
3317 pub egg: super::nest_egg::AdminApiNestEgg,
3318 pub nest: super::nest::AdminApiNest,
3319 pub backup_configuration: Option<super::backup_configuration::AdminApiBackupConfiguration>,
3320
3321 pub status: Option<ServerStatus>,
3322
3323 pub is_suspended: bool,
3324 pub is_transferring: bool,
3325
3326 pub name: compact_str::CompactString,
3327 pub description: Option<compact_str::CompactString>,
3328
3329 #[schema(inline)]
3330 pub limits: AdminApiServerLimits,
3331 pub pinned_cpus: Vec<i16>,
3332 #[schema(inline)]
3333 pub feature_limits: ApiServerFeatureLimits,
3334
3335 pub startup: compact_str::CompactString,
3336 pub image: compact_str::CompactString,
3337 pub labels: IndexMap<compact_str::CompactString, compact_str::CompactString>,
3338 #[schema(inline)]
3339 pub auto_kill: wings_api::ServerConfigurationAutoKill,
3340 pub auto_start_behavior: ServerAutoStartBehavior,
3341 pub timezone: Option<compact_str::CompactString>,
3342 #[schema(inline)]
3343 pub trash: Option<TrashSettings>,
3344 #[schema(inline)]
3345 pub trash_defaults: TrashSettings,
3346
3347 pub hugepages_passthrough_enabled: bool,
3348 pub kvm_passthrough_enabled: bool,
3349
3350 pub created: chrono::DateTime<chrono::Utc>,
3351}
3352
3353#[schema_extension_derive::extendible]
3354#[init_args(Server, crate::State)]
3355#[hook_args(crate::State)]
3356#[derive(ToSchema, Serialize)]
3357#[schema(title = "Server")]
3358pub struct ApiServer {
3359 pub uuid: uuid::Uuid,
3360 pub uuid_short: compact_str::CompactString,
3361 pub allocation: Option<super::server_allocation::ApiServerAllocation>,
3362 pub egg: super::nest_egg::ApiNestEgg,
3363 pub egg_configuration: super::egg_configuration::ApiEggConfiguration,
3364
3365 pub status: Option<ServerStatus>,
3366
3367 pub is_owner: bool,
3368 pub is_suspended: bool,
3369 pub is_transferring: bool,
3370 pub permissions: Vec<compact_str::CompactString>,
3371 pub ignored_files: Vec<compact_str::CompactString>,
3372
3373 pub location_uuid: uuid::Uuid,
3374 pub location_name: compact_str::CompactString,
3375 pub location_flag: Option<compact_str::CompactString>,
3376 pub node_uuid: uuid::Uuid,
3377 pub node_name: compact_str::CompactString,
3378 pub node_maintenance_enabled: bool,
3379
3380 pub sftp_host: compact_str::CompactString,
3381 pub sftp_port: i32,
3382
3383 pub name: compact_str::CompactString,
3384 pub description: Option<compact_str::CompactString>,
3385
3386 #[schema(inline)]
3387 pub limits: ApiServerLimits,
3388 #[schema(inline)]
3389 pub feature_limits: ApiServerFeatureLimits,
3390
3391 pub startup: compact_str::CompactString,
3392 pub image: compact_str::CompactString,
3393 #[schema(inline)]
3394 pub auto_kill: wings_api::ServerConfigurationAutoKill,
3395 pub auto_start_behavior: ServerAutoStartBehavior,
3396 pub timezone: Option<compact_str::CompactString>,
3397 #[schema(inline)]
3398 pub trash: ApiServerTrash,
3399
3400 pub created: chrono::DateTime<chrono::Utc>,
3401}
3402
3403#[cfg(test)]
3404mod tests {
3405 use super::{is_path_ignored, is_path_ignored_subtree, validate_labels};
3406 use crate::ignore_list::IgnoreList;
3407 use compact_str::CompactString;
3408 use indexmap::IndexMap;
3409
3410 fn compile(patterns: &[&str]) -> IgnoreList {
3411 IgnoreList::try_from_lines(patterns).unwrap()
3412 }
3413
3414 #[test]
3416 fn an_anchored_pattern_matches_every_spelling_of_the_path() {
3417 let list = compile(&["/config/secrets.yml"]);
3418
3419 for path in [
3420 "/config/secrets.yml",
3421 "/./config/secrets.yml",
3422 "/config//secrets.yml",
3423 "/config/./secrets.yml",
3424 "/config/../config/secrets.yml",
3425 "/../config/secrets.yml",
3426 "config/secrets.yml",
3427 ] {
3428 assert!(is_path_ignored(&list, path, false), "{path}");
3429 }
3430
3431 assert!(!is_path_ignored(&list, "/config/public.yml", false));
3432 }
3433
3434 #[test]
3435 fn a_denied_name_hides_its_subtree_and_a_reinclude_leaves_its_parents_enterable() {
3436 let list = compile(&["secrets"]);
3437
3438 assert!(is_path_ignored(&list, "/secrets", true));
3439 assert!(is_path_ignored(&list, "/secrets/deep/token.txt", false));
3440 assert!(is_path_ignored(&list, "/game/secrets/token.txt", false));
3441 assert!(!is_path_ignored(&list, "/secrets.txt", false));
3442
3443 let list = compile(&["*", "!/game/csgo/cfg"]);
3444
3445 assert!(is_path_ignored(&list, "/game/csgo", true));
3446 assert!(!is_path_ignored_subtree(&list, "/game/csgo", true));
3447 assert!(!is_path_ignored_subtree(&list, "/game/./csgo/", true));
3448 assert!(is_path_ignored_subtree(&list, "/game/hl2", true));
3449 assert!(!is_path_ignored(&list, "/game/csgo/cfg/server.cfg", false));
3450 }
3451
3452 #[test]
3453 fn appending_a_list_keeps_everything_it_hid_hidden() {
3454 let caller = ["!/a/b", "/a/**"];
3457
3458 assert!(is_path_ignored(&compile(&caller), "/a/b", false));
3459 assert!(!is_path_ignored(
3460 &compile(&["/a/**", "!/a/b"]),
3461 "/a/b",
3462 false
3463 ));
3464
3465 for granted in [
3466 vec!["!/a/b", "/a/**"],
3467 vec!["/a/**", "!/a/b", "!/a/b", "/a/**"],
3468 vec!["!/a/**", "!/a/b", "/a/**"],
3469 ] {
3470 assert!(granted.ends_with(&caller));
3471 assert!(
3472 is_path_ignored(&compile(&granted), "/a/b", false),
3473 "{granted:?}"
3474 );
3475 }
3476 }
3477
3478 #[test]
3479 fn the_server_root_is_never_ignored() {
3480 let list = compile(&["*"]);
3481
3482 for path in ["/", "", ".", "/.", "/config/.."] {
3483 assert!(!is_path_ignored(&list, path, true), "{path}");
3484 assert!(!is_path_ignored_subtree(&list, path, true), "{path}");
3485 }
3486 }
3487
3488 fn labels(pairs: &[(&str, &str)]) -> IndexMap<CompactString, CompactString> {
3490 pairs
3491 .iter()
3492 .map(|(key, value)| (CompactString::from(*key), CompactString::from(*value)))
3493 .collect()
3494 }
3495
3496 #[test]
3497 fn ordinary_labels_and_no_labels_at_all_are_accepted() {
3498 assert!(
3499 validate_labels(
3500 &labels(&[("environment", "production"), ("owner", "team-a")]),
3501 &()
3502 )
3503 .is_ok()
3504 );
3505 assert!(validate_labels(&labels(&[]), &()).is_ok());
3506 }
3507
3508 #[test]
3509 fn the_keys_wings_unconditionally_overwrites_are_rejected() {
3510 for key in ["Service", "ContainerType"] {
3511 assert!(
3512 validate_labels(&labels(&[(key, "value")]), &()).is_err(),
3513 "{key}"
3514 );
3515 }
3516 }
3517
3518 #[test]
3519 fn at_most_sixty_four_labels_are_accepted() {
3520 let build = |count: usize| {
3521 (0..count)
3522 .map(|i| {
3523 (
3524 CompactString::from(format!("key-{i}")),
3525 CompactString::new("v"),
3526 )
3527 })
3528 .collect::<IndexMap<_, _>>()
3529 };
3530
3531 assert!(validate_labels(&build(64), &()).is_ok());
3532 assert!(validate_labels(&build(65), &()).is_err());
3533 }
3534
3535 #[test]
3536 fn keys_and_values_are_limited_to_255_characters_rather_than_bytes() {
3537 let long = "é".repeat(255);
3538 let too_long = "a".repeat(256);
3539
3540 assert!(validate_labels(&labels(&[(&long, "v")]), &()).is_ok());
3541 assert!(validate_labels(&labels(&[("k", &long)]), &()).is_ok());
3542
3543 assert!(validate_labels(&labels(&[(&too_long, "v")]), &()).is_err());
3544 assert!(validate_labels(&labels(&[("k", &too_long)]), &()).is_err());
3545 }
3546
3547 #[test]
3548 fn keys_that_are_empty_or_only_whitespace_are_rejected() {
3549 for key in ["", " ", " "] {
3550 assert!(
3551 validate_labels(&labels(&[(key, "value")]), &()).is_err(),
3552 "{key:?}"
3553 );
3554 }
3555 }
3556
3557 #[test]
3558 fn control_characters_are_rejected_in_keys_and_in_values() {
3559 for control in ["\n", "\r", "\0", "\u{7}"] {
3560 let key = format!("ke{control}y");
3561 let value = format!("va{control}lue");
3562
3563 assert!(
3564 validate_labels(&labels(&[(&key, "value")]), &()).is_err(),
3565 "{key:?}"
3566 );
3567 assert!(
3568 validate_labels(&labels(&[("key", &value)]), &()).is_err(),
3569 "{value:?}"
3570 );
3571 }
3572 }
3573}