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