shuttle_common/models/
deployment.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
use chrono::{DateTime, Local, SecondsFormat, Utc};
use comfy_table::{
    modifiers::UTF8_ROUND_CORNERS,
    presets::{NOTHING, UTF8_BORDERS_ONLY, UTF8_FULL},
    Attribute, Cell, CellAlignment, Color, ContentArrangement, Table,
};
use crossterm::style::Stylize;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display, str::FromStr};
use uuid::Uuid;

use crate::deployment::{DeploymentStateBeta, State};

/// Max length of strings in the git metadata
pub const GIT_STRINGS_MAX_LENGTH: usize = 80;
/// Max HTTP body size for a deployment POST request
pub const CREATE_SERVICE_BODY_LIMIT: usize = 50_000_000;
const GIT_OPTION_NONE_TEXT: &str = "N/A";

#[derive(Deserialize, Serialize, Debug)]
pub struct Response {
    pub id: Uuid,
    pub service_id: String,
    pub state: State,
    pub last_update: DateTime<Utc>,
    pub git_commit_id: Option<String>,
    pub git_commit_msg: Option<String>,
    pub git_branch: Option<String>,
    pub git_dirty: Option<bool>,
}

#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentListResponseBeta {
    pub deployments: Vec<DeploymentResponseBeta>,
}

#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentResponseBeta {
    pub id: String,
    pub state: DeploymentStateBeta,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    /// URIs where this deployment can currently be reached (only relevant for Running state)
    pub uris: Vec<String>,
    pub build_id: Option<String>,
    pub build_meta: Option<BuildMetaBeta>,
}

impl Display for Response {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{} deployment '{}' is {}",
            self.last_update
                .format("%Y-%m-%dT%H:%M:%SZ")
                .to_string()
                .dim(),
            self.id,
            self.state
                .to_string()
                // Unwrap is safe because Color::from_str returns the color white if the argument is not a Color.
                .with(crossterm::style::Color::from_str(self.state.get_color()).unwrap())
        )
    }
}

impl DeploymentResponseBeta {
    pub fn to_string_summary_colored(&self) -> String {
        // TODO: make this look nicer
        format!(
            "Deployment {} - {}",
            self.id.as_str().bold(),
            self.state.to_string_colored(),
        )
    }
    pub fn to_string_colored(&self) -> String {
        // TODO: make this look nicer
        format!(
            "Deployment {} - {}\n{}",
            self.id.as_str().bold(),
            self.state.to_string_colored(),
            self.uris.join("\n"),
        )
    }
}

impl State {
    /// We return a &str rather than a Color here, since `comfy-table` re-exports
    /// crossterm::style::Color and we depend on both `comfy-table` and `crossterm`
    /// we may end up with two different versions of Color.
    pub fn get_color(&self) -> &str {
        match self {
            State::Queued | State::Building | State::Built | State::Loading => "cyan",
            State::Running => "green",
            State::Completed | State::Stopped => "blue",
            State::Crashed => "red",
            State::Unknown => "yellow",
        }
    }
}

pub fn deployments_table_beta(deployments: &[DeploymentResponseBeta], raw: bool) -> String {
    let mut table = Table::new();
    table
        .load_preset(if raw { NOTHING } else { UTF8_BORDERS_ONLY })
        .set_content_arrangement(ContentArrangement::Disabled)
        .set_header(vec!["Deployment ID", "Status", "Date", "Git revision"]);

    for deploy in deployments.iter() {
        let datetime: DateTime<Local> = DateTime::from(deploy.created_at);
        table.add_row(vec![
            Cell::new(&deploy.id).add_attribute(Attribute::Bold),
            Cell::new(&deploy.state)
                // Unwrap is safe because Color::from_str returns the color white if str is not a Color.
                .fg(Color::from_str(deploy.state.get_color()).unwrap()),
            Cell::new(datetime.to_rfc3339_opts(SecondsFormat::Secs, false)),
            Cell::new(
                deploy
                    .build_meta
                    .as_ref()
                    .map(ToString::to_string)
                    .unwrap_or_default(),
            ),
        ]);
    }

    table.to_string()
}

