1use std::str::FromStr;
2
3use anyhow::Context;
4use async_timing_util::unix_timestamp_ms;
5use build::StandardRegistryConfig;
6use clap::Parser;
7use config::core::AwsEcrConfig;
8use derive_empty_traits::EmptyTraits;
9use serde::{
10 de::{
11 value::{MapAccessDeserializer, SeqAccessDeserializer},
12 Visitor,
13 },
14 Deserialize, Deserializer, Serialize,
15};
16use serror::Serror;
17use strum::{AsRefStr, Display, EnumString};
18use typeshare::typeshare;
19
20pub mod alert;
22pub mod alerter;
24pub mod api_key;
26pub mod build;
28pub mod builder;
30pub mod config;
32pub mod deployment;
34pub mod logger;
36pub mod permission;
38pub mod procedure;
40pub mod provider;
42pub mod repo;
44pub mod resource;
46pub mod server;
48pub mod server_template;
50pub mod stack;
52pub mod sync;
54pub mod tag;
56pub mod toml;
58pub mod update;
60pub mod user;
62pub mod user_group;
64pub mod variable;
66
67#[typeshare(serialized_as = "number")]
68pub type I64 = i64;
69#[typeshare(serialized_as = "number")]
70pub type U64 = u64;
71#[typeshare(serialized_as = "any")]
72pub type MongoDocument = bson::Document;
73#[typeshare(serialized_as = "any")]
74pub type JsonValue = serde_json::Value;
75#[typeshare(serialized_as = "MongoIdObj")]
76pub type MongoId = String;
77#[typeshare(serialized_as = "__Serror")]
78pub type _Serror = Serror;
79
80#[typeshare]
82#[derive(
83 Debug,
84 Clone,
85 Default,
86 PartialEq,
87 Serialize,
88 Deserialize,
89 Parser,
90 EmptyTraits,
91)]
92pub struct NoData {}
93
94pub trait MergePartial: Sized {
95 type Partial;
96 fn merge_partial(self, partial: Self::Partial) -> Self;
97}
98
99pub fn all_logs_success(logs: &[update::Log]) -> bool {
100 for log in logs {
101 if !log.success {
102 return false;
103 }
104 }
105 true
106}
107
108pub fn optional_string(string: &str) -> Option<String> {
109 if string.is_empty() {
110 None
111 } else {
112 Some(string.to_string())
113 }
114}
115
116pub fn get_image_name(
117 build::Build {
118 name,
119 config:
120 build::BuildConfig {
121 image_name,
122 image_registry,
123 ..
124 },
125 ..
126 }: &build::Build,
127 aws_ecr: impl FnOnce(&String) -> Option<AwsEcrConfig>,
128) -> anyhow::Result<String> {
129 let name = if image_name.is_empty() {
130 to_monitor_name(name)
131 } else {
132 to_monitor_name(image_name)
133 };
134 let name = match image_registry {
135 build::ImageRegistry::None(_) => name,
136 build::ImageRegistry::AwsEcr(label) => {
137 let AwsEcrConfig {
138 region, account_id, ..
139 } = aws_ecr(label).with_context(|| {
140 format!("didn't find aws ecr config for registry {label}")
141 })?;
142 format!("{account_id}.dkr.ecr.{region}.amazonaws.com/{name}")
143 }
144 build::ImageRegistry::Standard(StandardRegistryConfig {
145 domain,
146 account,
147 organization,
148 }) => {
149 if !organization.is_empty() {
150 let org = organization.to_lowercase();
151 format!("{domain}/{org}/{name}")
152 } else if !account.is_empty() {
153 format!("{domain}/{account}/{name}")
154 } else {
155 name
156 }
157 }
158 };
159 Ok(name)
160}
161
162pub fn to_monitor_name(name: &str) -> String {
163 name.to_lowercase().replace([' ', '.'], "_")
164}
165
166pub fn monitor_timestamp() -> i64 {
167 unix_timestamp_ms() as i64
168}
169
170#[typeshare]
171#[derive(Serialize, Deserialize, Debug, Clone)]
172pub struct MongoIdObj {
173 #[serde(rename = "$oid")]
174 pub oid: String,
175}
176
177#[typeshare]
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct __Serror {
180 pub error: String,
181 pub trace: Vec<String>,
182}
183
184#[typeshare]
185#[derive(
186 Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq,
187)]
188pub struct SystemCommand {
189 #[serde(default)]
190 pub path: String,
191 #[serde(default)]
192 pub command: String,
193}
194
195impl SystemCommand {
196 pub fn command(&self) -> Option<String> {
197 if self.is_none() {
198 None
199 } else {
200 Some(format!("cd {} && {}", self.path, self.command))
201 }
202 }
203
204 pub fn into_option(self) -> Option<SystemCommand> {
205 if self.is_none() {
206 None
207 } else {
208 Some(self)
209 }
210 }
211
212 pub fn is_none(&self) -> bool {
213 self.path.is_empty() || self.command.is_empty()
214 }
215}
216
217#[typeshare]
218#[derive(Serialize, Debug, Clone, Copy, Default, PartialEq)]
219pub struct Version {
220 pub major: i32,
221 pub minor: i32,
222 pub patch: i32,
223}
224
225impl<'de> Deserialize<'de> for Version {
226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227 where
228 D: serde::Deserializer<'de>,
229 {
230 #[derive(Deserialize)]
231 struct VersionInner {
232 major: i32,
233 minor: i32,
234 patch: i32,
235 }
236
237 impl From<VersionInner> for Version {
238 fn from(
239 VersionInner {
240 major,
241 minor,
242 patch,
243 }: VersionInner,
244 ) -> Self {
245 Version {
246 major,
247 minor,
248 patch,
249 }
250 }
251 }
252
253 struct VersionVisitor;
254
255 impl<'de> Visitor<'de> for VersionVisitor {
256 type Value = Version;
257 fn expecting(
258 &self,
259 formatter: &mut std::fmt::Formatter,
260 ) -> std::fmt::Result {
261 write!(
262 formatter,
263 "version string or object | example: '0.2.4' or {{ \"major\": 0, \"minor\": 2, \"patch\": 4, }}"
264 )
265 }
266
267 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
268 where
269 E: serde::de::Error,
270 {
271 v.try_into()
272 .map_err(|e| serde::de::Error::custom(format!("{e:#}")))
273 }
274
275 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
276 where
277 A: serde::de::MapAccess<'de>,
278 {
279 Ok(
280 VersionInner::deserialize(MapAccessDeserializer::new(map))?
281 .into(),
282 )
283 }
284 }
285
286 deserializer.deserialize_any(VersionVisitor)
287 }
288}
289
290impl std::fmt::Display for Version {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 f.write_fmt(format_args!(
293 "{}.{}.{}",
294 self.major, self.minor, self.patch
295 ))
296 }
297}
298
299impl TryFrom<&str> for Version {
300 type Error = anyhow::Error;
301
302 fn try_from(value: &str) -> Result<Self, Self::Error> {
303 let mut split = value.split('.');
304 let major = split
305 .next()
306 .context("must provide at least major version")?
307 .parse::<i32>()
308 .context("major version must be integer")?;
309 let minor = split
310 .next()
311 .map(|minor| minor.parse::<i32>())
312 .transpose()
313 .context("minor version must be integer")?
314 .unwrap_or_default();
315 let patch = split
316 .next()
317 .map(|patch| patch.parse::<i32>())
318 .transpose()
319 .context("patch version must be integer")?
320 .unwrap_or_default();
321 Ok(Version {
322 major,
323 minor,
324 patch,
325 })
326 }
327}
328
329impl Version {
330 pub fn increment(&mut self) {
331 self.patch += 1;
332 }
333
334 pub fn is_none(&self) -> bool {
335 self.major == 0 && self.minor == 0 && self.patch == 0
336 }
337}
338
339#[typeshare]
340#[derive(
341 Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
342)]
343pub struct EnvironmentVar {
344 pub variable: String,
345 pub value: String,
346}
347
348pub fn environment_vars_to_string(vars: &[EnvironmentVar]) -> String {
349 vars
350 .iter()
351 .map(|EnvironmentVar { variable, value }| {
352 format!("{variable}={value}")
353 })
354 .collect::<Vec<_>>()
355 .join("\n")
356}
357
358pub fn environment_vars_from_str(
359 value: &str,
360) -> anyhow::Result<Vec<EnvironmentVar>> {
361 let trimmed = value.trim();
362 if trimmed.is_empty() {
363 return Ok(Vec::new());
364 }
365 let res = trimmed
366 .split('\n')
367 .map(|line| line.trim())
368 .enumerate()
369 .filter(|(_, line)| {
370 !line.is_empty()
371 && !line.starts_with('#')
372 && !line.starts_with("//")
373 })
374 .map(|(i, line)| {
375 let (variable, value) = line
376 .split_once('=')
377 .with_context(|| format!("line {i} missing assignment (=)"))
378 .map(|(variable, value)| {
379 (variable.trim().to_string(), value.trim().to_string())
380 })?;
381 anyhow::Ok(EnvironmentVar { variable, value })
382 })
383 .collect::<anyhow::Result<Vec<_>>>()?;
384 Ok(res)
385}
386
387pub fn env_vars_deserializer<'de, D>(
388 deserializer: D,
389) -> Result<Vec<EnvironmentVar>, D::Error>
390where
391 D: Deserializer<'de>,
392{
393 deserializer.deserialize_any(EnvironmentVarVisitor)
394}
395
396pub fn option_env_vars_deserializer<'de, D>(
397 deserializer: D,
398) -> Result<Option<Vec<EnvironmentVar>>, D::Error>
399where
400 D: Deserializer<'de>,
401{
402 deserializer.deserialize_any(OptionEnvVarVisitor)
403}
404
405struct EnvironmentVarVisitor;
406
407impl<'de> Visitor<'de> for EnvironmentVarVisitor {
408 type Value = Vec<EnvironmentVar>;
409
410 fn expecting(
411 &self,
412 formatter: &mut std::fmt::Formatter,
413 ) -> std::fmt::Result {
414 write!(formatter, "string or Vec<EnvironmentVar>")
415 }
416
417 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
418 where
419 E: serde::de::Error,
420 {
421 environment_vars_from_str(v)
422 .map_err(|e| serde::de::Error::custom(format!("{e:#}")))
423 }
424
425 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
426 where
427 A: serde::de::SeqAccess<'de>,
428 {
429 #[derive(Deserialize)]
430 struct EnvironmentVarInner {
431 variable: String,
432 value: String,
433 }
434
435 impl From<EnvironmentVarInner> for EnvironmentVar {
436 fn from(value: EnvironmentVarInner) -> Self {
437 Self {
438 variable: value.variable,
439 value: value.value,
440 }
441 }
442 }
443
444 let res = Vec::<EnvironmentVarInner>::deserialize(
445 SeqAccessDeserializer::new(seq),
446 )?
447 .into_iter()
448 .map(Into::into)
449 .collect();
450 Ok(res)
451 }
452}
453
454struct OptionEnvVarVisitor;
455
456impl<'de> Visitor<'de> for OptionEnvVarVisitor {
457 type Value = Option<Vec<EnvironmentVar>>;
458
459 fn expecting(
460 &self,
461 formatter: &mut std::fmt::Formatter,
462 ) -> std::fmt::Result {
463 write!(formatter, "null or string or Vec<EnvironmentVar>")
464 }
465
466 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
467 where
468 E: serde::de::Error,
469 {
470 EnvironmentVarVisitor.visit_str(v).map(Some)
471 }
472
473 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
474 where
475 A: serde::de::SeqAccess<'de>,
476 {
477 EnvironmentVarVisitor.visit_seq(seq).map(Some)
478 }
479
480 fn visit_none<E>(self) -> Result<Self::Value, E>
481 where
482 E: serde::de::Error,
483 {
484 Ok(None)
485 }
486
487 fn visit_unit<E>(self) -> Result<Self::Value, E>
488 where
489 E: serde::de::Error,
490 {
491 Ok(None)
492 }
493}
494
495#[typeshare]
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct LatestCommit {
498 pub hash: String,
499 pub message: String,
500}
501
502#[typeshare]
503#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
504pub struct CloneArgs {
505 pub name: String,
507 pub provider: Option<String>,
509 pub https: bool,
511 pub repo: Option<String>,
513 pub branch: Option<String>,
515 pub commit: Option<String>,
517 pub destination: Option<String>,
519 pub on_clone: Option<SystemCommand>,
521 pub on_pull: Option<SystemCommand>,
523 pub account: Option<String>,
525}
526
527impl From<&self::build::Build> for CloneArgs {
528 fn from(build: &self::build::Build) -> CloneArgs {
529 CloneArgs {
530 name: build.name.clone(),
531 repo: optional_string(&build.config.repo),
532 branch: optional_string(&build.config.branch),
533 commit: optional_string(&build.config.commit),
534 destination: None,
535 on_clone: build.config.pre_build.clone().into_option(),
536 on_pull: None,
537 provider: optional_string(&build.config.git_provider),
538 https: build.config.git_https,
539 account: optional_string(&build.config.git_account),
540 }
541 }
542}
543
544impl From<&self::repo::Repo> for CloneArgs {
545 fn from(repo: &self::repo::Repo) -> CloneArgs {
546 CloneArgs {
547 name: repo.name.clone(),
548 repo: optional_string(&repo.config.repo),
549 branch: optional_string(&repo.config.branch),
550 commit: optional_string(&repo.config.commit),
551 destination: optional_string(&repo.config.path),
552 on_clone: repo.config.on_clone.clone().into_option(),
553 on_pull: repo.config.on_pull.clone().into_option(),
554 provider: optional_string(&repo.config.git_provider),
555 https: repo.config.git_https,
556 account: optional_string(&repo.config.git_account),
557 }
558 }
559}
560
561impl From<&self::sync::ResourceSync> for CloneArgs {
562 fn from(sync: &self::sync::ResourceSync) -> Self {
563 CloneArgs {
564 name: sync.name.clone(),
565 repo: optional_string(&sync.config.repo),
566 branch: optional_string(&sync.config.branch),
567 commit: optional_string(&sync.config.commit),
568 destination: None,
569 on_clone: None,
570 on_pull: None,
571 provider: optional_string(&sync.config.git_provider),
572 https: sync.config.git_https,
573 account: optional_string(&sync.config.git_account),
574 }
575 }
576}
577
578impl From<&self::stack::Stack> for CloneArgs {
579 fn from(stack: &self::stack::Stack) -> Self {
580 CloneArgs {
581 name: stack.name.clone(),
582 repo: optional_string(&stack.config.repo),
583 branch: optional_string(&stack.config.branch),
584 commit: optional_string(&stack.config.commit),
585 destination: None,
586 on_clone: None,
587 on_pull: None,
588 provider: optional_string(&stack.config.git_provider),
589 https: stack.config.git_https,
590 account: optional_string(&stack.config.git_account),
591 }
592 }
593}
594
595#[typeshare]
596#[derive(
597 Serialize,
598 Deserialize,
599 Debug,
600 Display,
601 EnumString,
602 PartialEq,
603 Hash,
604 Eq,
605 Clone,
606 Copy,
607 Default,
608)]
609#[serde(rename_all = "snake_case")]
610#[strum(serialize_all = "snake_case")]
611pub enum Timelength {
612 #[serde(rename = "1-sec")]
613 #[strum(serialize = "1-sec")]
614 OneSecond,
615 #[serde(rename = "5-sec")]
616 #[strum(serialize = "5-sec")]
617 FiveSeconds,
618 #[serde(rename = "10-sec")]
619 #[strum(serialize = "10-sec")]
620 TenSeconds,
621 #[serde(rename = "15-sec")]
622 #[strum(serialize = "15-sec")]
623 FifteenSeconds,
624 #[serde(rename = "30-sec")]
625 #[strum(serialize = "30-sec")]
626 ThirtySeconds,
627 #[default]
628 #[serde(rename = "1-min")]
629 #[strum(serialize = "1-min")]
630 OneMinute,
631 #[serde(rename = "2-min")]
632 #[strum(serialize = "2-min")]
633 TwoMinutes,
634 #[serde(rename = "5-min")]
635 #[strum(serialize = "5-min")]
636 FiveMinutes,
637 #[serde(rename = "10-min")]
638 #[strum(serialize = "10-min")]
639 TenMinutes,
640 #[serde(rename = "15-min")]
641 #[strum(serialize = "15-min")]
642 FifteenMinutes,
643 #[serde(rename = "30-min")]
644 #[strum(serialize = "30-min")]
645 ThirtyMinutes,
646 #[serde(rename = "1-hr")]
647 #[strum(serialize = "1-hr")]
648 OneHour,
649 #[serde(rename = "2-hr")]
650 #[strum(serialize = "2-hr")]
651 TwoHours,
652 #[serde(rename = "6-hr")]
653 #[strum(serialize = "6-hr")]
654 SixHours,
655 #[serde(rename = "8-hr")]
656 #[strum(serialize = "8-hr")]
657 EightHours,
658 #[serde(rename = "12-hr")]
659 #[strum(serialize = "12-hr")]
660 TwelveHours,
661 #[serde(rename = "1-day")]
662 #[strum(serialize = "1-day")]
663 OneDay,
664 #[serde(rename = "3-day")]
665 #[strum(serialize = "3-day")]
666 ThreeDay,
667 #[serde(rename = "1-wk")]
668 #[strum(serialize = "1-wk")]
669 OneWeek,
670 #[serde(rename = "2-wk")]
671 #[strum(serialize = "2-wk")]
672 TwoWeeks,
673 #[serde(rename = "30-day")]
674 #[strum(serialize = "30-day")]
675 ThirtyDays,
676}
677
678impl TryInto<async_timing_util::Timelength> for Timelength {
679 type Error = anyhow::Error;
680 fn try_into(
681 self,
682 ) -> Result<async_timing_util::Timelength, Self::Error> {
683 async_timing_util::Timelength::from_str(&self.to_string())
684 .context("failed to parse timelength?")
685 }
686}
687
688#[typeshare]
689#[derive(
690 Debug,
691 Clone,
692 Copy,
693 PartialEq,
694 Eq,
695 Hash,
696 Serialize,
697 Deserialize,
698 Default,
699 Display,
700 EnumString,
701 AsRefStr,
702)]
703pub enum Operation {
704 #[default]
706 None,
707
708 CreateServer,
710 UpdateServer,
711 DeleteServer,
712 RenameServer,
713 PruneImages,
714 PruneContainers,
715 PruneNetworks,
716 CreateNetwork,
717 DeleteNetwork,
718 StopAllContainers,
719
720 CreateBuild,
722 UpdateBuild,
723 DeleteBuild,
724 RunBuild,
725 CancelBuild,
726
727 CreateBuilder,
729 UpdateBuilder,
730 DeleteBuilder,
731
732 CreateDeployment,
734 UpdateDeployment,
735 DeleteDeployment,
736 Deploy,
737 StartContainer,
738 RestartContainer,
739 PauseContainer,
740 UnpauseContainer,
741 StopContainer,
742 RemoveContainer,
743 RenameDeployment,
744
745 CreateRepo,
747 UpdateRepo,
748 DeleteRepo,
749 CloneRepo,
750 PullRepo,
751 BuildRepo,
752 CancelRepoBuild,
753
754 CreateAlerter,
756 UpdateAlerter,
757 DeleteAlerter,
758
759 CreateProcedure,
761 UpdateProcedure,
762 DeleteProcedure,
763 RunProcedure,
764
765 CreateServerTemplate,
767 UpdateServerTemplate,
768 DeleteServerTemplate,
769 LaunchServer,
770
771 CreateResourceSync,
773 UpdateResourceSync,
774 DeleteResourceSync,
775 RunSync,
776
777 CreateStack,
779 UpdateStack,
780 RenameStack,
781 DeleteStack,
782 RefreshStackCache,
783 DeployStack,
784 StartStack,
785 RestartStack,
786 PauseStack,
787 UnpauseStack,
788 StopStack,
789 DestroyStack,
790
791 StartStackService,
793 RestartStackService,
794 PauseStackService,
795 UnpauseStackService,
796 StopStackService,
797
798 CreateVariable,
800 UpdateVariableValue,
801 DeleteVariable,
802
803 CreateGitProviderAccount,
805 UpdateGitProviderAccount,
806 DeleteGitProviderAccount,
807
808 CreateDockerRegistryAccount,
810 UpdateDockerRegistryAccount,
811 DeleteDockerRegistryAccount,
812}
813
814#[typeshare]
815#[derive(
816 Serialize,
817 Deserialize,
818 Debug,
819 Default,
820 Display,
821 EnumString,
822 PartialEq,
823 Hash,
824 Eq,
825 Clone,
826 Copy,
827)]
828pub enum SearchCombinator {
829 #[default]
830 Or,
831 And,
832}