Skip to content

Commit ee371d1

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

File tree

4 files changed

+110
-33
lines changed

4 files changed

+110
-33
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: 99 additions & 33 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

@@ -119,6 +120,7 @@ impl Client {
119120
reference: impl AsRef<str>,
120121
annotations: Option<BTreeMap<String, String>>,
121122
infer_annotations: InferPredefinedAnnotations,
123+
skip_compose: bool,
122124
) -> Result<Option<String>> {
123125
let reference: Reference = reference
124126
.as_ref()
@@ -137,8 +139,15 @@ impl Client {
137139
)
138140
.await?;
139141

140-
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
141-
.await
142+
self.push_locked_core(
143+
locked,
144+
auth,
145+
reference,
146+
annotations,
147+
infer_annotations,
148+
skip_compose,
149+
)
150+
.await
142151
}
143152

144153
/// Push a Spin application to an OCI registry and return the digest (or None
@@ -149,15 +158,23 @@ impl Client {
149158
reference: impl AsRef<str>,
150159
annotations: Option<BTreeMap<String, String>>,
151160
infer_annotations: InferPredefinedAnnotations,
161+
skip_compose: bool,
152162
) -> Result<Option<String>> {
153163
let reference: Reference = reference
154164
.as_ref()
155165
.parse()
156166
.with_context(|| format!("cannot parse reference {}", reference.as_ref()))?;
157167
let auth = Self::auth(&reference).await?;
158168

159-
self.push_locked_core(locked, auth, reference, annotations, infer_annotations)
160-
.await
169+
self.push_locked_core(
170+
locked,
171+
auth,
172+
reference,
173+
annotations,
174+
infer_annotations,
175+
skip_compose,
176+
)
177+
.await
161178
}
162179

