1use crate::{
2 models::{
3 InsertQueryBuilder, UpdateQueryBuilder, nest_egg_variable::CreateNestEggVariableOptions,
4 },
5 prelude::*,
6};
7use garde::Validate;
8use indexmap::IndexMap;
9use serde::{Deserialize, Serialize};
10use sqlx::{Row, postgres::PgRow};
11use std::{
12 collections::{BTreeMap, HashSet},
13 sync::{Arc, LazyLock},
14};
15use utoipa::ToSchema;
16
17pub fn validate_docker_images(
18 docker_images: &IndexMap<compact_str::CompactString, compact_str::CompactString>,
19 _context: &(),
20) -> Result<(), garde::Error> {
21 let mut seen_images = HashSet::new();
22 for image in docker_images.values() {
23 if !seen_images.insert(image) {
24 return Err(garde::Error::new(compact_str::format_compact!(
25 "duplicate docker image: {}",
26 image
27 )));
28 }
29 }
30
31 Ok(())
32}
33
34pub fn validate_config_allocations(
35 config_allocations: &NestEggConfigAllocations,
36 _context: &(),
37) -> Result<(), garde::Error> {
38 if !config_allocations.user_self_assign.is_valid() {
39 return Err(garde::Error::new(
40 "port ranges must be 1024-65535 and start_port < end_port",
41 ));
42 }
43
44 Ok(())
45}
46
47fn true_fn() -> bool {
48 true
49}
50
51#[derive(ToSchema, Serialize, Deserialize, Clone, Copy)]
52#[serde(rename_all = "lowercase")]
53#[schema(rename_all = "lowercase")]
54pub enum ServerConfigurationFileParser {
55 File,
56 Yaml,
57 Properties,
58 Ini,
59 Json,
60 Xml,
61 Toml,
62}
63
64#[derive(ToSchema, Serialize, Deserialize, Clone)]
65pub struct ProcessConfigurationFileReplacement {
66 pub r#match: compact_str::CompactString,
67 #[serde(default)]
68 pub insert_new: bool,
69 #[serde(default = "true_fn")]
70 pub update_existing: bool,
71 pub if_value: Option<compact_str::CompactString>,
72 pub replace_with: serde_json::Value,
73}
74
75#[derive(ToSchema, Serialize, Deserialize, Clone)]
76pub struct ProcessConfigurationFile {
77 pub file: compact_str::CompactString,
78 #[serde(default = "true_fn")]
79 pub create_new: bool,
80 #[schema(inline)]
81 pub parser: ServerConfigurationFileParser,
82 #[schema(inline)]
83 pub replace: Vec<ProcessConfigurationFileReplacement>,
84}
85
86#[derive(ToSchema, Serialize, Clone)]
87pub struct ProcessConfiguration {
88 #[schema(inline)]
89 pub startup: crate::models::nest_egg::NestEggConfigStartup,
90 #[schema(inline)]
91 pub stop: crate::models::nest_egg::NestEggConfigStop,
92 #[schema(inline)]
93 pub configs: Vec<ProcessConfigurationFile>,
94}
95
96#[derive(ToSchema, Serialize, Deserialize, Clone, Default)]
97pub struct NestEggConfigStartup {
98 #[serde(
99 default,
100 deserialize_with = "crate::deserialize::deserialize_array_or_not"
101 )]
102 pub done: Vec<compact_str::CompactString>,
103 #[serde(default)]
104 pub strip_ansi: bool,
105}
106
107#[derive(ToSchema, Serialize, Deserialize, Clone, Default)]
108pub struct NestEggConfigStop {
109 pub r#type: compact_str::CompactString,
110 pub value: Option<compact_str::CompactString>,
111}
112
113#[derive(ToSchema, Serialize, Deserialize, Clone)]
114pub struct NestEggConfigScript {
115 pub container: compact_str::CompactString,
116 pub entrypoint: compact_str::CompactString,
117 #[serde(alias = "script")]
118 pub content: String,
119}
120
121#[derive(ToSchema, Serialize, Deserialize, Clone, Copy)]
122pub struct NestEggConfigAllocationsUserSelfAssign {
123 pub enabled: bool,
124 pub require_primary_allocation: bool,
125
126 pub start_port: u16,
127 pub end_port: u16,
128}
129
130impl Default for NestEggConfigAllocationsUserSelfAssign {
131 fn default() -> Self {
132 Self {
133 enabled: false,
134 require_primary_allocation: true,
135 start_port: 49152,
136 end_port: 65535,
137 }
138 }
139}
140
141impl NestEggConfigAllocationsUserSelfAssign {
142 #[inline]
143 pub fn is_valid(&self) -> bool {
144 self.start_port < self.end_port && self.start_port >= 1024
145 }
146}
147
148#[derive(ToSchema, Serialize, Deserialize, Default, Clone)]
149pub struct NestEggConfigAllocations {
150 #[schema(inline)]
151 #[serde(default)]
152 pub user_self_assign: NestEggConfigAllocationsUserSelfAssign,
153}
154
155#[derive(ToSchema, Serialize, Deserialize, Clone)]
156pub struct ExportedNestEggConfigsFilesFile {
157 #[serde(default = "true_fn")]
158 pub create_new: bool,
159 #[schema(inline)]
160 pub parser: ServerConfigurationFileParser,
161 #[schema(inline)]
162 pub replace: Vec<ProcessConfigurationFileReplacement>,
163}
164
165#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
166pub struct ExportedNestEggConfigs {
167 #[garde(skip)]
168 #[schema(inline)]
169 #[serde(
170 default,
171 deserialize_with = "crate::deserialize::deserialize_nest_egg_config_files"
172 )]
173 pub files: IndexMap<compact_str::CompactString, ExportedNestEggConfigsFilesFile>,
174 #[garde(skip)]
175 #[schema(inline)]
176 #[serde(
177 default,
178 deserialize_with = "crate::deserialize::deserialize_pre_stringified"
179 )]
180 pub startup: NestEggConfigStartup,
181 #[garde(skip)]
182 #[schema(inline)]
183 #[serde(
184 default,
185 deserialize_with = "crate::deserialize::deserialize_nest_egg_config_stop"
186 )]
187 pub stop: NestEggConfigStop,
188 #[garde(skip)]
189 #[schema(inline)]
190 #[serde(default)]
191 pub allocations: NestEggConfigAllocations,
192}
193
194#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
195pub struct ExportedNestEggScripts {
196 #[garde(skip)]
197 #[schema(inline)]
198 pub installation: NestEggConfigScript,
199}
200
201#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
202pub struct ExportedNestEgg {
203 #[garde(skip)]
204 #[serde(default = "uuid::Uuid::new_v4")]
205 pub uuid: uuid::Uuid,
206 #[garde(length(chars, min = 3, max = 255))]
207 #[schema(min_length = 3, max_length = 255)]
208 pub name: compact_str::CompactString,
209 #[garde(length(max = 1024))]
210 #[schema(max_length = 1024)]
211 #[serde(deserialize_with = "crate::deserialize::deserialize_string_option")]
212 pub description: Option<compact_str::CompactString>,
213 #[garde(length(chars, min = 2, max = 255))]
214 #[schema(min_length = 2, max_length = 255)]
215 pub author: compact_str::CompactString,
216
217 #[garde(skip)]
218 #[schema(inline)]
219 pub config: ExportedNestEggConfigs,
220 #[garde(skip)]
221 #[schema(inline)]
222 pub scripts: ExportedNestEggScripts,
223
224 #[garde(length(chars, min = 1, max = 8192))]
225 #[schema(min_length = 1, max_length = 8192)]
226 pub startup: compact_str::CompactString,
227 #[garde(skip)]
228 #[serde(default)]
229 pub force_outgoing_ip: bool,
230 #[garde(skip)]
231 #[serde(default)]
232 pub separate_port: bool,
233
234 #[garde(skip)]
235 #[serde(
236 default,
237 deserialize_with = "crate::deserialize::deserialize_defaultable"
238 )]
239 pub features: Vec<compact_str::CompactString>,
240 #[garde(custom(validate_docker_images))]
241 pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
242 #[garde(skip)]
243 #[serde(
244 default,
245 deserialize_with = "crate::deserialize::deserialize_defaultable"
246 )]
247 pub file_denylist: Vec<compact_str::CompactString>,
248
249 #[garde(skip)]
250 #[schema(inline)]
251 pub variables: Vec<super::nest_egg_variable::ExportedNestEggVariable>,
252}
253
254#[derive(Serialize, Deserialize, Clone)]
255pub struct NestEgg {
256 pub uuid: uuid::Uuid,
257 pub nest: Fetchable<super::nest::Nest>,
258 pub egg_repository_egg: Option<Fetchable<super::egg_repository_egg::EggRepositoryEgg>>,
259
260 pub name: compact_str::CompactString,
261 pub description: Option<compact_str::CompactString>,
262 pub author: compact_str::CompactString,
263
264 pub config_files: Vec<ProcessConfigurationFile>,
265 pub config_startup: NestEggConfigStartup,
266 pub config_stop: NestEggConfigStop,
267 pub config_script: NestEggConfigScript,
268 pub config_allocations: NestEggConfigAllocations,
269
270 pub startup: compact_str::CompactString,
271 pub force_outgoing_ip: bool,
272 pub separate_port: bool,
273
274 pub features: Vec<compact_str::CompactString>,
275 pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
276 pub file_denylist: Vec<compact_str::CompactString>,
277
278 pub created: chrono::NaiveDateTime,
279}
280
281impl BaseModel for NestEgg {
282 const NAME: &'static str = "nest_egg";
283
284 #[inline]
285 fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
286 let prefix = prefix.unwrap_or_default();
287
288 BTreeMap::from([
289 (
290 "nest_eggs.uuid",
291 compact_str::format_compact!("{prefix}uuid"),
292 ),
293 (
294 "nest_eggs.nest_uuid",
295 compact_str::format_compact!("{prefix}nest_uuid"),
296 ),
297 (
298 "nest_eggs.egg_repository_egg_uuid",
299 compact_str::format_compact!("{prefix}egg_repository_egg_uuid"),
300 ),
301 (
302 "nest_eggs.name",
303 compact_str::format_compact!("{prefix}name"),
304 ),
305 (
306 "nest_eggs.description",
307 compact_str::format_compact!("{prefix}description"),
308 ),
309 (
310 "nest_eggs.author",
311 compact_str::format_compact!("{prefix}author"),
312 ),
313 (
314 "nest_eggs.config_files",
315 compact_str::format_compact!("{prefix}config_files"),
316 ),
317 (
318 "nest_eggs.config_startup",
319 compact_str::format_compact!("{prefix}config_startup"),
320 ),
321 (
322 "nest_eggs.config_stop",
323 compact_str::format_compact!("{prefix}config_stop"),
324 ),
325 (
326 "nest_eggs.config_script",
327 compact_str::format_compact!("{prefix}config_script"),
328 ),
329 (
330 "nest_eggs.config_allocations",
331 compact_str::format_compact!("{prefix}config_allocations"),
332 ),
333 (
334 "nest_eggs.startup",
335 compact_str::format_compact!("{prefix}startup"),
336 ),
337 (
338 "nest_eggs.force_outgoing_ip",
339 compact_str::format_compact!("{prefix}force_outgoing_ip"),
340 ),
341 (
342 "nest_eggs.separate_port",
343 compact_str::format_compact!("{prefix}separate_port"),
344 ),
345 (
346 "nest_eggs.features",
347 compact_str::format_compact!("{prefix}features"),
348 ),
349 (
350 "nest_eggs.docker_images",
351 compact_str::format_compact!("{prefix}docker_images"),
352 ),
353 (
354 "nest_eggs.file_denylist",
355 compact_str::format_compact!("{prefix}file_denylist"),
356 ),
357 (
358 "nest_eggs.created",
359 compact_str::format_compact!("{prefix}created"),
360 ),
361 (
362 "nest_eggs.created",
363 compact_str::format_compact!("{prefix}created"),
364 ),
365 ])
366 }
367
368 #[inline]
369 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
370 let prefix = prefix.unwrap_or_default();
371
372 Ok(Self {
373 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
374 nest: super::nest::Nest::get_fetchable(
375 row.try_get(compact_str::format_compact!("{prefix}nest_uuid").as_str())?,
376 ),
377 egg_repository_egg: row
378 .try_get::<Option<uuid::Uuid>, _>(
379 compact_str::format_compact!("{prefix}egg_repository_egg_uuid").as_str(),
380 )?
381 .map(super::egg_repository_egg::EggRepositoryEgg::get_fetchable),
382 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
383 description: row
384 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
385 author: row.try_get(compact_str::format_compact!("{prefix}author").as_str())?,
386 config_files: serde_json::from_value(
387 row.try_get(compact_str::format_compact!("{prefix}config_files").as_str())?,
388 )?,
389 config_startup: serde_json::from_value(
390 row.try_get(compact_str::format_compact!("{prefix}config_startup").as_str())?,
391 )?,
392 config_stop: serde_json::from_value(
393 row.try_get(compact_str::format_compact!("{prefix}config_stop").as_str())?,
394 )?,
395 config_script: serde_json::from_value(
396 row.try_get(compact_str::format_compact!("{prefix}config_script").as_str())?,
397 )?,
398 config_allocations: serde_json::from_value(
399 row.try_get(compact_str::format_compact!("{prefix}config_allocations").as_str())?,
400 )?,
401 startup: row.try_get(compact_str::format_compact!("{prefix}startup").as_str())?,
402 force_outgoing_ip: row
403 .try_get(compact_str::format_compact!("{prefix}force_outgoing_ip").as_str())?,
404 separate_port: row
405 .try_get(compact_str::format_compact!("{prefix}separate_port").as_str())?,
406 features: row.try_get(compact_str::format_compact!("{prefix}features").as_str())?,
407 docker_images: serde_json::from_str(row.try_get::<&str, _>(
408 compact_str::format_compact!("{prefix}docker_images").as_str(),
409 )?)
410 .unwrap_or_default(),
411 file_denylist: row
412 .try_get(compact_str::format_compact!("{prefix}file_denylist").as_str())?,
413 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
414 })
415 }
416}
417
418impl NestEgg {
419 pub async fn import(
420 state: &crate::State,
421 nest_uuid: uuid::Uuid,
422 egg_repository_egg_uuid: Option<uuid::Uuid>,
423 exported_egg: ExportedNestEgg,
424 ) -> Result<Self, crate::database::DatabaseError> {
425 let egg = Self::create(
426 state,
427 CreateNestEggOptions {
428 nest_uuid,
429 egg_repository_egg_uuid,
430 author: exported_egg.author,
431 name: exported_egg.name,
432 description: exported_egg.description,
433 config_files: exported_egg
434 .config
435 .files
436 .into_iter()
437 .map(|(file, config)| ProcessConfigurationFile {
438 file,
439 create_new: config.create_new,
440 parser: config.parser,
441 replace: config.replace,
442 })
443 .collect(),
444 config_startup: exported_egg.config.startup,
445 config_stop: exported_egg.config.stop,
446 config_script: exported_egg.scripts.installation,
447 config_allocations: exported_egg.config.allocations,
448 startup: exported_egg.startup,
449 force_outgoing_ip: exported_egg.force_outgoing_ip,
450 separate_port: exported_egg.separate_port,
451 features: exported_egg.features,
452 docker_images: exported_egg.docker_images,
453 file_denylist: exported_egg.file_denylist,
454 },
455 )
456 .await?;
457
458 for variable in exported_egg.variables {
459 if rule_validator::validate_rules(&variable.rules, &()).is_err() {
460 continue;
461 }
462
463 if let Err(err) = super::nest_egg_variable::NestEggVariable::create(
464 state,
465 CreateNestEggVariableOptions {
466 egg_uuid: egg.uuid,
467 name: variable.name,
468 description: variable.description,
469 order: variable.order,
470 env_variable: variable.env_variable,
471 default_value: variable.default_value,
472 user_viewable: variable.user_viewable,
473 user_editable: variable.user_editable,
474 secret: variable.secret,
475 rules: variable.rules,
476 },
477 )
478 .await
479 {
480 tracing::warn!("error while importing nest egg variable: {:?}", err);
481 }
482 }
483
484 Ok(egg)
485 }
486
487 pub async fn import_update(
488 &self,
489 database: &crate::database::Database,
490 exported_egg: ExportedNestEgg,
491 ) -> Result<(), crate::database::DatabaseError> {
492 sqlx::query!(
493 "UPDATE nest_eggs
494 SET
495 author = $2, name = $3, description = $4,
496 config_files = $5, config_startup = $6, config_stop = $7,
497 config_script = $8, config_allocations = $9, startup = $10,
498 force_outgoing_ip = $11, separate_port = $12, features = $13,
499 docker_images = $14, file_denylist = $15
500 WHERE nest_eggs.uuid = $1",
501 self.uuid,
502 &exported_egg.author,
503 &exported_egg.name,
504 exported_egg.description.as_deref(),
505 serde_json::to_value(
506 &exported_egg
507 .config
508 .files
509 .into_iter()
510 .map(|(file, config)| ProcessConfigurationFile {
511 file,
512 create_new: config.create_new,
513 parser: config.parser,
514 replace: config.replace,
515 })
516 .collect::<Vec<_>>(),
517 )?,
518 serde_json::to_value(&exported_egg.config.startup)?,
519 serde_json::to_value(&exported_egg.config.stop)?,
520 serde_json::to_value(&exported_egg.scripts.installation)?,
521 serde_json::to_value(&exported_egg.config.allocations)?,
522 &exported_egg.startup,
523 exported_egg.force_outgoing_ip,
524 exported_egg.separate_port,
525 &exported_egg
526 .features
527 .into_iter()
528 .map(|f| f.into())
529 .collect::<Vec<_>>(),
530 serde_json::to_string(&exported_egg.docker_images)?,
531 &exported_egg
532 .file_denylist
533 .into_iter()
534 .map(|f| f.into())
535 .collect::<Vec<_>>(),
536 )
537 .execute(database.write())
538 .await?;
539
540 let unused_variables = sqlx::query!(
541 "SELECT nest_egg_variables.uuid
542 FROM nest_egg_variables
543 WHERE nest_egg_variables.egg_uuid = $1 AND nest_egg_variables.env_variable != ALL($2)",
544 self.uuid,
545 &exported_egg
546 .variables
547 .iter()
548 .map(|v| v.env_variable.as_str())
549 .collect::<Vec<_>>() as &[&str]
550 )
551 .fetch_all(database.read())
552 .await?;
553
554 for (i, variable) in exported_egg.variables.iter().enumerate() {
555 if rule_validator::validate_rules(&variable.rules, &()).is_err() {
556 continue;
557 }
558
559 if let Err(err) = sqlx::query!(
560 "INSERT INTO nest_egg_variables (
561 egg_uuid, name, description, order_, env_variable,
562 default_value, user_viewable, user_editable, rules
563 )
564 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
565 ON CONFLICT (egg_uuid, env_variable) DO UPDATE SET
566 name = EXCLUDED.name,
567 description = EXCLUDED.description,
568 order_ = EXCLUDED.order_,
569 default_value = EXCLUDED.default_value,
570 user_viewable = EXCLUDED.user_viewable,
571 user_editable = EXCLUDED.user_editable,
572 rules = EXCLUDED.rules",
573 self.uuid,
574 &variable.name,
575 variable.description.as_deref(),
576 if variable.order == 0 {
577 i as i16 + 1
578 } else {
579 variable.order
580 },
581 &variable.env_variable,
582 variable.default_value.as_deref(),
583 variable.user_viewable,
584 variable.user_editable,
585 &variable
586 .rules
587 .iter()
588 .map(|r| r.as_str())
589 .collect::<Vec<_>>() as &[&str]
590 )
591 .execute(database.read())
592 .await
593 {
594 tracing::warn!("error while importing nest egg variable: {:?}", err);
595 }
596 }
597
598 let order_base = exported_egg.variables.len() as i16
599 + exported_egg
600 .variables
601 .iter()
602 .map(|v| v.order)
603 .max()
604 .unwrap_or_default();
605
606 sqlx::query!(
607 "UPDATE nest_egg_variables
608 SET order_ = $1 + array_position($2, nest_egg_variables.uuid)
609 WHERE nest_egg_variables.uuid = ANY($2) AND nest_egg_variables.egg_uuid = $3",
610 order_base as i32,
611 &unused_variables
612 .into_iter()
613 .map(|v| v.uuid)
614 .collect::<Vec<_>>(),
615 self.uuid,
616 )
617 .execute(database.write())
618 .await?;
619
620 Ok(())
621 }
622
623 pub async fn by_nest_uuid_with_pagination(
624 database: &crate::database::Database,
625 nest_uuid: uuid::Uuid,
626 page: i64,
627 per_page: i64,
628 search: Option<&str>,
629 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
630 let offset = (page - 1) * per_page;
631
632 let rows = sqlx::query(&format!(
633 r#"
634 SELECT {}, COUNT(*) OVER() AS total_count
635 FROM nest_eggs
636 WHERE nest_eggs.nest_uuid = $1 AND ($2 IS NULL OR nest_eggs.name ILIKE '%' || $2 || '%')
637 ORDER BY nest_eggs.created
638 LIMIT $3 OFFSET $4
639 "#,
640 Self::columns_sql(None)
641 ))
642 .bind(nest_uuid)
643 .bind(search)
644 .bind(per_page)
645 .bind(offset)
646 .fetch_all(database.read())
647 .await?;
648
649 Ok(super::Pagination {
650 total: rows
651 .first()
652 .map_or(Ok(0), |row| row.try_get("total_count"))?,
653 per_page,
654 page,
655 data: rows
656 .into_iter()
657 .map(|row| Self::map(None, &row))
658 .try_collect_vec()?,
659 })
660 }
661
662 pub async fn by_user_with_pagination(
663 database: &crate::database::Database,
664 user: &super::user::User,
665 page: i64,
666 per_page: i64,
667 search: Option<&str>,
668 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
669 let offset = (page - 1) * per_page;
670
671 let rows = sqlx::query(&format!(
672 r#"
673 SELECT *, COUNT(*) OVER() AS total_count
674 FROM (
675 SELECT DISTINCT ON (nest_eggs.uuid) {}
676 FROM servers
677 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
678 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
679 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
680 WHERE (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1 OR $2)
681 AND ($3 IS NULL OR nest_eggs.name ILIKE '%' || $3 || '%')
682 ORDER BY nest_eggs.uuid
683 ) AS eggs
684 ORDER BY eggs.created
685 LIMIT $4 OFFSET $5
686 "#,
687 Self::columns_sql(None)
688 ))
689 .bind(user.uuid)
690 .bind(user.admin || user.role.as_ref().is_some_and(|r| r.admin_permissions.iter().any(|p| p == "servers.read")))
691 .bind(search)
692 .bind(per_page)
693 .bind(offset)
694 .fetch_all(database.read())
695 .await?;
696
697 Ok(super::Pagination {
698 total: rows
699 .first()
700 .map_or(Ok(0), |row| row.try_get("total_count"))?,
701 per_page,
702 page,
703 data: rows
704 .into_iter()
705 .map(|row| Self::map(None, &row))
706 .try_collect_vec()?,
707 })
708 }
709
710 pub async fn by_nest_uuid_uuid(
711 database: &crate::database::Database,
712 nest_uuid: uuid::Uuid,
713 uuid: uuid::Uuid,
714 ) -> Result<Option<Self>, crate::database::DatabaseError> {
715 let row = sqlx::query(&format!(
716 r#"
717 SELECT {}
718 FROM nest_eggs
719 WHERE nest_eggs.nest_uuid = $1 AND nest_eggs.uuid = $2
720 "#,
721 Self::columns_sql(None)
722 ))
723 .bind(nest_uuid)
724 .bind(uuid)
725 .fetch_optional(database.read())
726 .await?;
727
728 row.try_map(|row| Self::map(None, &row))
729 }
730
731 pub async fn by_nest_uuid_name(
732 database: &crate::database::Database,
733 nest_uuid: uuid::Uuid,
734 name: &str,
735 ) -> Result<Option<Self>, crate::database::DatabaseError> {
736 let row = sqlx::query(&format!(
737 r#"
738 SELECT {}
739 FROM nest_eggs
740 WHERE nest_eggs.nest_uuid = $1 AND nest_eggs.name = $2
741 "#,
742 Self::columns_sql(None)
743 ))
744 .bind(nest_uuid)
745 .bind(name)
746 .fetch_optional(database.read())
747 .await?;
748
749 row.try_map(|row| Self::map(None, &row))
750 }
751
752 pub async fn count_by_nest_uuid(
753 database: &crate::database::Database,
754 nest_uuid: uuid::Uuid,
755 ) -> i64 {
756 sqlx::query_scalar(
757 r#"
758 SELECT COUNT(*)
759 FROM nest_eggs
760 WHERE nest_eggs.nest_uuid = $1
761 "#,
762 )
763 .bind(nest_uuid)
764 .fetch_one(database.read())
765 .await
766 .unwrap_or(0)
767 }
768
769 #[inline]
770 pub async fn into_exported(
771 self,
772 database: &crate::database::Database,
773 ) -> Result<ExportedNestEgg, crate::database::DatabaseError> {
774 Ok(ExportedNestEgg {
775 uuid: self.uuid,
776 author: self.author,
777 name: self.name,
778 description: self.description,
779 config: ExportedNestEggConfigs {
780 files: self
781 .config_files
782 .into_iter()
783 .map(|file| {
784 (
785 file.file,
786 ExportedNestEggConfigsFilesFile {
787 create_new: file.create_new,
788 parser: file.parser,
789 replace: file.replace,
790 },
791 )
792 })
793 .collect(),
794 startup: self.config_startup,
795 stop: self.config_stop,
796 allocations: self.config_allocations,
797 },
798 scripts: ExportedNestEggScripts {
799 installation: self.config_script,
800 },
801 startup: self.startup,
802 force_outgoing_ip: self.force_outgoing_ip,
803 separate_port: self.separate_port,
804 features: self.features,
805 docker_images: self.docker_images,
806 file_denylist: self.file_denylist,
807 variables: super::nest_egg_variable::NestEggVariable::all_by_egg_uuid(
808 database, self.uuid,
809 )
810 .await?
811 .into_iter()
812 .map(|variable| variable.into_exported())
813 .collect(),
814 })
815 }
816
817 #[inline]
818 pub async fn into_admin_api_object(
819 self,
820 database: &crate::database::Database,
821 ) -> Result<AdminApiNestEgg, crate::database::DatabaseError> {
822 Ok(AdminApiNestEgg {
823 uuid: self.uuid,
824 egg_repository_egg: match self.egg_repository_egg {
825 Some(egg_repository_egg) => Some(
826 egg_repository_egg
827 .fetch_cached(database)
828 .await?
829 .into_admin_egg_api_object(database)
830 .await?,
831 ),
832 None => None,
833 },
834 name: self.name,
835 description: self.description,
836 author: self.author,
837 config_files: self.config_files,
838 config_startup: self.config_startup,
839 config_stop: self.config_stop,
840 config_script: self.config_script,
841 config_allocations: self.config_allocations,
842 startup: self.startup,
843 force_outgoing_ip: self.force_outgoing_ip,
844 separate_port: self.separate_port,
845 features: self.features,
846 docker_images: self.docker_images,
847 file_denylist: self.file_denylist,
848 created: self.created.and_utc(),
849 })
850 }
851
852 #[inline]
853 pub fn into_api_object(self) -> ApiNestEgg {
854 ApiNestEgg {
855 uuid: self.uuid,
856 name: self.name,
857 description: self.description,
858 startup: self.startup,
859 separate_port: self.separate_port,
860 features: self.features,
861 docker_images: self.docker_images,
862 created: self.created.and_utc(),
863 }
864 }
865}
866
867#[async_trait::async_trait]
868impl ByUuid for NestEgg {
869 async fn by_uuid(
870 database: &crate::database::Database,
871 uuid: uuid::Uuid,
872 ) -> Result<Self, crate::database::DatabaseError> {
873 let row = sqlx::query(&format!(
874 r#"
875 SELECT {}
876 FROM nest_eggs
877 WHERE nest_eggs.uuid = $1
878 "#,
879 Self::columns_sql(None)
880 ))
881 .bind(uuid)
882 .fetch_one(database.read())
883 .await?;
884
885 Self::map(None, &row)
886 }
887}
888
889#[derive(ToSchema, Deserialize, Validate)]
890pub struct CreateNestEggOptions {
891 #[garde(skip)]
892 pub nest_uuid: uuid::Uuid,
893 #[garde(skip)]
894 pub egg_repository_egg_uuid: Option<uuid::Uuid>,
895 #[garde(length(chars, min = 2, max = 255))]
896 #[schema(min_length = 2, max_length = 255)]
897 pub author: compact_str::CompactString,
898 #[garde(length(chars, min = 3, max = 255))]
899 #[schema(min_length = 3, max_length = 255)]
900 pub name: compact_str::CompactString,
901 #[garde(length(chars, min = 1, max = 1024))]
902 #[schema(min_length = 1, max_length = 1024)]
903 pub description: Option<compact_str::CompactString>,
904 #[garde(skip)]
905 #[schema(inline)]
906 pub config_files: Vec<ProcessConfigurationFile>,
907 #[garde(skip)]
908 #[schema(inline)]
909 pub config_startup: NestEggConfigStartup,
910 #[garde(skip)]
911 #[schema(inline)]
912 pub config_stop: NestEggConfigStop,
913 #[garde(skip)]
914 #[schema(inline)]
915 pub config_script: NestEggConfigScript,
916 #[schema(inline)]
917 #[garde(custom(validate_config_allocations))]
918 pub config_allocations: NestEggConfigAllocations,
919 #[garde(length(chars, min = 1, max = 4096))]
920 #[schema(min_length = 1, max_length = 4096)]
921 pub startup: compact_str::CompactString,
922 #[garde(skip)]
923 pub force_outgoing_ip: bool,
924 #[garde(skip)]
925 pub separate_port: bool,
926 #[garde(skip)]
927 pub features: Vec<compact_str::CompactString>,
928 #[garde(custom(validate_docker_images))]
929 pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
930 #[garde(skip)]
931 pub file_denylist: Vec<compact_str::CompactString>,
932}
933
934#[async_trait::async_trait]
935impl CreatableModel for NestEgg {
936 type CreateOptions<'a> = CreateNestEggOptions;
937 type CreateResult = Self;
938
939 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
940 static CREATE_LISTENERS: LazyLock<CreateListenerList<NestEgg>> =
941 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
942
943 &CREATE_LISTENERS
944 }
945
946 async fn create(
947 state: &crate::State,
948 mut options: Self::CreateOptions<'_>,
949 ) -> Result<Self, crate::database::DatabaseError> {
950 options.validate()?;
951
952 if let Some(egg_repository_egg_uuid) = options.egg_repository_egg_uuid {
953 super::egg_repository_egg::EggRepositoryEgg::by_uuid_optional_cached(
954 &state.database,
955 egg_repository_egg_uuid,
956 )
957 .await?
958 .ok_or(crate::database::InvalidRelationError("egg_repository_egg"))?;
959 }
960
961 let mut transaction = state.database.write().begin().await?;
962
963 let mut query_builder = InsertQueryBuilder::new("nest_eggs");
964
965 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
966 .await?;
967
968 query_builder
969 .set("nest_uuid", options.nest_uuid)
970 .set("egg_repository_egg_uuid", options.egg_repository_egg_uuid)
971 .set("author", &options.author)
972 .set("name", &options.name)
973 .set("description", &options.description)
974 .set("config_files", serde_json::to_value(&options.config_files)?)
975 .set(
976 "config_startup",
977 serde_json::to_value(&options.config_startup)?,
978 )
979 .set("config_stop", serde_json::to_value(&options.config_stop)?)
980 .set(
981 "config_script",
982 serde_json::to_value(&options.config_script)?,
983 )
984 .set(
985 "config_allocations",
986 serde_json::to_value(&options.config_allocations)?,
987 )
988 .set("startup", &options.startup)
989 .set("force_outgoing_ip", options.force_outgoing_ip)
990 .set("separate_port", options.separate_port)
991 .set("features", &options.features)
992 .set(
993 "docker_images",
994 serde_json::to_string(&options.docker_images)?,
995 )
996 .set("file_denylist", &options.file_denylist);
997
998 let row = query_builder
999 .returning(&Self::columns_sql(None))
1000 .fetch_one(&mut *transaction)
1001 .await?;
1002 let nest_egg = Self::map(None, &row)?;
1003
1004 transaction.commit().await?;
1005
1006 Ok(nest_egg)
1007 }
1008}
1009
1010#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
1011pub struct UpdateNestEggOptions {
1012 #[garde(skip)]
1013 #[serde(
1014 default,
1015 skip_serializing_if = "Option::is_none",
1016 with = "::serde_with::rust::double_option"
1017 )]
1018 pub egg_repository_egg_uuid: Option<Option<uuid::Uuid>>,
1019 #[garde(length(chars, min = 2, max = 255))]
1020 #[schema(min_length = 2, max_length = 255)]
1021 pub author: Option<compact_str::CompactString>,
1022 #[garde(length(chars, min = 3, max = 255))]
1023 #[schema(min_length = 3, max_length = 255)]
1024 pub name: Option<compact_str::CompactString>,
1025 #[garde(length(chars, min = 1, max = 1024))]
1026 #[schema(min_length = 1, max_length = 1024)]
1027 #[serde(
1028 default,
1029 skip_serializing_if = "Option::is_none",
1030 with = "::serde_with::rust::double_option"
1031 )]
1032 pub description: Option<Option<compact_str::CompactString>>,
1033 #[garde(skip)]
1034 #[schema(inline)]
1035 pub config_files: Option<Vec<ProcessConfigurationFile>>,
1036 #[garde(skip)]
1037 #[schema(inline)]
1038 pub config_startup: Option<NestEggConfigStartup>,
1039 #[garde(skip)]
1040 #[schema(inline)]
1041 pub config_stop: Option<NestEggConfigStop>,
1042 #[garde(skip)]
1043 #[schema(inline)]
1044 pub config_script: Option<NestEggConfigScript>,
1045 #[schema(inline)]
1046 #[garde(inner(custom(validate_config_allocations)))]
1047 pub config_allocations: Option<NestEggConfigAllocations>,
1048 #[garde(length(chars, min = 1, max = 4096))]
1049 #[schema(min_length = 1, max_length = 4096)]
1050 pub startup: Option<compact_str::CompactString>,
1051 #[garde(skip)]
1052 pub force_outgoing_ip: Option<bool>,
1053 #[garde(skip)]
1054 pub separate_port: Option<bool>,
1055 #[garde(skip)]
1056 pub features: Option<Vec<compact_str::CompactString>>,
1057 #[garde(inner(custom(validate_docker_images)))]
1058 pub docker_images: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
1059 #[garde(skip)]
1060 pub file_denylist: Option<Vec<compact_str::CompactString>>,
1061}
1062
1063#[async_trait::async_trait]
1064impl UpdatableModel for NestEgg {
1065 type UpdateOptions = UpdateNestEggOptions;
1066
1067 fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
1068 static UPDATE_LISTENERS: LazyLock<UpdateListenerList<NestEgg>> =
1069 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1070
1071 &UPDATE_LISTENERS
1072 }
1073
1074 async fn update(
1075 &mut self,
1076 state: &crate::State,
1077 mut options: Self::UpdateOptions,
1078 ) -> Result<(), crate::database::DatabaseError> {
1079 options.validate()?;
1080
1081 let egg_repository_egg =
1082 if let Some(egg_repository_egg_uuid) = &options.egg_repository_egg_uuid {
1083 match egg_repository_egg_uuid {
1084 Some(uuid) => {
1085 super::egg_repository_egg::EggRepositoryEgg::by_uuid_optional_cached(
1086 &state.database,
1087 *uuid,
1088 )
1089 .await?
1090 .ok_or(crate::database::InvalidRelationError("egg_repository_egg"))?;
1091 Some(Some(
1092 super::egg_repository_egg::EggRepositoryEgg::get_fetchable(*uuid),
1093 ))
1094 }
1095 None => Some(None),
1096 }
1097 } else {
1098 None
1099 };
1100
1101 let mut transaction = state.database.write().begin().await?;
1102
1103 let mut query_builder = UpdateQueryBuilder::new("nest_eggs");
1104
1105 Self::run_update_handlers(
1106 self,
1107 &mut options,
1108 &mut query_builder,
1109 state,
1110 &mut transaction,
1111 )
1112 .await?;
1113
1114 query_builder
1115 .set(
1116 "egg_repository_egg_uuid",
1117 options.egg_repository_egg_uuid.as_ref().map(|o| o.as_ref()),
1118 )
1119 .set("author", options.author.as_ref())
1120 .set("name", options.name.as_ref())
1121 .set(
1122 "description",
1123 options.description.as_ref().map(|d| d.as_ref()),
1124 )
1125 .set(
1126 "config_files",
1127 options
1128 .config_files
1129 .as_ref()
1130 .map(serde_json::to_value)
1131 .transpose()?,
1132 )
1133 .set(
1134 "config_startup",
1135 options
1136 .config_startup
1137 .as_ref()
1138 .map(serde_json::to_value)
1139 .transpose()?,
1140 )
1141 .set(
1142 "config_stop",
1143 options
1144 .config_stop
1145 .as_ref()
1146 .map(serde_json::to_value)
1147 .transpose()?,
1148 )
1149 .set(
1150 "config_script",
1151 options
1152 .config_script
1153 .as_ref()
1154 .map(serde_json::to_value)
1155 .transpose()?,
1156 )
1157 .set(
1158 "config_allocations",
1159 options
1160 .config_allocations
1161 .as_ref()
1162 .map(serde_json::to_value)
1163 .transpose()?,
1164 )
1165 .set("startup", options.startup.as_ref())
1166 .set("force_outgoing_ip", options.force_outgoing_ip)
1167 .set("separate_port", options.separate_port)
1168 .set("features", options.features.as_ref())
1169 .set(
1170 "docker_images",
1171 options
1172 .docker_images
1173 .as_ref()
1174 .map(serde_json::to_string)
1175 .transpose()?,
1176 )
1177 .set("file_denylist", options.file_denylist.as_ref())
1178 .where_eq("uuid", self.uuid);
1179
1180 query_builder.execute(&mut *transaction).await?;
1181
1182 if let Some(egg_repository_egg) = egg_repository_egg {
1183 self.egg_repository_egg = egg_repository_egg;
1184 }
1185 if let Some(author) = options.author {
1186 self.author = author;
1187 }
1188 if let Some(name) = options.name {
1189 self.name = name;
1190 }
1191 if let Some(description) = options.description {
1192 self.description = description;
1193 }
1194 if let Some(config_files) = options.config_files {
1195 self.config_files = config_files;
1196 }
1197 if let Some(config_startup) = options.config_startup {
1198 self.config_startup = config_startup;
1199 }
1200 if let Some(config_stop) = options.config_stop {
1201 self.config_stop = config_stop;
1202 }
1203 if let Some(config_script) = options.config_script {
1204 self.config_script = config_script;
1205 }
1206 if let Some(config_allocations) = options.config_allocations {
1207 self.config_allocations = config_allocations;
1208 }
1209 if let Some(startup) = options.startup {
1210 self.startup = startup;
1211 }
1212 if let Some(force_outgoing_ip) = options.force_outgoing_ip {
1213 self.force_outgoing_ip = force_outgoing_ip;
1214 }
1215 if let Some(separate_port) = options.separate_port {
1216 self.separate_port = separate_port;
1217 }
1218 if let Some(features) = options.features {
1219 self.features = features;
1220 }
1221 if let Some(docker_images) = options.docker_images {
1222 self.docker_images = docker_images;
1223 }
1224 if let Some(file_denylist) = options.file_denylist {
1225 self.file_denylist = file_denylist;
1226 }
1227
1228 transaction.commit().await?;
1229
1230 Ok(())
1231 }
1232}
1233
1234#[async_trait::async_trait]
1235impl DeletableModel for NestEgg {
1236 type DeleteOptions = ();
1237
1238 fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
1239 static DELETE_LISTENERS: LazyLock<DeleteListenerList<NestEgg>> =
1240 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1241
1242 &DELETE_LISTENERS
1243 }
1244
1245 async fn delete(
1246 &self,
1247 state: &crate::State,
1248 options: Self::DeleteOptions,
1249 ) -> Result<(), anyhow::Error> {
1250 let mut transaction = state.database.write().begin().await?;
1251
1252 self.run_delete_handlers(&options, state, &mut transaction)
1253 .await?;
1254
1255 sqlx::query(
1256 r#"
1257 DELETE FROM nest_eggs
1258 WHERE nest_eggs.uuid = $1
1259 "#,
1260 )
1261 .bind(self.uuid)
1262 .execute(&mut *transaction)
1263 .await?;
1264
1265 transaction.commit().await?;
1266
1267 Ok(())
1268 }
1269}
1270
1271#[derive(ToSchema, Serialize)]
1272#[schema(title = "AdminNestEgg")]
1273pub struct AdminApiNestEgg {
1274 pub uuid: uuid::Uuid,
1275 pub egg_repository_egg: Option<super::egg_repository_egg::AdminApiEggEggRepositoryEgg>,
1276
1277 pub name: compact_str::CompactString,
1278 pub description: Option<compact_str::CompactString>,
1279 pub author: compact_str::CompactString,
1280
1281 #[schema(inline)]
1282 pub config_files: Vec<ProcessConfigurationFile>,
1283 #[schema(inline)]
1284 pub config_startup: NestEggConfigStartup,
1285 #[schema(inline)]
1286 pub config_stop: NestEggConfigStop,
1287 #[schema(inline)]
1288 pub config_script: NestEggConfigScript,
1289 #[schema(inline)]
1290 pub config_allocations: NestEggConfigAllocations,
1291
1292 pub startup: compact_str::CompactString,
1293 pub force_outgoing_ip: bool,
1294 pub separate_port: bool,
1295
1296 pub features: Vec<compact_str::CompactString>,
1297 pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1298 pub file_denylist: Vec<compact_str::CompactString>,
1299
1300 pub created: chrono::DateTime<chrono::Utc>,
1301}
1302
1303#[derive(ToSchema, Serialize)]
1304#[schema(title = "NestEgg")]
1305pub struct ApiNestEgg {
1306 pub uuid: uuid::Uuid,
1307
1308 pub name: compact_str::CompactString,
1309 pub description: Option<compact_str::CompactString>,
1310
1311 pub startup: compact_str::CompactString,
1312 pub separate_port: bool,
1313
1314 pub features: Vec<compact_str::CompactString>,
1315 pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1316
1317 pub created: chrono::DateTime<chrono::Utc>,
1318}