Skip to content

Commit a9b418d

Browse files
authored
Merge pull request #3361 from itowlson/oci-bare-wasm-packages
Enable running `wkg oci` packages as bare Wasm
2 parents 13a1bef + fd6022f commit a9b418d

File tree

6 files changed

+115
-34
lines changed

6 files changed

+115
-34
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ jobs:
217217
chmod +x grain-linux-x64
218218
mv grain-linux-x64 grain
219219
echo "$PWD" >> $GITHUB_PATH
220+
- name: Install wkg
221+
run: cargo install wkg
220222

221223
- name: Run Full Integration Tests
222224
run: make test-integration-full

crates/oci/src/client.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pub const DATA_MEDIATYPE: &str = "application/vnd.wasm.content.layer.v1+data";
3535
pub const ARCHIVE_MEDIATYPE: &str = "application/vnd.wasm.content.bundle.v1.tar+gzip";
3636
// Note: this will be updated with a canonical value once defined upstream
3737
const WASM_LAYER_MEDIA_TYPE: &str = "application/vnd.wasm.content.layer.v1+wasm";
38+
// Media type for a Wasm binary pushed by wkg
39+
const WASM_LAYER_MEDIA_TYPE_WKG: &str = "application/wasm";
3840

3941
const CONFIG_FILE: &str = "config.json";
4042
const LATEST_TAG: &str = "latest";
@@ -499,7 +501,7 @@ impl Client {
499501
}
500502