163180
/// Push a Spin application to an OCI registry and return the digest (or None
@@ -169,10 +186,11 @@ impl Client {
169186
reference: Reference,
170187
annotations: Option<BTreeMap<String, String>>,
171188
infer_annotations: InferPredefinedAnnotations,
189+
skip_compose: bool,
172190
) -> Result<Option<String>> {
173191
let mut locked_app = locked.clone();
174192
let mut layers = self
175-
.assemble_layers(&mut locked_app, AssemblyMode::Simple)
193+
.assemble_layers(&mut locked_app, AssemblyMode::Simple, skip_compose)
176194
.await
177195
.context("could not assemble layers for locked application")?;
178196

@@ -183,7 +201,7 @@ impl Client {
183201
{
184202
locked_app = locked.clone();
185203
layers = self
186-
.assemble_layers(&mut locked_app, AssemblyMode::Archive)
204+
.assemble_layers(&mut locked_app, AssemblyMode::Archive, skip_compose)
187205
.await
188206
.context("could not assemble archive layers for locked application")?;
189207
}
@@ -246,43 +264,57 @@ impl Client {
246264
&mut self,
247265
locked: &mut LockedApp,
248266
assembly_mode: AssemblyMode,
267+
skip_compose: bool,
249268
) -> Result<Vec<ImageLayer>> {
250269
let mut layers = Vec::new();
251270
let mut components = Vec::new();
252271
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")?;
260-
261-
let source = parse_file_url(source.as_str())?;
262-
let layer = Self::wasm_layer(&source).await?;
263-
264-
// Update the module source with the content ref of the layer.
265-
c.source.content = self.content_ref_for_layer(&layer);
266-
267-
layers.push(layer);
268-
269-
let mut deps = BTreeMap::default();
270-
for (dep_name, mut dep) in c.dependencies {
271-
let source = dep
272+
if !skip_compose {
273+
let composed = spin_compose::compose(&ComponentSourceLoader, &c)
274+
.await
275+
.with_context(|| {
276+
format!("failed to resolve dependencies for component {:?}", c.id)
277+
})?;
278+
279+
let layer = ImageLayer::new(composed, WASM_LAYER_MEDIA_TYPE.to_string(), None);
280+
c.source.content = self.content_ref_for_layer(&layer);
281+
c.dependencies.clear();
282+
layers.push(layer);
283+
} else {
284+
// Add the wasm module for the component as layers.
285+
let source = c
286+
.clone()
272287
.source
273288
.content
274289
.source
275-
.context("dependency loaded from disk should contain a file source")?;
276-
let source = parse_file_url(source.as_str())?;
290+
.context("component loaded from disk should contain a file source")?;
277291

292+
let source = parse_file_url(source.as_str())?;
278293
let layer = Self::wasm_layer(&source).await?;
279294

280-
dep.source.content = self.content_ref_for_layer(&layer);
281-
deps.insert(dep_name, dep);
295+
// Update the module source with the content ref of the layer.
296+
c.source.content = self.content_ref_for_layer(&layer);
282297

283298
layers.push(layer);
299+
300+
let mut deps = BTreeMap::default();
301+
for (dep_name, mut dep) in c.dependencies {
302+
let source = dep
303+
.source
304+
.content
305+
.source
306+
.context("dependency loaded from disk should contain a file source")?;
307+
let source = parse_file_url(source.as_str())?;
308+
309+
let layer = Self::wasm_layer(&source).await?;
310+
311+
dep.source.content = self.content_ref_for_layer(&layer);
312+
deps.insert(dep_name, dep);
313+
314+
layers.push(layer);
315+
}
316+
c.dependencies = deps;
284317
}
285-
c.dependencies = deps;
286318

287319
let mut files = Vec::new();
288320
for f in c.files {
@@ -669,6 +701,32 @@ impl Client {
669701
}
670702
}
671703

704+
struct ComponentSourceLoader;
705+
706+
#[async_trait]
707+
impl spin_compose::ComponentSourceLoader for ComponentSourceLoader {
708+
async fn load_component_source(
709+
&self,
710+
source: &LockedComponentSource,
711+
) -> anyhow::Result<Vec<u8>> {
712+
let source = source
713+
.content
714+
.source
715+
.as_ref()
716+
.context("component loaded from disk should contain a file source")?;
717+
718+
let source = parse_file_url(source.as_str())?;
719+
720+
let bytes = fs::read(&source)
721+
.await
722+
.with_context(|| format!("cannot read wasm module {}", quoted_path(source)))?;
723+
724+
let component = spin_componentize::componentize_if_necessary(&bytes)?;
725+
726+
Ok(component.into())
727+
}
728+
}
729+
672730
/// Unpack contents of the provided archive layer, represented by bytes and its
673731
/// corresponding digest, into the provided cache.
674732
/// A temporary staging directory is created via tempfile::tempdir() to store
@@ -946,6 +1004,7 @@ mod test {
9461004
locked_components: Vec<LockedComponent>,
9471005
expected_layer_count: usize,
9481006
expected_error: Option<&'static str>,
1007+
skip_compose: bool,
9491008
}
9501009

9511010
let tests: Vec<TestCase> = [
@@ -968,6 +1027,7 @@ mod test {
9681027
}}]),
9691028
expected_layer_count: 2,
9701029
expected_error: None,
1030+
skip_compose: false,
9711031
},
9721032
TestCase {
9731033
name: "One component layer and two file layers",
@@ -992,6 +1052,7 @@ mod test {
9921052
}]),
9931053
expected_layer_count: 3,
9941054
expected_error: None,
1055+
skip_compose: false,
9951056
},
9961057
TestCase {
9971058
name: "One component layer and one file with inlined content",
@@ -1012,6 +1073,7 @@ mod test {
10121073
}]),
10131074
expected_layer_count: 1,
10141075
expected_error: None,
1076+
skip_compose: false,
10151077
},
10161078
TestCase {
10171079
name: "One component layer and one dependency component layer",
@@ -1036,6 +1098,7 @@ mod test {
10361098
}]),
10371099
expected_layer_count: 2,
10381100
expected_error: None,
1101+
skip_compose: false,
10391102
},
10401103
TestCase {
10411104
name: "Component has no source",
@@ -1050,6 +1113,7 @@ mod test {
10501113
}]),
10511114
expected_layer_count: 0,
10521115
expected_error: Some("Invalid URL: \"\""),
1116+
skip_compose: false,
10531117
},
10541118
TestCase {
10551119
name: "Duplicate component sources",
@@ -1070,6 +1134,7 @@ mod test {
10701134
}}]),
10711135
expected_layer_count: 1,
10721136
expected_error: None,
1137+
skip_compose: false,
10731138
},
10741139
TestCase {
10751140
name: "Duplicate file paths",
@@ -1107,6 +1172,7 @@ mod test {
11071172
}]),
11081173
expected_layer_count: 4,
11091174
expected_error: None,
1175+
skip_compose: false,
11101176
},
11111177
]
11121178
.to_vec();
@@ -1137,7 +1203,7 @@ mod test {
11371203
assert_eq!(
11381204
e,
11391205
client
1140-
.assemble_layers(&mut locked, AssemblyMode::Simple)
1206+
.assemble_layers(&mut locked, AssemblyMode::Simple, tc.skip_compose)
11411207
.await
11421208
.unwrap_err()
11431209
.to_string(),
@@ -1149,7 +1215,7 @@ mod test {
11491215
assert_eq!(
11501216
tc.expected_layer_count,
11511217
client
1152-
.assemble_layers(&mut locked, AssemblyMode::Simple)
1218+
.assemble_layers(&mut locked, AssemblyMode::Simple, tc.skip_compose)
11531219
.await
11541220
.unwrap()
11551221
.len(),

src/commands/registry.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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,
@@ -94,6 +98,7 @@ impl Push {
9498
&self.reference,
9599
annotations,
96100
InferPredefinedAnnotations::All,
101+
self.skip_compose,
97102
)
98103
.await?;
99104
match digest {

0 commit comments

Comments
 (0)