pub fn get_deployments_table(
    deployments: &[Response],
    service_name: &str,
    page: u32,
    raw: bool,
    page_hint: bool,
) -> String {
    if deployments.is_empty() {
        // The page starts at 1 in the CLI.
        let mut s = if page <= 1 {
            "No deployments are linked to this service\n".to_string()
        } else {
            "No more deployments are linked to this service\n".to_string()
        };
        if !raw {
            s = s.yellow().bold().to_string();
        }

        s
    } else {
        let mut table = Table::new();

        if raw {
            table
                .load_preset(NOTHING)
                .set_content_arrangement(ContentArrangement::Disabled)
                .set_header(vec![
                    Cell::new("Deployment ID").set_alignment(CellAlignment::Left),
                    Cell::new("Status").set_alignment(CellAlignment::Left),
                    Cell::new("Last updated").set_alignment(CellAlignment::Left),
                    Cell::new("Commit ID").set_alignment(CellAlignment::Left),
                    Cell::new("Commit Message").set_alignment(CellAlignment::Left),
                    Cell::new("Branch").set_alignment(CellAlignment::Left),
                    Cell::new("Dirty").set_alignment(CellAlignment::Left),
                ]);
        } else {
            table
                .load_preset(UTF8_FULL)
                .apply_modifier(UTF8_ROUND_CORNERS)
                .set_content_arrangement(ContentArrangement::DynamicFullWidth)
                .set_header(vec![
                    Cell::new("Deployment ID")
                        .set_alignment(CellAlignment::Center)
                        .add_attribute(Attribute::Bold),
                    Cell::new("Status")
                        .set_alignment(CellAlignment::Center)
                        .add_attribute(Attribute::Bold),
                    Cell::new("Last updated")
                        .set_alignment(CellAlignment::Center)
                        .add_attribute(Attribute::Bold),
                    Cell::new("Commit ID")
                        .set_alignment(CellAlignment::Center)
                        .add_attribute(Attribute::Bold),
                    Cell::new("Commit Message")
                        .set_alignment(CellAlignment::Center)
                        .add_attribute(Attribute::Bold),
                    Cell::new("Branch")
                        .set_alignment(CellAlignment::Center)
                        .add_attribute(Attribute::Bold),
                    Cell::new("Dirty")
                        .set_alignment(CellAlignment::Center)
                        .add_attribute(Attribute::Bold),
                ]);
        }

        for deploy in deployments.iter() {
            let truncated_commit_id = deploy
                .git_commit_id
                .as_ref()
                .map_or(String::from(GIT_OPTION_NONE_TEXT), |val| {
                    val.chars().take(7).collect()
                });

            let truncated_commit_msg = deploy
                .git_commit_msg
                .as_ref()
                .map_or(String::from(GIT_OPTION_NONE_TEXT), |val| {
                    val.chars().take(24).collect::<String>()
                });

            if raw {
                table.add_row(vec![
                    Cell::new(deploy.id),
                    Cell::new(&deploy.state),
                    Cell::new(deploy.last_update.format("%Y-%m-%dT%H:%M:%SZ")),
                    Cell::new(truncated_commit_id),
                    Cell::new(truncated_commit_msg),
                    Cell::new(
                        deploy
                            .git_branch
                            .as_ref()
                            .map_or(GIT_OPTION_NONE_TEXT, |val| val as &str),
                    ),
                    Cell::new(
                        deploy
                            .git_dirty
                            .map_or(String::from(GIT_OPTION_NONE_TEXT), |val| val.to_string()),
                    ),
                ]);
            } else {
                table.add_row(vec![
                    Cell::new(deploy.id),
                    Cell::new(&deploy.state)
                        // Unwrap is safe because Color::from_str returns the color white if str is not a Color.
                        .fg(Color::from_str(deploy.state.get_color()).unwrap())
                        .set_alignment(CellAlignment::Center),
                    Cell::new(deploy.last_update.format("%Y-%m-%dT%H:%M:%SZ"))
                        .set_alignment(CellAlignment::Center),
                    Cell::new(truncated_commit_id),
                    Cell::new(truncated_commit_msg),
                    Cell::new(
                        deploy
                            .git_branch
                            .as_ref()
                            .map_or(GIT_OPTION_NONE_TEXT, |val| val as &str),
                    ),
                    Cell::new(
                        deploy
                            .git_dirty
                            .map_or(String::from(GIT_OPTION_NONE_TEXT), |val| val.to_string()),
                    )
                    .set_alignment(CellAlignment::Center),
                ]);
            }
        }

        let formatted_table = format!("\nMost recent deployments for {service_name}\n{table}\n");
        if page_hint {
            format!(
                "{formatted_table}More deployments are available on the next page using `--page {}`\n",
                page + 1
            )
        } else {
            formatted_table
        }
    }
}

