Skip to content

Commit 895614d

Browse files
committed
feat(push): allow pushing composed components to registry
Signed-off-by: Brian H <[email protected]>
1 parent 29ba553 commit 895614d

File tree

4 files changed

+130
-35
lines changed

4 files changed

+130
-35
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oci/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ edition = { workspace = true }
88
anyhow = { workspace = true }
99
async-compression = { version = "0.4", features = ["gzip", "tokio"] }
1010
async-tar = "0.5"
11+
async-trait = { workspace = true }
1112
base64 = "0.22"
1213
chrono = "0.4"
1314
# Fork with updated auth to support ACR login
@@ -22,6 +23,8 @@ reqwest = "0.12"
2223
serde = { workspace = true }
2324
serde_json = { workspace = true }
2425
spin-common = { path = "../common" }
26+
spin-componentize = { path = "../componentize" }
27+
spin-compose = { path = "../compose" }
2528
spin-loader = { path = "../loader" }
2629
spin-locked-app = { path = "../locked-app" }
2730
tempfile = { workspace = true }

crates/oci/src/client.rs

Lines changed: 112 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::collections::{BTreeMap, HashMap};
44
use std::path::{Path, PathBuf};
55

66
use anyhow::{bail, Context, Result};
7+
use async_trait::async_trait;
78
use docker_credential::DockerCredential;
89
use futures_util::future;
910
use futures_util::stream::{self, StreamExt, TryStreamExt};
@@ -18,7 +19,7 @@ use spin_common::ui::quoted_path;
1819
use spin_common::url::parse_file_url;
1920
use spin_loader::cache::Cache;
2021
use spin_loader::FilesMountStrategy;
21-
use spin_locked_app::locked::{ContentPath, ContentRef, LockedApp};
22+
use spin_locked_app::locked::{ContentPath, ContentRef, LockedApp, LockedComponentSource};
2223
use tokio::fs;
2324
use walkdir::WalkDir;
2425

@@ -67,6 +68,16 @@ enum AssemblyMode {
6768
Archive,
6869
}
6970

