Skip to content

Commit e3cccea

Browse files
committed
up: Split app resolution into two stages
First we "resolve" the AppSource into a ResolvedAppSource. This step isn't expected to "load" an app fully, e.g. copying static assets. At this stage we have enough information to determine the trigger executor, which lets us serve `--help`. Then we "load" the ResolvedAppSource into a LockedApp as before. Note that this change is basically a no-op for OCI apps as the changes to split these stages are incompatible with the rest of the OCI code today. Signed-off-by: Lann Martin <[email protected]>
1 parent 676709d commit e3cccea

File tree

2 files changed

+107
-53
lines changed

2 files changed

+107
-53
lines changed

src/commands/up.rs

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
mod app_source;
22

33
use std::{
4-
collections::HashSet,
54
ffi::OsString,
65
fmt::Debug,
76
path::{Path, PathBuf},
87
};
98

10-
use anyhow::{anyhow, bail, ensure, Context, Result};
9+
use anyhow::{anyhow, bail, Context, Result};
1110
use clap::{CommandFactory, Parser};
1211
use reqwest::Url;
1312
use spin_app::locked::LockedApp;
@@ -18,7 +17,7 @@ use tempfile::TempDir;
1817

1918
use crate::opts::*;
2019

21-
use self::app_source::AppSource;
20+
use self::app_source::{AppSource, ResolvedAppSource};
2221

2322
const APPLICATION_OPT: &str = "APPLICATION";
2423

@@ -124,7 +123,7 @@ impl UpCommand {
124123
}
125124

