1use std::collections::HashMap;
2
3use anyhow::Context;
4use bson::{doc, Document};
5use derive_builder::Builder;
6use derive_default_builder::DefaultBuilder;
7use derive_variants::EnumVariants;
8use partial_derive2::Partial;
9use serde::{
10 de::{value::SeqAccessDeserializer, Visitor},
11 Deserialize, Deserializer, Serialize,
12};
13use strum::{Display, EnumString};
14use typeshare::typeshare;
15
16use super::{
17 resource::{Resource, ResourceListItem, ResourceQuery},
18 EnvironmentVar, Version,
19};
20
21#[typeshare]
22pub type Deployment = Resource<DeploymentConfig, ()>;
23
24#[typeshare]
25pub type DeploymentListItem =
26 ResourceListItem<DeploymentListItemInfo>;
27
28#[typeshare]
29#[derive(Serialize, Deserialize, Debug, Clone)]
30pub struct DeploymentListItemInfo {
31 pub state: DeploymentState,
33 pub status: Option<String>,
35 pub image: String,
37 pub server_id: String,
39 pub build_id: Option<String>,
41}
42
43#[typeshare(serialized_as = "Partial<DeploymentConfig>")]
44pub type _PartialDeploymentConfig = PartialDeploymentConfig;
45
46#[typeshare]
47#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]
48#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
49#[partial(skip_serializing_none, from, diff)]
50pub struct DeploymentConfig {
51 #[serde(default, alias = "server")]
53 #[partial_attr(serde(alias = "server"))]
54 #[builder(default)]
55 pub server_id: String,
56
57 #[serde(default)]
60 #[builder(default)]
61 pub image: DeploymentImage,
62
63 #[serde(default)]
70 #[builder(default)]
71 pub image_registry_account: String,
72
73 #[serde(default)]
75 #[builder(default)]
76 pub skip_secret_interp: bool,
77
78 #[serde(default)]
80 #[builder(default)]
81 pub redeploy_on_build: bool,
82
83 #[serde(default = "default_send_alerts")]
85 #[builder(default = "default_send_alerts()")]
86 #[partial_default(default_send_alerts())]
87 pub send_alerts: bool,
88
89 #[serde(default = "default_network")]
92 #[builder(default = "default_network()")]
93 #[partial_default(default_network())]
94 pub network: String,
95
96 #[serde(default)]
98 #[builder(default)]
99 pub restart: RestartMode,
100
101 #[serde(default)]
106 #[builder(default)]
107 pub command: String,
108
109 #[serde(default)]
111 #[builder(default)]
112 pub termination_signal: TerminationSignal,
113
114 #[serde(default = "default_termination_timeout")]
116 #[builder(default = "default_termination_timeout()")]
117 #[partial_default(default_termination_timeout())]
118 pub termination_timeout: i32,
119
120 #[serde(default)]
123 #[builder(default)]
124 pub extra_args: Vec<String>,
125
126 #[serde(
129 default = "default_term_signal_labels",
130 deserialize_with = "term_labels_deserializer"
131 )]
132 #[partial_attr(serde(
133 default,
134 deserialize_with = "option_term_labels_deserializer"
135 ))]
136 #[builder(default = "default_term_signal_labels()")]
137 #[partial_default(default_term_signal_labels())]
138 pub term_signal_labels: Vec<TerminationSignalLabel>,
139
140 #[serde(default, deserialize_with = "conversions_deserializer")]
144 #[partial_attr(serde(
145 default,
146 deserialize_with = "option_conversions_deserializer"
147 ))]
148 #[builder(default)]
149 pub ports: Vec<Conversion>,
150
151 #[serde(default, deserialize_with = "conversions_deserializer")]
154 #[partial_attr(serde(
155 default,
156 deserialize_with = "option_conversions_deserializer"
157 ))]
158 #[builder(default)]
159 pub volumes: Vec<Conversion>,
160
161 #[serde(
163 default,
164 deserialize_with = "super::env_vars_deserializer"
165 )]
166 #[partial_attr(serde(
167 default,
168 deserialize_with = "super::option_env_vars_deserializer"
169 ))]
170 #[builder(default)]
171 pub environment: Vec<EnvironmentVar>,
172
173 #[serde(
175 default,
176 deserialize_with = "super::env_vars_deserializer"
177 )]
178 #[partial_attr(serde(
179 default,
180 deserialize_with = "super::option_env_vars_deserializer"
181 ))]
182 #[builder(default)]
183 pub labels: Vec<EnvironmentVar>,
184}
185
186impl DeploymentConfig {
187 pub fn builder() -> DeploymentConfigBuilder {
188 DeploymentConfigBuilder::default()
189 }
190}
191
192fn default_send_alerts() -> bool {
193 true
194}
195
196fn default_term_signal_labels() -> Vec<TerminationSignalLabel> {
197 vec![TerminationSignalLabel::default()]
198}
199
200fn default_termination_timeout() -> i32 {
201 10
202}
203
204fn default_network() -> String {
205 String::from("host")
206}
207
208impl Default for DeploymentConfig {
209 fn default() -> Self {
210 Self {
211 server_id: Default::default(),
212 send_alerts: default_send_alerts(),
213 image: Default::default(),
214 image_registry_account: Default::default(),
215 skip_secret_interp: Default::default(),
216 redeploy_on_build: Default::default(),
217 term_signal_labels: default_term_signal_labels(),
218 termination_signal: Default::default(),
219 termination_timeout: default_termination_timeout(),
220 ports: Default::default(),
221 volumes: Default::default(),
222 environment: Default::default(),
223 labels: Default::default(),
224 network: default_network(),
225 restart: Default::default(),
226 command: Default::default(),
227 extra_args: Default::default(),
228 }
229 }
230}
231
232#[typeshare]
233#[derive(
234 Serialize, Deserialize, Debug, Clone, PartialEq, EnumVariants,
235)]
236#[variant_derive(
237 Serialize,
238 Deserialize,
239 Debug,
240 Clone,
241 Copy,
242 PartialEq,
243 Eq,
244 Display,
245 EnumString
246)]
247#[serde(tag = "type", content = "params")]
248pub enum DeploymentImage {
249 Image {
251 #[serde(default)]
253 image: String,
254 },
255
256 Build {
258 #[serde(default, alias = "build")]
260 build_id: String,
261 #[serde(default)]
264 version: Version,
265 },
266}
267
268impl Default for DeploymentImage {
269 fn default() -> Self {
270 Self::Image {
271 image: Default::default(),
272 }
273 }
274}
275
276#[typeshare]
277#[derive(
278 Debug, Clone, Default, PartialEq, Serialize, Deserialize,
279)]
280pub struct Conversion {
281 pub local: String,
283 pub container: String,
285}
286
287pub fn conversions_to_string(conversions: &[Conversion]) -> String {
288 conversions
289 .iter()
290 .map(|Conversion { local, container }| {
291 format!("{local}={container}")
292 })
293 .collect::<Vec<_>>()
294 .join("\n")
295}
296
297pub fn conversions_from_str(
298 value: &str,
299) -> anyhow::Result<Vec<Conversion>> {
300 let trimmed = value.trim();
301 if trimmed.is_empty() {
302 return Ok(Vec::new());
303 }
304 let res = trimmed
305 .split('\n')
306 .map(|line| line.trim())
307 .enumerate()
308 .filter(|(_, line)| {
309 !line.is_empty()
310 && !line.starts_with('#')
311 && !line.starts_with("//")
312 })
313 .map(|(i, line)| {
314 let (local, container) = line
315 .split_once('=')
316 .with_context(|| format!("line {i} missing assignment (=)"))
317 .map(|(local, container)| {
318 (local.trim().to_string(), container.trim().to_string())
319 })?;
320 anyhow::Ok(Conversion { local, container })
321 })
322 .collect::<anyhow::Result<Vec<_>>>()?;
323 Ok(res)
324}
325
326pub fn conversions_deserializer<'de, D>(
327 deserializer: D,
328) -> Result<Vec<Conversion>, D::Error>
329where
330 D: Deserializer<'de>,
331{
332 deserializer.deserialize_any(ConversionVisitor)
333}
334
335pub fn option_conversions_deserializer<'de, D>(
336 deserializer: D,
337) -> Result<Option<Vec<Conversion>>, D::Error>
338where
339 D: Deserializer<'de>,
340{
341 deserializer.deserialize_any(OptionConversionVisitor)
342}
343
344struct ConversionVisitor;
345
346impl<'de> Visitor<'de> for ConversionVisitor {
347 type Value = Vec<Conversion>;
348
349 fn expecting(
350 &self,
351 formatter: &mut std::fmt::Formatter,
352 ) -> std::fmt::Result {
353 write!(formatter, "string or Vec<Conversion>")
354 }
355
356 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
357 where
358 E: serde::de::Error,
359 {
360 conversions_from_str(v)
361 .map_err(|e| serde::de::Error::custom(format!("{e:#}")))
362 }
363
364 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
365 where
366 A: serde::de::SeqAccess<'de>,
367 {
368 #[derive(Deserialize)]
369 struct ConversionInner {
370 local: String,
371 container: String,
372 }
373
374 impl From<ConversionInner> for Conversion {
375 fn from(value: ConversionInner) -> Self {
376 Self {
377 local: value.local,
378 container: value.container,
379 }
380 }
381 }
382
383 let res = Vec::<ConversionInner>::deserialize(
384 SeqAccessDeserializer::new(seq),
385 )?
386 .into_iter()
387 .map(Into::into)
388 .collect();
389 Ok(res)
390 }
391}
392
393struct OptionConversionVisitor;
394
395impl<'de> Visitor<'de> for OptionConversionVisitor {
396 type Value = Option<Vec<Conversion>>;
397
398 fn expecting(
399 &self,
400 formatter: &mut std::fmt::Formatter,
401 ) -> std::fmt::Result {
402 write!(formatter, "null or string or Vec<Conversion>")
403 }
404
405 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
406 where
407 E: serde::de::Error,
408 {
409 ConversionVisitor.visit_str(v).map(Some)
410 }
411
412 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
413 where
414 A: serde::de::SeqAccess<'de>,
415 {
416 ConversionVisitor.visit_seq(seq).map(Some)
417 }
418
419 fn visit_none<E>(self) -> Result<Self::Value, E>
420 where
421 E: serde::de::Error,
422 {
423 Ok(None)
424 }
425
426 fn visit_unit<E>(self) -> Result<Self::Value, E>
427 where
428 E: serde::de::Error,
429 {
430 Ok(None)
431 }
432}
433
434#[typeshare]
436#[derive(Serialize, Deserialize, Debug, Clone)]
437pub struct ContainerSummary {
438 pub name: String,
440 pub id: String,
442 pub image: String,
444 pub labels: HashMap<String, String>,
446 pub state: DeploymentState,
448 pub status: Option<String>,
450 pub network_mode: Option<String>,
452 pub networks: Option<Vec<String>>,
454}
455
456#[typeshare]
457#[derive(Serialize, Deserialize, Debug, Clone)]
458pub struct DockerContainerStats {
459 #[serde(alias = "Name")]
460 pub name: String,
461 #[serde(alias = "CPUPerc")]
462 pub cpu_perc: String,
463 #[serde(alias = "MemPerc")]
464 pub mem_perc: String,
465 #[serde(alias = "MemUsage")]
466 pub mem_usage: String,
467 #[serde(alias = "NetIO")]
468 pub net_io: String,
469 #[serde(alias = "BlockIO")]
470 pub block_io: String,
471 #[serde(alias = "PIDs")]
472 pub pids: String,
473}
474
475#[typeshare]
482#[derive(
483 Serialize,
484 Deserialize,
485 Debug,
486 PartialEq,
487 Hash,
488 Eq,
489 Clone,
490 Copy,
491 Default,
492 Display,
493 EnumString,
494)]
495#[serde(rename_all = "snake_case")]
496#[strum(serialize_all = "snake_case")]
497pub enum DeploymentState {
498 #[default]
499 Unknown,
500 NotDeployed,
501 Created,
502 Restarting,
503 Running,
504 Removing,
505 Paused,
506 Exited,
507 Dead,
508}
509
510#[typeshare]
511#[derive(
512 Serialize,
513 Deserialize,
514 Debug,
515 PartialEq,
516 Hash,
517 Eq,
518 Clone,
519 Copy,
520 Default,
521 Display,
522 EnumString,
523)]
524pub enum RestartMode {
525 #[default]
526 #[serde(rename = "no")]
527 #[strum(serialize = "no")]
528 NoRestart,
529 #[serde(rename = "on-failure")]
530 #[strum(serialize = "on-failure")]
531 OnFailure,
532 #[serde(rename = "always")]
533 #[strum(serialize = "always")]
534 Always,
535 #[serde(rename = "unless-stopped")]
536 #[strum(serialize = "unless-stopped")]
537 UnlessStopped,
538}
539
540#[typeshare]
541#[derive(
542 Serialize,
543 Deserialize,
544 Debug,
545 PartialEq,
546 Hash,
547 Eq,
548 Clone,
549 Copy,
550 Default,
551 Display,
552 EnumString,
553)]
554#[serde(rename_all = "UPPERCASE")]
555#[strum(serialize_all = "UPPERCASE")]
556pub enum TerminationSignal {
557 #[serde(alias = "1")]
558 SigHup,
559 #[serde(alias = "2")]
560 SigInt,
561 #[serde(alias = "3")]
562 SigQuit,
563 #[default]
564 #[serde(alias = "15")]
565 SigTerm,
566}
567
568#[typeshare]
569#[derive(
570 Serialize,
571 Deserialize,
572 Debug,
573 Clone,
574 Default,
575 PartialEq,
576 Eq,
577 Builder,
578)]
579pub struct TerminationSignalLabel {
580 #[builder(default)]
581 pub signal: TerminationSignal,
582 #[builder(default)]
583 pub label: String,
584}
585
586pub fn term_signal_labels_to_string(
587 labels: &[TerminationSignalLabel],
588) -> String {
589 labels
590 .iter()
591 .map(|TerminationSignalLabel { signal, label }| {
592 format!("{signal}={label}")
593 })
594 .collect::<Vec<_>>()
595 .join("\n")
596}
597
598pub fn term_signal_labels_from_str(
599 value: &str,
600) -> anyhow::Result<Vec<TerminationSignalLabel>> {
601 let trimmed = value.trim();
602 if trimmed.is_empty() {
603 return Ok(Vec::new());
604 }
605 let res = trimmed
606 .split('\n')
607 .map(|line| line.trim())
608 .enumerate()
609 .filter(|(_, line)| {
610 !line.is_empty()
611 && !line.starts_with('#')
612 && !line.starts_with("//")
613 })
614 .map(|(i, line)| {
615 let (signal, label) = line
616 .split_once('=')
617 .with_context(|| format!("line {i} missing assignment (=)"))
618 .map(|(signal, label)| {
619 (
620 signal.trim().parse::<TerminationSignal>().with_context(
621 || format!("line {i} does not have valid signal"),
622 ),
623 label.trim().to_string(),
624 )
625 })?;
626 anyhow::Ok(TerminationSignalLabel {
627 signal: signal?,
628 label,
629 })
630 })
631 .collect::<anyhow::Result<Vec<_>>>()?;
632 Ok(res)
633}
634
635pub fn term_labels_deserializer<'de, D>(
636 deserializer: D,
637) -> Result<Vec<TerminationSignalLabel>, D::Error>
638where
639 D: Deserializer<'de>,
640{
641 deserializer.deserialize_any(TermSignalLabelVisitor)
642}
643
644pub fn option_term_labels_deserializer<'de, D>(
645 deserializer: D,
646) -> Result<Option<Vec<TerminationSignalLabel>>, D::Error>
647where
648 D: Deserializer<'de>,
649{
650 deserializer.deserialize_any(OptionTermSignalLabelVisitor)
651}
652
653struct TermSignalLabelVisitor;
654
655impl<'de> Visitor<'de> for TermSignalLabelVisitor {
656 type Value = Vec<TerminationSignalLabel>;
657
658 fn expecting(
659 &self,
660 formatter: &mut std::fmt::Formatter,
661 ) -> std::fmt::Result {
662 write!(formatter, "string or Vec<TerminationSignalLabel>")
663 }
664
665 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
666 where
667 E: serde::de::Error,
668 {
669 term_signal_labels_from_str(v)
670 .map_err(|e| serde::de::Error::custom(format!("{e:#}")))
671 }
672
673 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
674 where
675 A: serde::de::SeqAccess<'de>,
676 {
677 #[derive(Deserialize)]
678 struct TermSignalLabelInner {
679 signal: TerminationSignal,
680 label: String,
681 }
682
683 impl From<TermSignalLabelInner> for TerminationSignalLabel {
684 fn from(value: TermSignalLabelInner) -> Self {
685 Self {
686 signal: value.signal,
687 label: value.label,
688 }
689 }
690 }
691
692 let res = Vec::<TermSignalLabelInner>::deserialize(
693 SeqAccessDeserializer::new(seq),
694 )?
695 .into_iter()
696 .map(Into::into)
697 .collect();
698 Ok(res)
699 }
700}
701
702struct OptionTermSignalLabelVisitor;
703
704impl<'de> Visitor<'de> for OptionTermSignalLabelVisitor {
705 type Value = Option<Vec<TerminationSignalLabel>>;
706
707 fn expecting(
708 &self,
709 formatter: &mut std::fmt::Formatter,
710 ) -> std::fmt::Result {
711 write!(formatter, "null or string or Vec<TerminationSignalLabel>")
712 }
713
714 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
715 where
716 E: serde::de::Error,
717 {
718 TermSignalLabelVisitor.visit_str(v).map(Some)
719 }
720
721 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
722 where
723 A: serde::de::SeqAccess<'de>,
724 {
725 TermSignalLabelVisitor.visit_seq(seq).map(Some)
726 }
727
728 fn visit_none<E>(self) -> Result<Self::Value, E>
729 where
730 E: serde::de::Error,
731 {
732 Ok(None)
733 }
734
735 fn visit_unit<E>(self) -> Result<Self::Value, E>
736 where
737 E: serde::de::Error,
738 {
739 Ok(None)
740 }
741}
742
743#[typeshare]
744#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
745pub struct DeploymentActionState {
746 pub deploying: bool,
747 pub starting: bool,
748 pub restarting: bool,
749 pub pausing: bool,
750 pub unpausing: bool,
751 pub stopping: bool,
752 pub removing: bool,
753 pub renaming: bool,
754}
755
756#[typeshare]
757pub type DeploymentQuery = ResourceQuery<DeploymentQuerySpecifics>;
758
759#[typeshare]
760#[derive(
761 Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder,
762)]
763pub struct DeploymentQuerySpecifics {
764 #[serde(default)]
765 pub server_ids: Vec<String>,
766
767 #[serde(default)]
768 pub build_ids: Vec<String>,
769}
770
771impl super::resource::AddFilters for DeploymentQuerySpecifics {
772 fn add_filters(&self, filters: &mut Document) {
773 if !self.server_ids.is_empty() {
774 filters
775 .insert("config.server_id", doc! { "$in": &self.server_ids });
776 }
777 if !self.build_ids.is_empty() {
778 filters.insert("config.image.type", "Build");
779 filters.insert(
780 "config.image.params.build_id",
781 doc! { "$in": &self.build_ids },
782 );
783 }
784 }
785}
786
787pub fn extract_registry_domain(
788 image_name: &str,
789) -> anyhow::Result<String> {
790 let mut split = image_name.split('/');
791 let maybe_domain =
792 split.next().context("image name cannot be empty string")?;
793 if maybe_domain.contains('.') {
794 Ok(maybe_domain.to_string())
795 } else {
796 Ok(String::from("docker.io"))
797 }
798}