grafbase_local_backend/api/
deploy.rs

1use super::client::create_client;
2use super::consts::{API_URL, PACKAGE_JSON, PROJECT_METADATA_FILE, TAR_CONTENT_TYPE};
3use super::errors::{ApiError, DeployError};
4use super::graphql::mutations::{
5    ArchiveFileSizeLimitExceededError, DailyDeploymentCountLimitExceededError, DeploymentCreate,
6    DeploymentCreateArguments, DeploymentCreateInput, DeploymentCreatePayload,
7};
8use super::types::ProjectMetadata;
9use crate::consts::USER_AGENT;
10use common::consts::GRAFBASE_DIRECTORY_NAME;
11use common::environment::Environment;
12use cynic::http::ReqwestExt;
13use cynic::{Id, MutationBuilder};
14use reqwest::{header, Body, Client};
15use tokio::fs::read_to_string;
16use tokio_util::codec::{BytesCodec, FramedRead};
17use tokio_util::compat::TokioAsyncReadCompatExt;
18
19/// # Errors
20///
21/// See [`ApiError`]
22pub async fn deploy() -> Result<(), ApiError> {
23    let environment = Environment::get();
24
25    let project_metadata_file_path = environment.project_dot_grafbase_path.join(PROJECT_METADATA_FILE);
26
27    match project_metadata_file_path.try_exists() {
28        Ok(true) => {}
29        Ok(false) => return Err(ApiError::UnlinkedProject),
30        Err(error) => return Err(ApiError::ReadProjectMetadataFile(error)),
31    }
32
33    let project_metadata_file = read_to_string(project_metadata_file_path)
34        .await
35        .map_err(ApiError::ReadProjectMetadataFile)?;
36
37    let project_metadata: ProjectMetadata =
38        serde_json::from_str(&project_metadata_file).map_err(|_| ApiError::CorruptProjectMetadataFile)?;
39
40    let (tar_file, tar_file_path) = tempfile::NamedTempFile::new()
41        .map_err(ApiError::CreateTempFile)?
42        .into_parts();
43
44    let tar_file: tokio::fs::File = tar_file.into();
45    let tar_file = tar_file.compat();
46
47    let mut tar = async_tar::Builder::new(tar_file);
48    tar.mode(async_tar::HeaderMode::Deterministic);
49
50    if environment.project_path.join(PACKAGE_JSON).exists() {
51        tar.append_path_with_name(environment.project_path.join(PACKAGE_JSON), PACKAGE_JSON)
52            .await
53            .map_err(ApiError::AppendToArchive)?;
54    }
55
56    tar.append_dir_all(GRAFBASE_DIRECTORY_NAME, &environment.project_grafbase_path)
57        .await
58        .map_err(ApiError::AppendToArchive)?;
59
60    let tar_file = tokio::fs::File::open(&tar_file_path)
61        .await
62        .map_err(ApiError::ReadArchive)?;
63
64    let client = create_client().await?;
65
66    #[allow(clippy::cast_possible_truncation)] // must fit or will be over allowed limit
67    let archive_file_size = tar_file.metadata().await.map_err(ApiError::ReadArchiveMetadata)?.len() as i32;
68
69    let operation = DeploymentCreate::build(DeploymentCreateArguments {
70        input: DeploymentCreateInput {
71            archive_file_size,
72            branch: None,
73            project_id: Id::new(project_metadata.project_id),
74        },
75    });
76
77    let response = client.post(API_URL).run_graphql(operation).await?;
78
79    let payload = response
80        .data
81        .ok_or(ApiError::UnauthorizedOrDeletedUser)?
82        .deployment_create;
83
84    match payload {
85        DeploymentCreatePayload::DeploymentCreateSuccess(payload) => {
86            let framed_tar = FramedRead::new(tar_file, BytesCodec::new());
87            let response = Client::new()
88                .put(payload.presigned_url)
89                .header(header::CONTENT_LENGTH, archive_file_size)
90                .header(header::CONTENT_TYPE, TAR_CONTENT_TYPE)
91                .header(header::USER_AGENT, USER_AGENT)
92                .body(Body::wrap_stream(framed_tar))
93                .send()
94                .await
95                .map_err(|_| ApiError::UploadError)?;
96
97            if !response.status().is_success() {
98                return Err(ApiError::UploadError);
99            }
100
101            Ok(())
102        }
103        DeploymentCreatePayload::ProjectDoesNotExistError(_) => Err(DeployError::ProjectDoesNotExist.into()),
104        DeploymentCreatePayload::ArchiveFileSizeLimitExceededError(ArchiveFileSizeLimitExceededError {
105            limit, ..
106        }) => Err(DeployError::ArchiveFileSizeLimitExceeded { limit }.into()),
107        DeploymentCreatePayload::DailyDeploymentCountLimitExceededError(DailyDeploymentCountLimitExceededError {
108            limit,
109            ..
110        }) => Err(DeployError::DailyDeploymentCountLimitExceeded { limit }.into()),
111        DeploymentCreatePayload::Unknown => Err(DeployError::Unknown.into()),
112    }
113}