#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct UploadArchiveResponseBeta {
    /// The S3 object version ID of the uploaded object
    pub archive_version_id: String,
}

#[derive(Default, Deserialize, Serialize)]
pub struct DeploymentRequest {
    /// Tar archive
    pub data: Vec<u8>,
    pub no_test: bool,
    pub git_commit_id: Option<String>,
    pub git_commit_msg: Option<String>,
    pub git_branch: Option<String>,
    pub git_dirty: Option<bool>,
}

#[derive(Deserialize, Serialize)]
#[serde(tag = "type", content = "content")]
#[typeshare::typeshare]
pub enum DeploymentRequestBeta {
    /// Build an image from the source code in an attached zip archive
    BuildArchive(DeploymentRequestBuildArchiveBeta),
    // TODO?: Add GitRepo(DeploymentRequestGitRepoBeta)
    /// Use this image directly. Can be used to skip the build step.
    Image(DeploymentRequestImageBeta),
}

#[derive(Default, Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentRequestBuildArchiveBeta {
    /// The S3 object version ID of the archive to use
    pub archive_version_id: String,
    pub build_args: Option<BuildArgsBeta>,
    /// Secrets to add before this deployment.
    /// TODO: Remove this in favour of a separate secrets uploading action.
    pub secrets: Option<HashMap<String, String>>,
    pub build_meta: Option<BuildMetaBeta>,
}

#[derive(Deserialize, Serialize, Default)]
#[serde(tag = "type", content = "content")]
#[typeshare::typeshare]
pub enum BuildArgsBeta {
    Rust(BuildArgsRustBeta),
    #[default]
    Unknown,
}

#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct BuildArgsRustBeta {
    /// Version of shuttle-runtime used by this crate
    pub shuttle_runtime_version: Option<String>,
    /// Use the built in cargo chef setup for caching
    pub cargo_chef: bool,
    /// Build with the built in `cargo build` setup
    pub cargo_build: bool,
    /// The cargo package name to compile
    pub package_name: Option<String>,
    /// The cargo binary name to compile
    pub binary_name: Option<String>,
    /// comma-separated list of features to activate
    pub features: Option<String>,
    /// Passed on to `cargo build`
    pub no_default_features: bool,
    /// Use the mold linker
    pub mold: bool,
}

impl Default for BuildArgsRustBeta {
    fn default() -> Self {
        Self {
            shuttle_runtime_version: Default::default(),
            cargo_chef: true,
            cargo_build: true,
            package_name: Default::default(),
            binary_name: Default::default(),
            features: Default::default(),
            no_default_features: Default::default(),
            mold: Default::default(),
        }
    }
}

#[derive(Default, Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct BuildMetaBeta {
    pub git_commit_id: Option<String>,
    pub git_commit_msg: Option<String>,
    pub git_branch: Option<String>,
    pub git_dirty: Option<bool>,
}

impl std::fmt::Display for BuildMetaBeta {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(true) = self.git_dirty {
            write!(f, "(dirty) ")?;
        }
        if let Some(ref c) = self.git_commit_id {
            write!(f, "[{}] ", c.chars().take(7).collect::<String>())?;
        }
        if let Some(ref m) = self.git_commit_msg {
            write!(f, "{m}")?;
        }

        Ok(())
    }
}

#[derive(Default, Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentRequestImageBeta {
    pub image: String,
    /// TODO: Remove this in favour of a separate secrets uploading action.
    pub secrets: Option<HashMap<String, String>>,
    // TODO: credentials fields for private repos??
}