501503
/// Pull a Spin application from an OCI registry.
502-
pub async fn pull(&mut self, reference: &str) -> Result<()> {
504+
pub async fn pull(&mut self, reference: &str) -> Result<OciImageManifest> {
503505
let reference: Reference = reference.parse().context("cannot parse reference")?;
504506
let auth = Self::auth(&reference).await?;
505507

@@ -526,7 +528,7 @@ impl Client {
526528

527529
// If a layer is a Wasm module, write it in the Wasm directory.
528530
// Otherwise, write it in the data directory (after unpacking if archive layer)
529-
stream::iter(manifest.layers)
531+
stream::iter(&manifest.layers)
530532
.map(|layer| {
531533
let this = &self;
532534
let reference = reference.clone();
@@ -541,14 +543,14 @@ impl Client {
541543

542544
tracing::debug!("Pulling layer {}", &layer.digest);
543545
let mut bytes = Vec::with_capacity(layer.size.try_into()?);
544-
this.oci.pull_blob(&reference, &layer, &mut bytes).await?;
546+
this.oci.pull_blob(&reference, layer, &mut bytes).await?;
545547
match layer.media_type.as_str() {
546548
SPIN_APPLICATION_MEDIA_TYPE => {
547549
this.write_locked_app_config(&reference.to_string(), &bytes)
548550
.await
549551
.with_context(|| "unable to write locked app config to cache")?;
550552
}
551-
WASM_LAYER_MEDIA_TYPE => {
553+
WASM_LAYER_MEDIA_TYPE | WASM_LAYER_MEDIA_TYPE_WKG => {
552554
this.cache.write_wasm(&bytes, &layer.digest).await?;
553555
}
554556
ARCHIVE_MEDIATYPE => {
@@ -566,7 +568,7 @@ impl Client {
566568
.await?;
567569
tracing::info!("Pulled {}@{}", reference, digest);
568570

569-
Ok(())
571+
Ok(manifest)
570572
}
571573

572574
/// Get the file path to an OCI manifest given a reference.

crates/oci/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub mod utils;
88
mod validate;
99

1010
pub use client::{Client, ComposeMode};
11-
pub use loader::OciLoader;
11+
pub use loader::{ExecutableArtifact, OciLoader};
1212

1313
/// URL scheme used for the locked app "origin" metadata field for OCI-sourced apps.
1414
pub const ORIGIN_URL_SCHEME: &str = "vnd.fermyon.origin-oci";

crates/oci/src/loader.rs

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ pub struct OciLoader {
1414
working_dir: PathBuf,
1515
}
1616

17+
/// The artifact loaded by the `OciLoader`
18+
pub enum ExecutableArtifact {
19+
/// The OCI reference contained a Spin application
20+
Application(LockedApp),
21+
/// The OCI reference contained a Wasm package
22+
Package(PathBuf),
23+
}
24+
1725
impl OciLoader {
1826
/// Creates a new OciLoader which builds temporary mount directory(s) in
1927
/// the given working_dir.
@@ -23,9 +31,13 @@ impl OciLoader {
2331
}
2432

2533
/// Pulls and loads an OCI Artifact and returns a LockedApp with the given OCI client and reference
26-
pub async fn load_app(&self, client: &mut Client, reference: &str) -> Result<LockedApp> {
34+
pub async fn load_app(
35+
&self,
36+
client: &mut Client,
37+
reference: &str,
38+
) -> Result<ExecutableArtifact> {
2739
// Fetch app
28-
client.pull(reference).await.with_context(|| {
40+
let manifest = client.pull(reference).await.with_context(|| {
2941
format!("cannot pull Spin application from registry reference {reference:?}")
3042
})?;
3143

@@ -34,42 +46,58 @@ impl OciLoader {
3446
.lockfile_path(&reference)
3547
.await
3648
.context("cannot get path to spin.lock")?;
37-
self.load_from_cache(lockfile_path, reference, &client.cache)
49+
self.load_from_cache(manifest, lockfile_path, reference, &client.cache)
3850
.await
3951
}
4052

4153
/// Loads an OCI Artifact from the given cache and returns a LockedApp with the given reference
4254
pub async fn load_from_cache(
4355
&self,
56+
manifest: oci_distribution::manifest::OciImageManifest,
4457
lockfile_path: PathBuf,
4558
reference: &str,
4659
cache: &Cache,
47-
) -> std::result::Result<LockedApp, anyhow::Error> {
60+
) -> std::result::Result<ExecutableArtifact, anyhow::Error> {
4861
let locked_content = tokio::fs::read(&lockfile_path)
4962
.await
5063
.with_context(|| format!("failed to read from {}", quoted_path(&lockfile_path)))?;
51-
let mut locked_app = LockedApp::from_json(&locked_content).with_context(|| {
52-
format!(
53-
"failed to decode locked app from {}",
54-
quoted_path(&lockfile_path)
55-
)
56-
})?;
57-
58-
// Update origin metadata
59-
let resolved_reference = Reference::try_from(reference).context("invalid reference")?;
60-
let origin_uri = format!("{ORIGIN_URL_SCHEME}:{resolved_reference}");
61-
locked_app
62-
.metadata
63-
.insert("origin".to_string(), origin_uri.into());
64-
65-
for component in &mut locked_app.components {
66-
self.resolve_component_content_refs(component, cache)
67-
.await
68-
.with_context(|| {
69-
format!("failed to resolve content for component {:?}", component.id)
70-
})?;
64+
let locked_json: serde_json::Value = serde_json::from_slice(&locked_content)
65+
.with_context(|| format!("OCI config {} is not JSON", quoted_path(&lockfile_path)))?;
66+
67+
if locked_json.get("spin_lock_version").is_some() {
68+
let mut locked_app = LockedApp::from_json(&locked_content).with_context(|| {
69+
format!(
70+
"failed to decode locked app from {}",
71+
quoted_path(&lockfile_path)
72+
)
73+
})?;
74+
75+
// Update origin metadata
76+
let resolved_reference = Reference::try_from(reference).context("invalid reference")?;
77+
let origin_uri = format!("{ORIGIN_URL_SCHEME}:{resolved_reference}");
78+
locked_app
79+
.metadata
80+
.insert("origin".to_string(), origin_uri.into());
81+
82+
for component in &mut locked_app.components {
83+
self.resolve_component_content_refs(component, cache)
84+
.await
85+
.with_context(|| {
86+
format!("failed to resolve content for component {:?}", component.id)
87+
})?;
88+
}
89+
Ok(ExecutableArtifact::Application(locked_app))
90+
} else {
91+
if manifest.layers.len() != 1 {
92+
anyhow::bail!(
93+
"expected single layer in OCI package, found {} layers",
94+
manifest.layers.len()
95+
);
96+
}
97+
let layer = &manifest.layers[0]; // guaranteed safe by previous check
98+
let wasm_path = cache.wasm_path(&layer.digest);
99+
Ok(ExecutableArtifact::Package(wasm_path))
71100
}
72-
Ok(locked_app)
73101
}
74102

75103
async fn resolve_component_content_refs(

src/commands/up.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use spin_app::locked::LockedApp;
1515
use spin_common::ui::quoted_path;
1616
use spin_factor_outbound_networking::validate_service_chaining_for_components;
1717
use spin_loader::FilesMountStrategy;
18-
use spin_oci::OciLoader;
18+
use spin_oci::{ExecutableArtifact, OciLoader};
1919
use spin_trigger::cli::{LaunchMetadata, SPIN_LOCAL_APP_DIR, SPIN_LOCKED_URL, SPIN_WORKING_DIR};
2020
use tempfile::TempDir;
2121

@@ -467,10 +467,18 @@ impl UpCommand {
467467
.await
468468
.context("cannot create registry client")?;
469469

470-
let locked_app = OciLoader::new(working_dir)
470+
let artifact = OciLoader::new(working_dir)
471471
.load_app(&mut client, reference)
472472
.await?;
473-
ResolvedAppSource::OciRegistry { locked_app }
473+
474+
match artifact {
475+
ExecutableArtifact::Application(locked_app) => {
476+
ResolvedAppSource::OciRegistry { locked_app }
477+
}
478+
ExecutableArtifact::Package(wasm_path) => {
479+
ResolvedAppSource::BareWasm { wasm_path }
480+
}
481+
}
474482
}
475483
AppSource::BareWasm(path) => ResolvedAppSource::BareWasm {
476484
wasm_path: path.clone(),

tests/integration.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,47 @@ mod integration_tests {
872872
Ok(())
873873
}
874874

875+
#[test]
876+
#[cfg(feature = "extern-dependencies-tests")]
877+
fn registry_bare_wasm_works() -> anyhow::Result<()> {
878+
let services = ServicesConfig::new(vec!["registry"])?;
879+
let spin_up_args = |env: &mut test_environment::TestEnvironment<()>| {
880+
let port = env
881+
.get_port(5000)?
882+
.context("no registry port was exposed by test services")?;
883+
let registry_url =
884+
format!("localhost:{port}/spin-e2e-tests/registry-base-wasm-works/v1");
885+
let mut registry_push = std::process::Command::new("wkg");
886+
registry_push.args([
887+
"oci",
888+
"push",
889+
&registry_url,
890+
"./target/wasm32-wasip1/release/test.wasm",
891+
"--insecure",
892+
]);
893+
registry_push.arg(format!("localhost:{port}"));
894+
env.run_in(&mut registry_push)?;
895+
Ok(vec!["-f".into(), registry_url, "--insecure".into()])
896+
};
897+
let mut env = super::testcases::bootstrap_smoke_test(
898+
services,
899+
None,
900+
&[],
901+
"http-rust",
902+
|_| Ok(Vec::new()),
903+
|_| Ok(()),
904+
HashMap::default(),
905+
spin_up_args,
906+
SpinAppType::Http,
907+
)?;
908+
assert_spin_request(
909+
env.runtime_mut(),
910+
Request::new(Method::Get, "/"),
911+
Response::new_with_body(200, "Hello World!"),
912+
)?;
913+
Ok(())
914+
}
915+
875916
#[test]
876917
fn test_wasi_http_rc_11_10() -> anyhow::Result<()> {
877918
test_wasi_http_rc("wasi-http-0.2.0-rc-2023-11-10")

0 commit comments

Comments
 (0)