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