1use std::time::Duration;
2
3use anyhow::Context as _;
4use reqwest;
5use thiserror::Error;
6use wasmer_backend_api::{
7 WasmerClient,
8 types::{DeployApp, DeployAppVersion, PublishDeployAppVars},
9};
10use wasmer_config::{app::AppConfigV1, package::PackageSource};
11
12use crate::package::publish::{
13 PackagePublishError, PublishOptions, PublishProgress, PublishWait, publish_package_directory,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum WaitMode {
19 Deployed,
21 Reachable,
23}
24
25#[derive(Debug, Clone)]
27pub enum DeployProgress {
28 Publishing(PublishProgress),
29 Deploying,
30 Waiting(WaitMode),
31}
32
33#[derive(Debug, Clone)]
35pub struct DeployOptions {
36 pub owner: Option<String>,
37 pub make_default: bool,
38 pub wait: WaitMode,
39 pub publish_package: bool,
40 pub package_namespace: Option<String>,
41 pub publish_timeout: Duration,
42}
43
44impl Default for DeployOptions {
45 fn default() -> Self {
46 Self {
47 owner: None,
48 make_default: true,
49 wait: WaitMode::Reachable,
50 publish_package: true,
51 package_namespace: None,
52 publish_timeout: Duration::from_secs(60 * 5),
53 }
54 }
55}
56
57#[derive(Debug, Error)]
59#[non_exhaustive]
60pub enum DeployError {
61 #[error("missing owner in configuration or options")]
62 MissingOwner,
63 #[error("missing app name in configuration")]
64 MissingName,
65 #[error("package publish error: {0}")]
66 Publish(#[from] PackagePublishError),
67 #[error("yaml error: {0}")]
68 Yaml(#[from] serde_yaml::Error),
69 #[error("backend API error: {0}")]
70 Api(#[from] anyhow::Error),
71 #[error("app http probe failed: {message}")]
72 AppHttpCheck { message: String },
73}
74
75pub async fn deploy_app<F>(
77 client: &WasmerClient,
78 mut config: AppConfigV1,
79 opts: DeployOptions,
80 mut progress: F,
81) -> Result<(DeployApp, DeployAppVersion), DeployError>
82where
83 F: FnMut(DeployProgress) + Send,
84{
85 let owner = opts
86 .owner
87 .clone()
88 .or_else(|| config.owner.clone())
89 .ok_or(DeployError::MissingOwner)?;
90
91 if opts.publish_package {
92 if let PackageSource::Path(ref path) = config.package {
93 let publish_opts = PublishOptions {
94 namespace: Some(
95 opts.package_namespace
96 .clone()
97 .unwrap_or_else(|| owner.clone()),
98 ),
99 timeout: opts.publish_timeout,
100 wait: PublishWait::Container,
101 ..Default::default()
102 };
103 let ident = publish_package_directory(client, path.as_ref(), publish_opts, |e| {
104 progress(DeployProgress::Publishing(e));
105 })
106 .await?;
107 config.package = ident.into();
108 }
109 }
110
111 let name = config.name.clone().ok_or(DeployError::MissingName)?;
112
113 let config_value = config.clone().to_yaml_value()?;
114 let raw_config = serde_yaml::to_string(&config_value)?.trim().to_string() + "\n";
115
116 progress(DeployProgress::Deploying);
117 let version = wasmer_backend_api::query::publish_deploy_app(
118 client,
119 PublishDeployAppVars {
120 config: raw_config,
121 name: name.clone().into(),
122 owner: Some(owner.clone().into()),
123 make_default: Some(opts.make_default),
124 },
125 )
126 .await?;
127
128 progress(DeployProgress::Waiting(opts.wait));
129 wait_app(client, &version, opts.wait, opts.make_default).await
130}
131
132async fn wait_app(
133 client: &WasmerClient,
134 version: &DeployAppVersion,
135 wait: WaitMode,
136 make_default: bool,
137) -> Result<(DeployApp, DeployAppVersion), DeployError> {
138 const PROBE_TIMEOUT: Duration = Duration::from_secs(60 * 5);
139 use wasmer_config::app::HEADER_APP_VERSION_ID;
140
141 let app_id = version
142 .app
143 .as_ref()
144 .ok_or_else(|| DeployError::Api(anyhow::anyhow!("app field empty")))?
145 .id
146 .inner()
147 .to_string();
148
149 let app = wasmer_backend_api::query::get_app_by_id(client, app_id.clone())
150 .await
151 .map_err(DeployError::Api)?;
152
153 match wait {
154 WaitMode::Deployed => {}
155 WaitMode::Reachable => {
156 tokio::time::sleep(Duration::from_secs(2)).await;
157 let check_url = if make_default { &app.url } else { &version.url };
158 let http = reqwest::Client::builder()
159 .connect_timeout(Duration::from_secs(10))
160 .timeout(Duration::from_secs(90))
161 .redirect(reqwest::redirect::Policy::none())
162 .build()
163 .context("failed to build HTTP client")?;
164 let start = tokio::time::Instant::now();
165 let mut sleep_ms = 1_000u64;
166 loop {
167 if start.elapsed() > Duration::from_secs(60 * 5) {
168 return Err(DeployError::AppHttpCheck {
169 message: format!(
170 "timed out waiting for app version '{}' to become reachable at '{}' (tried for {} seconds)",
171 version.id.inner(),
172 check_url,
173 PROBE_TIMEOUT.as_secs(),
174 ),
175 });
176 }
177 if let Ok(res) = http.get(check_url).send().await {
178 let header = res
179 .headers()
180 .get(HEADER_APP_VERSION_ID)
181 .and_then(|v| v.to_str().ok())
182 .unwrap_or("");
183 if header == version.id.inner() {
184 break;
185 }
186 }
187 let to_sleep = Duration::from_millis(sleep_ms);
188 tokio::time::sleep(to_sleep).await;
189 sleep_ms = (sleep_ms * 2).min(10_000);
190 }
191 }
192 }
193
194 Ok((app, version.clone()))
195}