shuttle_common/models/
deployment.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use strum::{Display, EnumString};
6
7#[cfg(feature = "display")]
8use crossterm::style::Stylize;
9
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Display, Serialize, EnumString)]
11#[serde(rename_all = "lowercase")]
12#[strum(serialize_all = "lowercase")]
13#[strum(ascii_case_insensitive)]
14#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
15#[typeshare::typeshare]
16pub enum DeploymentState {
17    Pending,
18    Building,
19    Running,
20    #[strum(serialize = "in progress")]
21    InProgress,
22    Stopped,
23    Stopping,
24    Failed,
25
26    /// Forward compatibility
27    #[cfg(feature = "unknown-variants")]
28    #[doc(hidden)]
29    #[typeshare(skip)]
30    #[serde(untagged, skip_serializing)]
31    #[strum(default, to_string = "Unknown: {0}")]
32    Unknown(String),
33}
34
35impl DeploymentState {
36    #[cfg(feature = "display")]
37    pub fn get_color_crossterm(&self) -> crossterm::style::Color {
38        use crossterm::style::Color;
39
40        match self {
41            Self::Pending => Color::DarkYellow,
42            Self::Building => Color::Yellow,
43            Self::InProgress => Color::Cyan,
44            Self::Running => Color::Green,
45            Self::Stopped => Color::DarkBlue,
46            Self::Stopping => Color::Blue,
47            Self::Failed => Color::Red,
48            #[cfg(feature = "unknown-variants")]
49            Self::Unknown(_) => Color::Grey,
50        }
51    }
52    #[cfg(all(feature = "tables", feature = "display"))]
53    pub fn get_color_comfy_table(&self) -> comfy_table::Color {
54        use comfy_table::Color;
55
56        match self {
57            Self::Pending => Color::DarkYellow,
58            Self::Building => Color::Yellow,
59            Self::InProgress => Color::Cyan,
60            Self::Running => Color::Green,
61            Self::Stopped => Color::DarkBlue,
62            Self::Stopping => Color::Blue,
63            Self::Failed => Color::Red,
64            #[cfg(feature = "unknown-variants")]
65            Self::Unknown(_) => Color::Grey,
66        }
67    }
68    #[cfg(feature = "display")]
69    pub fn to_string_colored(&self) -> String {
70        self.to_string()
71            .with(self.get_color_crossterm())
72            .to_string()
73    }
74}
75
76#[derive(Deserialize, Serialize)]
77#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
78#[typeshare::typeshare]
79pub struct DeploymentListResponse {
80    pub deployments: Vec<DeploymentResponse>,
81}
82
83#[derive(Deserialize, Serialize)]
84#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
85#[typeshare::typeshare]
86pub struct DeploymentResponse {
87    pub id: String,
88    pub state: DeploymentState,
89    pub created_at: DateTime<Utc>,
90    pub updated_at: DateTime<Utc>,
91    /// URIs where this deployment can currently be reached (only relevant for Running state)
92    pub uris: Vec<String>,
93    pub build_id: Option<String>,
94    pub build_meta: Option<BuildMeta>,
95}
96
97#[cfg(feature = "display")]
98impl DeploymentResponse {
99    pub fn to_string_summary_colored(&self) -> String {
100        // TODO: make this look nicer
101        format!(
102            "Deployment {} - {}",
103            self.id.as_str().bold(),
104            self.state.to_string_colored(),
105        )
106    }
107    pub fn to_string_colored(&self) -> String {
108        // TODO: make this look nicer
109        format!(
110            "Deployment {} - {}\n{}",
111            self.id.as_str().bold(),
112            self.state.to_string_colored(),
113            self.uris.join("\n"),
114        )
115    }
116}
117
118#[derive(Deserialize, Serialize)]
119#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
120#[typeshare::typeshare]
121pub struct UploadArchiveResponse {
122    /// The S3 object version ID of the uploaded object
123    pub archive_version_id: String,
124}
125
126#[derive(Deserialize, Serialize)]
127#[serde(tag = "type", content = "content")]
128#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
129#[typeshare::typeshare]
130pub enum DeploymentRequest {
131    /// Build an image from the source code in an attached zip archive
132    BuildArchive(DeploymentRequestBuildArchive),
133    // TODO?: Add GitRepo(DeploymentRequestGitRepo)
134    /// Use this image directly. Can be used to skip the build step.
135    Image(DeploymentRequestImage),
136    //
137    // No Unknown variant: is a Request type and should only be deserialized on backend
138}
139
140#[derive(Default, Deserialize, Serialize)]
141#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
142#[typeshare::typeshare]
143pub struct DeploymentRequestBuildArchive {
144    /// The S3 object version ID of the archive to use
145    pub archive_version_id: String,
146    pub build_args: Option<BuildArgs>,
147    /// Secrets to add before this deployment.
148    /// TODO: Remove this in favour of a separate secrets uploading action.
149    pub secrets: Option<HashMap<String, String>>,
150    pub build_meta: Option<BuildMeta>,
151}
152
153#[derive(Deserialize, Serialize)]
154#[serde(tag = "type", content = "content")]
155#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
156#[typeshare::typeshare]
157pub enum BuildArgs {
158    Rust(BuildArgsRust),
159    //
160    // No Unknown variant: is a Request type and should only be deserialized on backend
161}
162
163#[derive(Deserialize, Serialize)]
164#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
165#[typeshare::typeshare]
166pub struct BuildArgsRust {
167    /// Version of shuttle-runtime used by this crate
168    pub shuttle_runtime_version: Option<String>,
169    /// Use the built in cargo chef setup for caching
170    pub cargo_chef: bool,
171    /// Build with the built in `cargo build` setup
172    pub cargo_build: bool,
173    /// The cargo package name to compile
174    pub package_name: Option<String>,
175    /// The cargo binary name to compile
176    pub binary_name: Option<String>,
177    /// comma-separated list of features to activate
178    pub features: Option<String>,
179    /// Passed on to `cargo build`
180    pub no_default_features: bool,
181    /// Use the mold linker
182    pub mold: bool,
183}
184
185impl Default for BuildArgsRust {
186    fn default() -> Self {
187        Self {
188            shuttle_runtime_version: Default::default(),
189            cargo_chef: true,
190            cargo_build: true,
191            package_name: Default::default(),
192            binary_name: Default::default(),
193            features: Default::default(),
194            no_default_features: Default::default(),
195            mold: Default::default(),
196        }
197    }
198}
199
200/// Max length of strings in the git metadata
201pub const GIT_STRINGS_MAX_LENGTH: usize = 80;
202
203#[derive(Default, Deserialize, Serialize)]
204#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
205#[typeshare::typeshare]
206pub struct BuildMeta {
207    pub git_commit_id: Option<String>,
208    pub git_commit_msg: Option<String>,
209    pub git_branch: Option<String>,
210    pub git_dirty: Option<bool>,
211}
212
213#[cfg(feature = "display")]
214impl std::fmt::Display for BuildMeta {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        if let Some(true) = self.git_dirty {
217            write!(f, "(dirty) ")?;
218        }
219        if let Some(ref c) = self.git_commit_id {
220            write!(f, "[{}] ", c.chars().take(7).collect::<String>())?;
221        }
222        if let Some(ref m) = self.git_commit_msg {
223            write!(f, "{m}")?;
224        }
225
226        Ok(())
227    }
228}
229
230#[derive(Default, Deserialize, Serialize)]
231#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
232#[typeshare::typeshare]
233pub struct DeploymentRequestImage {
234    pub image: String,
235    /// TODO: Remove this in favour of a separate secrets uploading action.
236    pub secrets: Option<HashMap<String, String>>,
237    // TODO: credentials fields for private repos??
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct DeploymentMetadata {
242    pub env: Environment,
243    pub project_name: String,
244    /// Path to a folder that persists between deployments
245    pub storage_path: PathBuf,
246}
247
248/// The environment this project is running in
249#[derive(
250    Clone, Copy, Debug, Default, Display, EnumString, PartialEq, Eq, Serialize, Deserialize,
251)]
252#[serde(rename_all = "lowercase")]
253#[strum(serialize_all = "lowercase")]
254pub enum Environment {
255    #[default]
256    Local,
257    #[strum(serialize = "production")] // Keep this around for a while for backward compat
258    Deployment,
259    //
260    // No Unknown variant: is not deserialized in user facing libraries (just FromStr parsed)
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use std::str::FromStr;
267
268    #[test]
269    fn deployment_state_from_and_to_str() {
270        assert_eq!(
271            DeploymentState::Building,
272            DeploymentState::from_str("Building").unwrap()
273        );
274        assert_eq!(
275            DeploymentState::Building,
276            DeploymentState::from_str("BuilDing").unwrap()
277        );
278        assert_eq!(
279            DeploymentState::Building,
280            DeploymentState::from_str("building").unwrap()
281        );
282        assert_eq!(
283            DeploymentState::Building.to_string(),
284            "building".to_string()
285        );
286    }
287
288    #[cfg(feature = "unknown-variants")]
289    #[test]
290    fn unknown_state() {
291        assert_eq!(
292            DeploymentState::Unknown("flying".to_string()),
293            DeploymentState::from_str("flying").unwrap()
294        );
295        assert_eq!(
296            DeploymentState::Unknown("flying".to_string()).to_string(),
297            "Unknown: flying".to_string()
298        );
299    }
300
301    #[test]
302    fn env_from_str() {
303        assert_eq!(Environment::Local, Environment::from_str("local").unwrap());
304        assert_eq!(
305            Environment::Deployment,
306            Environment::from_str("production").unwrap()
307        );
308        assert!(Environment::from_str("somewhere_else").is_err());
309        assert_eq!(format!("{:?}", Environment::Local), "Local".to_owned());
310        assert_eq!(format!("{}", Environment::Local), "local".to_owned());
311        assert_eq!(Environment::Local.to_string(), "local".to_owned());
312    }
313}