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