Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 3 additions & 20 deletions src/api/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ use crate::api::Api;
use crate::commands;
use crate::commands::CommandError;

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AssetSignature {
pub(crate) signature: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AssetProcessingStatus {
pub(crate) status: String,
Expand All @@ -16,28 +12,15 @@ pub struct AssetProcessingStatus {
}

impl Api {
pub fn get_version_asset_signatures(
&self,
app_id: &str,
revision: u32,
) -> Result<Vec<AssetSignature>, CommandError> {
Ok(serde_json::from_value(commands::get(
&self.authentication,
&format!(
"v4/assets?select=signature&app_id=eq.{app_id}&app_revision=eq.{revision}&type=eq.edge-app-file"
),
)?)?)
}

pub fn get_processing_statuses(
&self,
app_id: &str,
revision: u32,
asset_ids: &[String],
) -> Result<Vec<AssetProcessingStatus>, CommandError> {
let response = commands::get(
&self.authentication,
&format!(
"v4/assets?select=status,processing_error,title&app_id=eq.{app_id}&app_revision=eq.{revision}&status=neq.finished"
"v4/assets?select=status,processing_error,title&id=in.({})&status=neq.finished",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, but worth catching before a large app hits it: this URL now scales with the number of files uploaded.

Each ULID is ~27 bytes with its separator, so ~300 uploaded files puts the request line past the usual 8 KB header limit and the poll starts 414ing — mid-deploy, after the upload has already succeeded. The first deploy of a large app is exactly the case where every file is missing and every id lands in this list, so it's reachable in one shot rather than needing an unlucky sequence.

Chunking the ids inside the poll loop covers it — the statuses from each batch just concatenate, and the failed / pending_count logic above doesn't care that they arrived in pieces.

asset_ids.join(",")
),
)?;

Expand Down
11 changes: 1 addition & 10 deletions src/api/edge_app/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::json;

use crate::api::Api;
use crate::commands;
Expand Down Expand Up @@ -81,12 +80,4 @@ impl Api {
Ok(apps[0].clone())
}
}

pub fn copy_assets(&self, payload: Value) -> Result<Vec<String>, CommandError> {
let response = commands::post(&self.authentication, "v4/edge-apps/copy-assets", &payload)?;
let copied_assets = serde_json::from_value::<Vec<String>>(response)?;

debug!("Copied assets: {copied_assets:?}");
Ok(copied_assets)
}
}
42 changes: 0 additions & 42 deletions src/api/edge_app/channel.rs

This file was deleted.

171 changes: 171 additions & 0 deletions src/api/edge_app/deploy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use std::collections::HashMap;
use std::fmt;
use std::time::Duration;

use log::debug;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::api::Api;
use crate::commands::CommandError;

const DEPLOY_TIMEOUT_SECONDS: u64 = 60;

#[derive(Debug, Serialize)]
pub struct DeployPayload {
pub manifest: Value,
pub file_tree: HashMap<String, String>,
pub delete_missing_settings: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct FailedFile {
pub path: String,
pub error: String,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct OutstandingFiles {
#[serde(default)]
pub missing: Vec<String>,
#[serde(default)]
pub pending: Vec<String>,
#[serde(default)]
pub failed: Vec<FailedFile>,
}

pub fn describe_failed_files(files: &[FailedFile]) -> String {
files
.iter()
.map(|file| format!("{}: {}", file.path, file.error))
.collect::<Vec<_>>()
.join("; ")
}

impl fmt::Display for OutstandingFiles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if !self.missing.is_empty() {
parts.push(format!("not uploaded: {}", self.missing.join(", ")));
}
if !self.pending.is_empty() {
parts.push(format!("still processing: {}", self.pending.join(", ")));
}
if !self.failed.is_empty() {
parts.push(format!("failed: {}", describe_failed_files(&self.failed)));
}
write!(f, "{}", parts.join("; "))
}
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct SettingsDiff {
#[serde(default)]
pub create: Vec<String>,
#[serde(default)]
pub update: Vec<String>,
#[serde(default)]
pub delete: Vec<String>,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct DeployDiff {
#[serde(default)]
pub settings: SettingsDiff,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct DeployPreview {
pub deploy_needed: bool,
#[serde(default)]
pub outstanding: OutstandingFiles,
#[serde(default)]
pub diff: DeployDiff,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct DeployResult {
pub revision: u32,
pub created: bool,
#[serde(default)]
pub published: bool,
#[serde(default)]
pub channel: String,
}

impl Api {
pub fn deploy_preview(
&self,
app_id: &str,
payload: &DeployPayload,
) -> Result<DeployPreview, CommandError> {
let (status, body) = self.post_deploy(app_id, "deploy/preview", payload)?;
if status != StatusCode::OK {
return Err(CommandError::WrongResponseStatus(status.as_u16()));
}

Ok(serde_json::from_value(body)?)
}

pub fn deploy(
&self,
app_id: &str,
payload: &DeployPayload,
) -> Result<DeployResult, CommandError> {
#[derive(Deserialize)]
struct Conflict {
#[serde(default)]
outstanding: OutstandingFiles,
}

let (status, body) = self.post_deploy(app_id, "deploy", payload)?;
match status {
StatusCode::OK => Ok(serde_json::from_value(body)?),
StatusCode::CONFLICT => {
let conflict: Conflict = serde_json::from_value(body)?;
Err(CommandError::DeployRejected(
conflict.outstanding.to_string(),
))
}
_ => Err(CommandError::WrongResponseStatus(status.as_u16())),
}
}

fn post_deploy(
&self,
app_id: &str,
endpoint: &str,
payload: &DeployPayload,
) -> Result<(StatusCode, Value), CommandError> {
let url = format!(
"{}/v3/edge-apps/{app_id}/{endpoint}",
&self.authentication.config.url

Check warning on line 143 in src/api/edge_app/deploy.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant reference in `format!` argument

warning: redundant reference in `format!` argument --> src/api/edge_app/deploy.rs:143:13 | 143 | &self.authentication.config.url | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: remove the redundant `&`: `self.authentication.config.url` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.97.0/index.html#useless_borrows_in_formatting = note: `#[warn(clippy::useless_borrows_in_formatting)]` on by default
);
debug!("POST {url}");

let response = self
.authentication
.build_client()?
.post(&url)
.timeout(Duration::from_secs(DEPLOY_TIMEOUT_SECONDS))
.json(payload)
.send()?;

let status = response.status();
debug!("POST {url} -> {status}");

match status {
StatusCode::OK | StatusCode::CONFLICT => {
Ok((status, serde_json::from_str(&response.text()?)?))
}
StatusCode::NOT_FOUND => Err(CommandError::AppNotFound(format!(
"Edge App with ID '{app_id}' not found."
))),
_ => {
debug!("Response: {:?}", &response.text()?);

Check warning on line 166 in src/api/edge_app/deploy.rs

View workflow job for this annotation

GitHub Actions / clippy

redundant reference in `debug!` argument

warning: redundant reference in `debug!` argument --> src/api/edge_app/deploy.rs:166:42 | 166 | debug!("Response: {:?}", &response.text()?); | ^^^^^^^^^^^^^^^^^ help: remove the redundant `&`: `response.text()?` | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.97.0/index.html#useless_borrows_in_formatting
Err(CommandError::WrongResponseStatus(status.as_u16()))
}
}
}
}
3 changes: 1 addition & 2 deletions src/api/edge_app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod app;
pub mod channel;
pub mod deploy;
pub mod installation;
pub mod setting;
pub mod version;
Loading
Loading