monitor_client/entities/
deployment.rs

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  /// The state of the deployment / underlying docker container.
32  pub state: DeploymentState,
33  /// The status of the docker container (eg. up 12 hours, exited 5 minutes ago.)
34  pub status: Option<String>,
35  /// The image attached to the deployment.
36  pub image: String,
37  /// The server that deployment sits on.
38  pub server_id: String,
39  /// An attached monitor build, if it exists.
40  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  /// The id of server the deployment is deployed on.
52  #[serde(default, alias = "server")]
53  #[partial_attr(serde(alias = "server"))]
54  #[builder(default)]
55  pub server_id: String,
56
57  /// The image which the deployment deploys.
58  /// Can either be a user inputted image, or a Monitor build.
59  #[serde(default)]
60  #[builder(default)]
61  pub image: DeploymentImage,
62
63  /// Configure the account used to pull the image from the registry.
64  /// Used with `docker login`.
65  ///
66  ///  - If the field is empty string, will use the same account config as the build, or none at all if using image.
67  ///  - If the field contains an account, a token for the account must be available.
68  ///  - Will get the registry domain from the build / image
69  #[serde(default)]
70  #[builder(default)]
71  pub image_registry_account: String,
72
73  /// Whether to skip secret interpolation into the deployment environment variables.
74  #[serde(default)]
75  #[builder(default)]
76  pub skip_secret_interp: bool,
77
78  /// Whether to redeploy the deployment whenever the attached build finishes.
79  #[serde(default)]
80  #[builder(default)]
81  pub redeploy_on_build: bool,
82
83  /// Whether to send ContainerStateChange alerts for this deployment.
84  #[serde(default = "default_send_alerts")]
85  #[builder(default = "default_send_alerts()")]
86  #[partial_default(default_send_alerts())]
87  pub send_alerts: bool,
88
89  /// The network attached to the container.
90  /// Default is `host`.
91  #[serde(default = "default_network")]
92  #[builder(default = "default_network()")]
93  #[partial_default(default_network())]
94  pub network: String,
95
96  /// The restart mode given to the container.
97  #[serde(default)]
98  #[builder(default)]
99  pub restart: RestartMode,
100
101  /// This is interpolated at the end of the `docker run` command,
102  /// which means they are either passed to the containers inner process,
103  /// or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile.
104  /// Empty is no command.
105  #[serde(default)]
106  #[builder(default)]
107  pub command: String,
108
109  /// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
110  #[serde(default)]
111  #[builder(default)]
112  pub termination_signal: TerminationSignal,
113
114  /// The termination timeout.
115  #[serde(default = "default_termination_timeout")]
116  #[builder(default = "default_termination_timeout()")]
117  #[partial_default(default_termination_timeout())]
118  pub termination_timeout: i32,
119
120  /// Extra args which are interpolated into the `docker run` command,
121  /// and affect the container configuration.
122  #[serde(default)]
123  #[builder(default)]
124  pub extra_args: Vec<String>,
125
126  /// Labels attached to various termination signal options.
127  /// Used to specify different shutdown functionality depending on the termination signal.
128  #[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  /// The container port mapping.
141  /// Irrelevant if container network is `host`.
142  /// Maps ports on host to ports on container.
143  #[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  /// The container volume mapping.
152  /// Maps files / folders on host to files / folders in container.
153  #[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  /// The environment variables passed to the container.
162  #[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  /// The docker labels given to the container.
174  #[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  /// Deploy any external image.
250  Image {
251    /// The docker image, can be from any registry that works with docker and that the host server can reach.
252    #[serde(default)]
253    image: String,
254  },
255
256  /// Deploy a monitor build.
257  Build {
258    /// The id of the build
259    #[serde(default, alias = "build")]
260    build_id: String,
261    /// Use a custom / older version of the image produced by the build.
262    /// if version is 0.0.0, this means `latest` image.
263    #[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  /// reference on the server.
282  pub local: String,
283  /// reference in the container.
284  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/// A summary of a docker container on a server.
435#[typeshare]
436#[derive(Serialize, Deserialize, Debug, Clone)]
437pub struct ContainerSummary {
438  /// Name of the container.
439  pub name: String,
440  /// Id of the container.
441  pub id: String,
442  /// The image the container is based on.
443  pub image: String,
444  /// The docker labels on the container.
445  pub labels: HashMap<String, String>,
446  /// The state of the container, like `running` or `not_deployed`
447  pub state: DeploymentState,
448  /// The status string of the docker container.
449  pub status: Option<String>,
450  /// The network mode of the container.
451  pub network_mode: Option<String>,
452  /// Network names attached to the container
453  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/// Variants de/serialized from/to snake_case.
476///
477/// Eg.
478/// - NotDeployed -> not_deployed
479/// - Restarting -> restarting
480/// - Running -> running.
481#[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}