71+
/// Indicates whether to compose the components of a Spin application when pushing an image.
72+
#[derive(Default)]
73+
enum ComposeMode {
74+
/// Compose components before pushing the image.
75+
#[default]
76+
All,
77+
/// Skip composing components before pushing the image.
78+
Skip,
79+
}
80+
7081
/// Client for interacting with an OCI registry for Spin applications.
7182
pub struct Client {
7283
/// Global cache for the metadata, Wasm modules, and static assets pulled from OCI registries.
@@ -119,6 +130,7 @@ impl Client {
119130
reference: impl AsRef<str>,
120131
annotations: Option<BTreeMap<String, String>>,
121132
infer_annotations: InferPredefinedAnnotations,
133+
compose_mode: ComposeMode,
122134
) -> Result<Option<String>> {
123135
let reference: Reference = reference
124136
.as_ref()
@@ -137,8 +149,15 @@ impl Client {
137149
)
138150
.await?;
139151

140-
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
141-
.await
152+
self.push_locked_core(
153+
locked,
154+
auth,
155+
reference,
156+
annotations,
157+
infer_annotations,
158+
compose_mode,
159+
)
160+
.await
142161
}
143162

144163
/// Push a Spin application to an OCI registry and return the digest (or None
@@ -149,15 +168,23 @@ impl Client {
149168
reference: impl AsRef<str>,
150169
annotations: Option<BTreeMap<String, String>>,
151170
infer_annotations: InferPredefinedAnnotations,
171+
compose_mode: ComposeMode,
152172
) -> Result<Option<String>> {
153173
let reference: Reference = reference
154174
.as_ref()
155175
.parse()
156176
.with_context(|| format!("cannot parse reference {}", reference.as_ref()))?;
157177
let auth = Self::auth(&reference).await?;
158178

159-
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
160-
.await
179+
self.push_locked_core(
180+
locked,
181+
auth,
182+
reference,
183+
annotations,
184+
infer_annotations,
185+
compose_mode,
186+
)
187+
.await
161188
}
162189

163190
/// Push a Spin application to an OCI registry and return the digest (or None
@@ -169,10 +196,11 @@ impl Client {
169196
reference: Reference,
170197
annotations: Option<BTreeMap<String, String>>,
171198
infer_annotations: InferPredefinedAnnotations,
199+
compose_mode: ComposeMode,
172200
) -> Result<Option<String>> {
173201
let mut locked_app = locked.clone();
174202
let mut layers = self
175-
.assemble_layers(&mut locked_app, AssemblyMode::Simple)
203+
.assemble_layers(&mut locked_app, AssemblyMode::Simple, compose_mode)
176204
.await
177205
.context("could not assemble layers for locked application")?;
178206

@@ -183,7 +211,7 @@ impl Client {
183211
{
184212
locked_app = locked.clone();
185213
layers = self
186-
.assemble_layers(&mut locked_app, AssemblyMode::Archive)
214+
.assemble_layers(&mut locked_app, AssemblyMode::Archive, compose_mode)
187215
.await
188216
.context("could not assemble archive layers for locked application")?;
189217
}
@@ -246,43 +274,59 @@ impl Client {
246274
&mut self,
247275
locked: &mut LockedApp,
248276
assembly_mode: AssemblyMode,
277+
compose_mode: ComposeMode,
249278
) -> Result<Vec<ImageLayer>> {
250279
let mut layers = Vec::new();
251280
let mut components = Vec::new();
252281
for mut c in locked.clone().components {
253-
// Add the wasm module for the component as layers.
254-
let source = c
255-
.clone()
256-
.source
257-
.content
258-
.source
259-
.context("component loaded from disk should contain a file source")?;
282+
match compose_mode {
283+
ComposeMode::All => {
284+
let composed = spin_compose::compose(&ComponentSourceLoader, &c)
285+
.await
286+
.with_context(|| {
287+
format!("failed to resolve dependencies for component {:?}", c.id)
288+
})?;
289+
290+
let layer = ImageLayer::new(composed, WASM_LAYER_MEDIA_TYPE.to_string(), None);
291+
c.source.content = self.content_ref_for_layer(&layer);
292+
c.dependencies.clear();
293+
layers.push(layer);
294+
}
295+
ComposeMode::Skip => {
296+
// Add the wasm module for the component as layers.
297+
let source = c
298+
.clone()
299+
.source
300+
.content
301+
.source
302+
.context("component loaded from disk should contain a file source")?;
260303

261-
let source = parse_file_url(source.as_str())?;
262-
let layer = Self::wasm_layer(&source).await?;
304+
let source = parse_file_url(source.as_str())?;
305+
let layer = Self::wasm_layer(&source).await?;
263306

264-
// Update the module source with the content ref of the layer.
265-
c.source.content = self.content_ref_for_layer(&layer);
307+
// Update the module source with the content ref of the layer.
308+
c.source.content = self.content_ref_for_layer(&layer);
266309

267-
layers.push(layer);
310+
layers.push(layer);
268311

269-
let mut deps = BTreeMap::default();
270-
for (dep_name, mut dep) in c.dependencies {
271-
let source = dep
272-
.source
273-
.content
274-
.source
275-
.context("dependency loaded from disk should contain a file source")?;
276-
let source = parse_file_url(source.as_str())?;
312+
let mut deps = BTreeMap::default();
313+
for (dep_name, mut dep) in c.dependencies {
314+
let source =
315+
dep.source.content.source.context(
316+
"dependency loaded from disk should contain a file source",
317+
)?;
318+
let source = parse_file_url(source.as_str())?;
277319

278-
let layer = Self::wasm_layer(&source).await?;
320+
let layer = Self::wasm_layer(&source).await?;
279321

280-
dep.source.content = self.content_ref_for_layer(&layer);
281-
deps.insert(dep_name, dep);
322+
dep.source.content = self.content_ref_for_layer(&layer);
323+
deps.insert(dep_name, dep);
282324

283-
layers.push(layer);
325+
layers.push(layer);
326+
}
327+
c.dependencies = deps;
328+
}
284329
}
285-
c.dependencies = deps;
286330

287331
let mut files = Vec::new();
288332
for f in c.files {
@@ -669,6 +713,32 @@ impl Client {
669713
}
670714
}
671715

716+
struct ComponentSourceLoader;
717+
718+
#[async_trait]
719+
impl spin_compose::ComponentSourceLoader for ComponentSourceLoader {
720+
async fn load_component_source(
721+
&self,
722+
source: &LockedComponentSource,
723+
) -> anyhow::Result<Vec<u8>> {
724+
let source = source
725+
.content
726+
.source
727+
.as_ref()
728+
.context("component loaded from disk should contain a file source")?;
729+
730+
let source = parse_file_url(source.as_str())?;
731+
732+
let bytes = fs::read(&source)
733+
.await
734+
.with_context(|| format!("cannot read wasm module {}", quoted_path(source)))?;
735+
736+
let component = spin_componentize::componentize_if_necessary(&bytes)?;
737+
738+
Ok(component.into())
739+
}
740+
}
741+
672742
/// Unpack contents of the provided archive layer, represented by bytes and its
673743
/// corresponding digest, into the provided cache.
674744
/// A temporary staging directory is created via tempfile::tempdir() to store
@@ -946,6 +1016,7 @@ mod test {
9461016
locked_components: Vec<LockedComponent>,
9471017
expected_layer_count: usize,
9481018
expected_error: Option<&'static str>,
1019+
compose_mode: ComposeMode,
9491020
}
9501021

9511022
let tests: Vec<TestCase> = [
@@ -968,6 +1039,7 @@ mod test {
9681039
}}]),
9691040
expected_layer_count: 2,
9701041
expected_error: None,
1042+
compose_mode: ComposeMode::Skip,
9711043
},
9721044
TestCase {
9731045
name: "One component layer and two file layers",
@@ -992,6 +1064,7 @@ mod test {
9921064
}]),
9931065
expected_layer_count: 3,
9941066
expected_error: None,
1067+
compose_mode: ComposeMode::Skip,
9951068
},
9961069
TestCase {
9971070
name: "One component layer and one file with inlined content",
@@ -1012,6 +1085,7 @@ mod test {
10121085
}]),
10131086
expected_layer_count: 1,
10141087
expected_error: None,
1088+
compose_mode: ComposeMode::Skip,
10151089
},
10161090
TestCase {
10171091
name: "One component layer and one dependency component layer",
@@ -1036,6 +1110,7 @@ mod test {
10361110
}]),
10371111
expected_layer_count: 2,
10381112
expected_error: None,
1113+
compose_mode: ComposeMode::Skip,
10391114
},
10401115
TestCase {
10411116
name: "Component has no source",
@@ -1050,6 +1125,7 @@ mod test {
10501125
}]),
10511126
expected_layer_count: 0,
10521127
expected_error: Some("Invalid URL: \"\""),
1128+
compose_mode: ComposeMode::Skip,
10531129
},
10541130
TestCase {
10551131
name: "Duplicate component sources",
@@ -1070,6 +1146,7 @@ mod test {
10701146
}}]),
10711147
expected_layer_count: 1,
10721148
expected_error: None,
1149+
compose_mode: ComposeMode::Skip,
10731150
},
10741151
TestCase {
10751152
name: "Duplicate file paths",
@@ -1107,6 +1184,7 @@ mod test {
11071184
}]),
11081185
expected_layer_count: 4,
11091186
expected_error: None,
1187+
compose_mode: ComposeMode::Skip,
11101188
},
11111189
]
11121190
.to_vec();
@@ -1137,7 +1215,7 @@ mod test {
11371215
assert_eq!(
11381216
e,
11391217
client
1140-
.assemble_layers(&mut locked, AssemblyMode::Simple)
1218+
.assemble_layers(&mut locked, AssemblyMode::Simple, tc.compose_mode)
11411219
.await
11421220
.unwrap_err()
11431221
.to_string(),
@@ -1149,7 +1227,7 @@ mod test {
11491227
assert_eq!(
11501228
tc.expected_layer_count,
11511229
client
1152-
.assemble_layers(&mut locked, AssemblyMode::Simple)
1230+
.assemble_layers(&mut locked, AssemblyMode::Simple, tc.compose_mode)
11531231
.await
11541232
.unwrap()
11551233
.len(),

src/commands/registry.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use anyhow::{Context, Result};
33
use clap::{Parser, Subcommand};
44
use indicatif::{ProgressBar, ProgressStyle};
55
use spin_common::arg_parser::parse_kv;
6-
use spin_oci::{client::InferPredefinedAnnotations, Client};
6+
use spin_oci::{client::InferPredefinedAnnotations, Client, ComposeMode};
77
use std::{io::Read, path::PathBuf, time::Duration};
88

99
/// Commands for working with OCI registries to distribute applications.
@@ -49,6 +49,10 @@ pub struct Push {
4949
)]
5050
pub insecure: bool,
5151

52+
/// Skip composing the application's components before pushing it.
53+
#[clap(long = "skip-compose")]
54+
pub skip_compose: bool,
55+
5256
/// Specifies to perform `spin build` before pushing the application.
5357
#[clap(long, takes_value = false, env = ALWAYS_BUILD_ENV)]
5458
pub build: bool,
@@ -88,12 +92,19 @@ impl Push {
8892

8993
let _spinner = create_dotted_spinner(2000, "Pushing app to the Registry".to_owned());
9094

95+
let compose_mode = if self.skip_compose {
96+
ComposeMode::Skip
97+
} else {
98+
ComposeMode::All
99+
};
100+
91101
let digest = client
92102
.push(
93103
&app_file,
94104
&self.reference,
95105
annotations,
96106
InferPredefinedAnnotations::All,
107+
self.skip_compose,
97108
)
98109
.await?;
99110
match digest {

0 commit comments

Comments
 (0)