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