126125
async fn run_inner(self) -> Result<()> {
127-
let app_source = self.resolve_app_source();
126+
let app_source = self.app_source();
128127

129128
if app_source == AppSource::None {
130129
if self.help {
@@ -148,20 +147,19 @@ impl UpCommand {
148147
.canonicalize()
149148
.context("Could not canonicalize working directory")?;
150149

151-
let mut locked_app = match &app_source {
152-
AppSource::None => bail!("Internal error - should have shown help"),
153-
AppSource::File(path) => self.prepare_app_from_file(path, &working_dir).await?,
154-
AppSource::OciRegistry(oci) => self.prepare_app_from_oci(oci, &working_dir).await?,
155-
AppSource::Unresolvable(err) => bail!("{err}"),
156-
};
150+
let resolved_app_source = self.resolve_app_source(&app_source, &working_dir).await?;
157151

158-
let trigger_cmd = trigger_command_from_locked_app(&locked_app)
152+
let trigger_cmd = trigger_command_for_resolved_app_source(&resolved_app_source)
159153
.with_context(|| format!("Couldn't find trigger executor for {app_source}"))?;
160154

161155
if self.help {
162156
return self.run_trigger(trigger_cmd, None).await;
163157
}
164158

159+
let mut locked_app = self
160+
.load_resolved_app_source(resolved_app_source, &working_dir)
161+
.await?;
162+
165163
self.update_locked_app(&mut locked_app);
166164

167165
let local_app_dir = app_source.local_app_dir().map(Into::into);
@@ -244,7 +242,7 @@ impl UpCommand {
244242
}
245243
}
246244

247-
fn resolve_app_source(&self) -> AppSource {
245+
fn app_source(&self) -> AppSource {
248246
match (&self.app_source, &self.file_source, &self.registry_source) {
249247
(None, None, None) => self.default_manifest_or_none(),
250248
(Some(source), None, None) => AppSource::infer_source(source),
@@ -294,29 +292,54 @@ impl UpCommand {
294292
Ok(locked_url)
295293
}
296294

297-
async fn prepare_app_from_file(
295+
// Take the AppSource and do the minimum amount of work necessary to
296+
// be able to resolve the trigger executor.
297+
async fn resolve_app_source(
298298
&self,
299-
manifest_path: &Path,
299+
app_source: &AppSource,
300300
working_dir: &Path,
301-
) -> Result<LockedApp> {
302-
let files_mount_strategy = if self.direct_mounts {
303-
FilesMountStrategy::Direct
304-
} else {
305-
FilesMountStrategy::Copy(working_dir.join("assets"))
306-
};
307-
spin_loader::from_file(manifest_path, files_mount_strategy)
308-
.await
309-
.with_context(|| format!("Failed to load manifest from {manifest_path:?}"))
301+
) -> anyhow::Result<ResolvedAppSource> {
302+
Ok(match &app_source {
303+
AppSource::File(path) => ResolvedAppSource::File {
304+
manifest_path: path.clone(),
305+
manifest: spin_manifest::manifest_from_file(path)?,
306+
},
307+
// TODO: We could make the `--help` experience a little faster if
308+
// we could fetch just the locked app JSON at this stage.
309+
AppSource::OciRegistry(reference) => {
310+
let mut client = spin_oci::Client::new(self.insecure, None)
311+
.await
312+
.context("cannot create registry client")?;
313+
314+
let locked_app = OciLoader::new(working_dir)
315+
.load_app(&mut client, reference)
316+
.await?;
317+
ResolvedAppSource::OciRegistry { locked_app }
318+
}
319+
AppSource::Unresolvable(err) => bail!("{err}"),
320+
AppSource::None => bail!("Internal error - should have shown help"),
321+
})
310322
}
311323

312-
async fn prepare_app_from_oci(&self, reference: &str, working_dir: &Path) -> Result<LockedApp> {
313-
let mut client = spin_oci::Client::new(self.insecure, None)
314-
.await
315-
.context("cannot create registry client")?;
316-
317-
OciLoader::new(working_dir)
318-
.load_app(&mut client, reference)
319-
.await
324+
// Finish preparing a ResolvedAppSource for execution.
325+
async fn load_resolved_app_source(
326+
&self,
327+
resolved: ResolvedAppSource,
328+
working_dir: &Path,
329+
) -> anyhow::Result<LockedApp> {
330+
match resolved {
331+
ResolvedAppSource::File { manifest_path, .. } => {
332+
let files_mount_strategy = if self.direct_mounts {
333+
FilesMountStrategy::Direct
334+
} else {
335+
FilesMountStrategy::Copy(working_dir.join("assets"))
336+
};
337+
spin_loader::from_file(&manifest_path, files_mount_strategy)
338+
.await
339+
.with_context(|| format!("Failed to load manifest from {manifest_path:?}"))
340+
}
341+
ResolvedAppSource::OciRegistry { locked_app } => Ok(locked_app),
342+
}
320343
}
321344

322345
fn update_locked_app(&self, locked_app: &mut LockedApp) {
@@ -395,17 +418,8 @@ fn trigger_command(trigger_type: &str) -> Vec<String> {
395418
vec!["trigger".to_owned(), trigger_type.to_owned()]
396419
}
397420

398-
fn trigger_command_from_locked_app(locked_app: &LockedApp) -> Result<Vec<String>> {
399-
let trigger_type = {
400-
let types = locked_app
401-
.triggers
402-
.iter()
403-
.map(|t| t.trigger_type.as_str())
404-
.collect::<HashSet<_>>();
405-
ensure!(!types.is_empty(), "no triggers in app");
406-
ensure!(types.len() == 1, "multiple trigger types not yet supported");
407-
types.into_iter().next().unwrap()
408-
};
421+
fn trigger_command_for_resolved_app_source(resolved: &ResolvedAppSource) -> Result<Vec<String>> {
422+
let trigger_type = resolved.trigger_type()?;
409423

410424
match trigger_type {
411425
"http" | "redis" => Ok(trigger_command(trigger_type)),
@@ -436,7 +450,7 @@ mod test {
436450
app_source: Some(file.clone()),
437451
..Default::default()
438452
}
439-
.resolve_app_source();
453+
.app_source();
440454

441455
assert_eq!(AppSource::File(PathBuf::from(file)), source);
442456
}
@@ -449,7 +463,7 @@ mod test {
449463
app_source: Some(dir.clone()),
450464
..Default::default()
451465
}
452-
.resolve_app_source();
466+
.app_source();
453467

454468
assert_eq!(
455469
AppSource::File(PathBuf::from(dir).join("spin.toml")),
@@ -465,7 +479,7 @@ mod test {
465479
app_source: Some(file),
466480
..Default::default()
467481
}
468-
.resolve_app_source();
482+
.app_source();
469483

470484
assert!(matches!(source, AppSource::Unresolvable(_)));
471485
}
@@ -478,7 +492,7 @@ mod test {
478492
app_source: Some(file),
479493
..Default::default()
480494
}
481-
.resolve_app_source();
495+
.app_source();
482496

483497
assert!(matches!(source, AppSource::Unresolvable(_)));
484498
}
@@ -491,7 +505,7 @@ mod test {
491505
app_source: Some(dir),
492506
..Default::default()
493507
}
494-
.resolve_app_source();
508+
.app_source();
495509

496510
assert!(matches!(source, AppSource::Unresolvable(_)));
497511
}
@@ -504,7 +518,7 @@ mod test {
504518
app_source: Some(reference.clone()),
505519
..Default::default()
506520
}
507-
.resolve_app_source();
521+
.app_source();
508522

509523
assert_eq!(AppSource::OciRegistry(reference), source);
510524
}
@@ -518,7 +532,7 @@ mod test {
518532
app_source: Some(reference.clone()),
519533
..Default::default()
520534
}
521-
.resolve_app_source();
535+
.app_source();
522536

523537
assert_eq!(AppSource::OciRegistry(reference), source);
524538
}
@@ -531,7 +545,7 @@ mod test {
531545
app_source: Some(garbage),
532546
..Default::default()
533547
}
534-
.resolve_app_source();
548+
.app_source();
535549

536550
// Honestly I feel Unresolvable might be a bit weak sauce for this case
537551
assert!(matches!(source, AppSource::Unresolvable(_)));

src/commands/up/app_source.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
use std::path::{Path, PathBuf};
1+
use std::{
2+
collections::HashSet,
3+
path::{Path, PathBuf},
4+
};
25

6+
use anyhow::ensure;
7+
use spin_locked_app::locked::LockedApp;
8+
use spin_manifest::schema::v2::AppManifest;
9+
10+
/// A source from which an App may be loaded.
311
#[derive(Debug, PartialEq, Eq)]
412
pub enum AppSource {
5-
None,
613
File(PathBuf),
714
OciRegistry(String),
815
Unresolvable(String),
16+
None,
917
}
1018

1119
impl AppSource {
@@ -52,10 +60,42 @@ impl AppSource {
5260
impl std::fmt::Display for AppSource {
5361
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5462
match self {
55-
Self::None => write!(f, "<no source>"),
5663
Self::File(path) => write!(f, "local app {path:?}"),
5764
Self::OciRegistry(reference) => write!(f, "remote app {reference:?}"),
5865
Self::Unresolvable(s) => write!(f, "unknown app source: {s:?}"),
66+
Self::None => write!(f, "<no source>"),
5967
}
6068
}
6169
}
70+
71+
/// This represents a "partially loaded" source which has enough information to
72+
/// dispatch to the correct trigger executor but hasn't (necessarily) gone
73+
/// through full validation / loading yet.
74+
pub enum ResolvedAppSource {
75+
File {
76+
manifest_path: PathBuf,
77+
manifest: AppManifest,
78+
},
79+
OciRegistry {
80+
locked_app: LockedApp,
81+
},
82+
}
83+
84+
impl ResolvedAppSource {
85+
pub fn trigger_type(&self) -> anyhow::Result<&str> {
86+
let types = match self {
87+
ResolvedAppSource::File { manifest, .. } => {
88+
manifest.triggers.keys().collect::<HashSet<_>>()
89+
}
90+
ResolvedAppSource::OciRegistry { locked_app } => locked_app
91+
.triggers
92+
.iter()
93+
.map(|t| &t.trigger_type)
94+
.collect::<HashSet<_>>(),
95+
};
96+
97+
ensure!(!types.is_empty(), "no triggers in app");
98+
ensure!(types.len() == 1, "multiple trigger types not yet supported");
99+
Ok(types.into_iter().next().unwrap())
100+
}
101+
}

0 commit comments

Comments
 (0)