Skip to content

Commit 0d97616

Browse files
committed
refactor: Move all steps into the child workflow
This allows running cleanup when e.g. final VM fails to start. The outer workflow must only contain a call to the child workflow and cleanup in case of error.
1 parent 42a5e77 commit 0d97616

3 files changed

Lines changed: 122 additions & 103 deletions

File tree

workflow/deployer-workflow/impl-flyio/src/lib.rs

Lines changed: 112 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,18 @@ const OBELISK_BIN_PATH: &str = "/obelisk/obelisk";
4444
const REGION: Region = Region::Ams;
4545
const WEBHOOK_PORT: u16 = 9090;
4646

47-
fn app_modify_without_cleanup(
48-
app_name: &str,
49-
config: ObeliskConfig,
50-
) -> Result<Vec<String>, AppInitModifyError> {
51-
// Allocate an IPv6 address first.
47+
fn allocate_ip(app_name: &str) -> Result<(), AppInitModifyError> {
5248
activity_fly_http::ips::allocate(
5349
app_name,
5450
IpRequest {
5551
config: IpVariant::Ipv6(Ipv6Config { region: None }),
5652
},
5753
)
58-
.map_err(AppInitModifyError::IpAllocateError)?;
54+
.map(|_ip| ())
55+
.map_err(AppInitModifyError::IpAllocateError)
56+
}
5957

58+
fn setup_volume(app_name: &str, config: &ObeliskConfig) -> Result<(), AppInitModifyError> {
6059
// Create a volume
6160
activity_fly_http::volumes::create(
6261
app_name,
@@ -124,7 +123,7 @@ fn app_modify_without_cleanup(
124123
}
125124

126125
// Write obelisk.toml
127-
let obelisk_toml = serialize_obelisk_toml(&config);
126+
let obelisk_toml = serialize_obelisk_toml(config);
128127
let exec_response = activity_fly_http::machines::exec(
129128
app_name,
130129
&temp_vm,
@@ -168,11 +167,94 @@ fn app_modify_without_cleanup(
168167
activity_fly_http::machines::delete(app_name, &temp_vm, true)
169168
.map_err(AppInitModifyError::TempVmError)?;
170169

171-
// All OK, return secrets that are needed by the configuration.
172-
Ok(get_secret_keys(config))
170+
Ok(())
171+
}
172+
173+
// Sleep until all requested secrets are stored in the app.
174+
fn wait_for_secrets(
175+
app_name: &str,
176+
required_secrets: HashSet<String>,
177+
sleep_between_retries_seconds: u32,
178+
) -> Result<(), AppInitModifyError> {
179+
while !required_secrets.is_empty() {
180+
let actual_secrets = match activity_fly_http::secrets::list(app_name) {
181+
Ok(actual_secrets) => actual_secrets
182+
.into_iter()
183+
.map(|secret| secret.name)
184+
.collect(),
185+
Err(_) => {
186+
// has the app been deleted?
187+
match activity_fly_http::apps::get(app_name) {
188+
Ok(None) => return Err(AppInitModifyError::AppDeleted),
189+
Ok(Some(_)) | Err(_) => HashSet::new(), // app exists or unknown, keep looping.
190+
}
191+
}
192+
};
193+
if required_secrets.is_subset(&actual_secrets) {
194+
break;
195+
}
196+
workflow_support::sleep(ScheduleAt::In(SchedulingDuration::Seconds(
197+
sleep_between_retries_seconds as u64,
198+
)));
199+
}
200+
Ok(())
201+
}
202+
203+
fn launch_final_vm(app_name: &str) -> Result<(), AppInitModifyError> {
204+
activity_fly_http::machines::create(
205+
app_name,
206+
VM_NAME_FINAL,
207+
&MachineConfig {
208+
image: IMAGE.to_string(),
209+
guest: Some(GuestConfig {
210+
cpu_kind: Some(CpuKind::Shared),
211+
cpus: Some(1),
212+
memory_mb: Some(256),
213+
kernel_args: None,
214+
}),
215+
auto_destroy: None,
216+
init: Some(InitConfig {
217+
cmd: Some(
218+
vec!["server", "run", "--config", "/volume/obelisk.toml"]
219+
.into_iter()
220+
.map(ToString::to_string)
221+
.collect(),
222+
),
223+
entrypoint: None, // defaults to /obelisk/obelisk
224+
exec: None,
225+
kernel_args: None,
226+
swap_size_mb: Some(256),
227+
tty: None,
228+
}),
229+
env: None,
230+
restart: Some(MachineRestart {
231+
max_retries: None,
232+
policy: RestartPolicy::No,
233+
}),
234+
stop_config: None,
235+
mounts: Some(vec![Mount {
236+
volume: VOLUME_NAME.to_string(),
237+
path: VOLUME_MOUNT_PATH.to_string(),
238+
}]),
239+
services: Some(vec![ServiceConfig {
240+
internal_port: WEBHOOK_PORT,
241+
protocol: ServiceProtocol::Tcp,
242+
ports: vec![PortConfig {
243+
port: 443,
244+
handlers: vec![PortHandler::Tls],
245+
}],
246+
}]),
247+
},
248+
Some(REGION),
249+
)
250+
.map(|_| ())
251+
.map_err(AppInitModifyError::FinalVmError)
173252
}
174253

175254
fn cleanup(app_name: &str, modify_error: AppInitModifyError) -> AppInitError {
255+
if matches!(modify_error, AppInitModifyError::AppDeleted) {
256+
return AppInitError::CleanupOk;
257+
}
176258
// Delete the app with force.
177259
match activity_fly_http::apps::delete(app_name, true) {
178260
Ok(()) => AppInitError::CleanupOk,
@@ -201,8 +283,19 @@ impl Guest for Component {
201283
fn app_modify_no_cleanup_on_error(
202284
app_name: String,
203285
config: ObeliskConfig,
204-
) -> Result<Vec<String>, AppInitModifyError> {
205-
app_modify_without_cleanup(&app_name, config)
286+
sleep_between_retries_seconds: u32,
287+
) -> Result<(), AppInitModifyError> {
288+
// Allocate an IPv6 address first.
289+
allocate_ip(&app_name)?;
290+
// Put `obelisk.toml`, downloaded WASM files and codegen cache on a new volume.
291+
setup_volume(&app_name, &config)?;
292+
// Sleep until all requested secrets are stored in the app.
293+
let required_secrets = get_secret_keys(config);
294+
wait_for_secrets(&app_name, required_secrets, sleep_between_retries_seconds)?;
295+
// All preparation is done, start the final VM.
296+
launch_final_vm(&app_name)?;
297+
// TODO Add a healthcheck to the exposed server and loop until success is reached, with configurable max retries. Cleanup on failure.
298+
Ok(())
206299
}
207300

208301
fn app_init(
@@ -212,89 +305,18 @@ impl Guest for Component {
212305
sleep_between_retries_seconds: u32,
213306
) -> Result<(), AppInitError> {
214307
app_create(&org_slug, &app_name)?;
215-
// Launch a child workflow by using import
216-
let required_secrets = workflow_import::app_modify_no_cleanup_on_error(&app_name, &config)
217-
.map_err(|err| cleanup(&app_name, err))?;
218-
// Sleep until all requested secrets are stored in the app.
219-
let required_secrets: HashSet<_> = required_secrets.into_iter().collect();
220-
while !required_secrets.is_empty() {
221-
let actual_secrets = match activity_fly_http::secrets::list(&app_name) {
222-
Ok(actual_secrets) => actual_secrets
223-
.into_iter()
224-
.map(|secret| secret.name)
225-
.collect(),
226-
Err(_) => {
227-
// has the app been deleted?
228-
match activity_fly_http::apps::get(&app_name) {
229-
Ok(None) => return Err(AppInitError::AppDeleted),
230-
Ok(Some(_)) | Err(_) => HashSet::new(), // app exists or unknown, keep looping.
231-
}
232-
}
233-
};
234-
if required_secrets.is_subset(&actual_secrets) {
235-
break;
236-
}
237-
workflow_support::sleep(ScheduleAt::In(SchedulingDuration::Seconds(
238-
sleep_between_retries_seconds as u64,
239-
)));
240-
}
241-
242-
// Launch the final VM
243-
activity_fly_http::machines::create(
308+
// Launch a child workflow by using import.
309+
// In case of any error including a trap (panic), delete the whole app.
310+
workflow_import::app_modify_no_cleanup_on_error(
244311
&app_name,
245-
VM_NAME_FINAL,
246-
&MachineConfig {
247-
image: IMAGE.to_string(),
248-
guest: Some(GuestConfig {
249-
cpu_kind: Some(CpuKind::Shared),
250-
cpus: Some(1),
251-
memory_mb: Some(256),
252-
kernel_args: None,
253-
}),
254-
auto_destroy: None,
255-
init: Some(InitConfig {
256-
cmd: Some(
257-
vec!["server", "run", "--config", "/volume/obelisk.toml"]
258-
.into_iter()
259-
.map(ToString::to_string)
260-
.collect(),
261-
),
262-
entrypoint: None, // defaults to /obelisk/obelisk
263-
exec: None,
264-
kernel_args: None,
265-
swap_size_mb: Some(256),
266-
tty: None,
267-
}),
268-
env: None,
269-
restart: Some(MachineRestart {
270-
max_retries: None,
271-
policy: RestartPolicy::No,
272-
}),
273-
stop_config: None,
274-
mounts: Some(vec![Mount {
275-
volume: VOLUME_NAME.to_string(),
276-
path: VOLUME_MOUNT_PATH.to_string(),
277-
}]),
278-
services: Some(vec![ServiceConfig {
279-
internal_port: WEBHOOK_PORT,
280-
protocol: ServiceProtocol::Tcp,
281-
ports: vec![PortConfig {
282-
port: 443,
283-
handlers: vec![PortHandler::Tls],
284-
}],
285-
}]),
286-
},
287-
Some(REGION),
312+
&config,
313+
sleep_between_retries_seconds,
288314
)
289-
.map_err(AppInitError::FinalVmError)?;
290-
291-
// TODO Add a healthcheck to the exposed server and loop until success is reached, with configurable max retries. Cleanup on failure.
292-
293-
Ok(())
315+
.map_err(|err| cleanup(&app_name, err))
294316
}
295317
}
296318

297-
fn get_secret_keys(config: ObeliskConfig) -> Vec<String> {
319+
fn get_secret_keys(config: ObeliskConfig) -> HashSet<String> {
298320
let a_iter = config
299321
.activity_wasm_list
300322
.into_iter()
@@ -309,8 +331,7 @@ fn get_secret_keys(config: ObeliskConfig) -> Vec<String> {
309331
.flat_map(|component| component.env_vars)
310332
.flatten()
311333
.filter(|env_var| !env_var.contains("="));
312-
let unique_keys: hashbrown::HashSet<_> = a_iter.chain(w_iter).collect();
313-
unique_keys.into_iter().collect()
334+
a_iter.chain(w_iter).collect()
314335
}
315336

316337
// FIXME: Insecure, use proper TOML serializer.

workflow/deployer-workflow/wit/obelisk-flyio_workflow@1.0.0-beta/types.wit

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ interface types {
4444
temp-vm-error(string),
4545
/// Cannot place files on the volume.
4646
volume-write-error(string),
47-
/// Error running `obelisk server verify -i`
47+
/// Error running `obelisk server verify --ignore-missing-env-vars`
4848
verify-error(string),
49+
/// Waiting for secrets was interrupted by deleting the app.
50+
app-deleted,
51+
/// Cannot start the final VM
52+
final-vm-error(string),
4953
/// Trap (panic) during execution
5054
execution-failed,
5155
}
@@ -65,10 +69,7 @@ interface types {
6569
/// App init failed, cleanup failed.
6670
/// The associated value contains the reason of failure, if available.
6771
cleanup-failed(app-cleanup-failed),
68-
/// Wait for secrets can be interrupted by deleting the app.
69-
app-deleted,
70-
/// Cannot start the final VM
71-
final-vm-error(string),
72+
7273
execution-failed,
7374
}
7475
}

workflow/deployer-workflow/wit/obelisk-flyio_workflow@1.0.0-beta/workflow.wit

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ interface workflow {
44
use types.{obelisk-config, app-init-modify-error, app-init-error};
55

66
/// Allocate an IP address.
7-
/// Create a volume.
8-
/// Launch a temporary VM.
9-
/// Store the config file on the volume.
10-
/// Execute `obelisk server verify --ignore-missing-env-vars` to download and verify WASM components.
11-
/// Shutdown and delete the temporary VM.
12-
/// Return list of secret keys the config requires.
7+
/// Create and prepare a volume.
8+
/// Wait until secrets are populated.
139
app-modify-no-cleanup-on-error: func(
1410
app-name: string,
1511
config: obelisk-config,
16-
) -> result<list<string>, app-init-modify-error>;
12+
sleep-between-retries-seconds: u32,
13+
) -> result<_, app-init-modify-error>;
1714

1815
/// Chcek whether the app_name exists. If it does, return app-name-conflict.
1916
/// Create a fly app.

0 commit comments

Comments
 (0)