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