Skip to content

Commit 2ba49e1

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

File tree

5 files changed

+130
-36
lines changed

5 files changed

+130
-36
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: 111 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,15 @@ enum AssemblyMode {
6768
Archive,
6869
}
6970

71+
/// Indicates whether to compose the components of a Spin application when pushing an image.
72+
#[derive(Copy, Clone)]
73+
pub enum ComposeMode {
74+
/// Compose components before pushing the image.
75+
All,
76+
/// Skip composing components before pushing the image.
77+
Skip,
78+
}
79+
7080
/// Client for interacting with an OCI registry for Spin applications.
7181
pub struct Client {
7282
/// Global cache for the metadata, Wasm modules, and static assets pulled from OCI registries.
@@ -119,6 +129,7 @@ impl Client {
119129
reference: impl AsRef<str>,
120130
annotations: Option<BTreeMap<String, String>>,
121131
infer_annotations: InferPredefinedAnnotations,
132+
compose_mode: ComposeMode,
122133
) -> Result<Option<String>> {
123134
let reference: Reference = reference
124135
.as_ref()
@@ -137,8 +148,15 @@ impl Client {
137148
)
138149
.await?;
139150

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

144162
/// Push a Spin application to an OCI registry and return the digest (or None
@@ -149,15 +167,23 @@ impl Client {
149167
reference: impl AsRef<str>,
150168
annotations: Option<BTreeMap<String, String>>,
151169
infer_annotations: InferPredefinedAnnotations,
170+
compose_mode: ComposeMode,
152171
) -> Result<Option<String>> {
153172
let reference: Reference = reference
154173
.as_ref()
155174
.parse()
156175
.with_context(|| format!("cannot parse reference {}", reference.as_ref()))?;
157176
let auth = Self::auth(&reference).await?;
158177

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

163189
/// Push a Spin application to an OCI registry and return the digest (or None
@@ -169,10 +195,11 @@ impl Client {
169195
reference: Reference,
170196
annotations: Option<BTreeMap<String, String>>,
171197
infer_annotations: InferPredefinedAnnotations,
198+
compose_mode: ComposeMode,
172199
) -> Result<Option<String>> {
173200
let mut locked_app = locked.clone();
174201
let mut layers = self
175-
.assemble_layers(&mut locked_app, AssemblyMode::Simple)
202+
.assemble_layers(&mut locked_app, AssemblyMode::Simple, compose_mode)
176203
.await
177204
.context("could not assemble layers for locked application")?;
178205

@@ -183,7 +210,7 @@ impl Client {
183210
{
184211
locked_app = locked.clone();
185212
layers = self
186-
.assemble_layers(&mut locked_app, AssemblyMode::Archive)
213+
.assemble_layers(&mut locked_app, AssemblyMode::Archive, compose_mode)
187214
.await
188215
.context("could not assemble archive layers for locked application")?;
189216
}
@@ -246,43 +273,59 @@ impl Client {
246273
&mut self,
247274
locked: &mut LockedApp,
248275
assembly_mode: AssemblyMode,
276+
compose_mode: ComposeMode,
249277
) -> Result<Vec<ImageLayer>> {
250278
let mut layers = Vec::new();
251279
let mut components = Vec::new();
252280
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")?;
281+
match compose_mode {
282+
ComposeMode::All => {
283+
let composed = spin_compose::compose(&ComponentSourceLoader, &c)
284+
.await
285+
.with_context(|| {
286+
format!("failed to resolve dependencies for component {:?}", c.id)
287+
})?;
288+
289+
let layer = ImageLayer::new(composed, WASM_LAYER_MEDIA_TYPE.to_string(), None);
290+
c.source.content = self.content_ref_for_layer(&layer);
291+
c.dependencies.clear();
292+
layers.push(layer);
293+
}
294+
ComposeMode::Skip => {
295+
// Add the wasm module for the component as layers.
296+
let source = c
297+
.clone()
298+
.source
299+
.content
300+
.source
301+
.context("component loaded from disk should contain a file source")?;
260302

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

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

267-
layers.push(layer);
309+
layers.push(layer);
268310

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())?;
311+
let mut deps = BTreeMap::default();
312+
for (dep_name, mut dep) in c.dependencies {
313+
let source =
314+
dep.source.content.source.context(
315+
"dependency loaded from disk should contain a file source",
316+
)?;
317+
let source = parse_file_url(source.as_str())?;
277318

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

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

283-
layers.push(layer);
324+
layers.push(layer);
325+
}
326+
c.dependencies = deps;
327+
}
284328
}
285-
c.dependencies = deps;
286329

287330
let mut files = Vec::new();
288331
for f in c.files {
@@ -669,6 +712,32 @@ impl Client {
669712
}
670713
}
671714

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

9511021
let tests: Vec<TestCase> = [
@@ -968,6 +1038,7 @@ mod test {
9681038
}}]),
9691039
expected_layer_count: 2,
9701040
expected_error: None,
1041+
compose_mode: ComposeMode::Skip,
9711042
},
9721043
TestCase {
9731044
name: "One component layer and two file layers",
@@ -992,6 +1063,7 @@ mod test {
9921063
}]),
9931064
expected_layer_count: 3,
9941065
expected_error: None,
1066+
compose_mode: ComposeMode::Skip,
9951067
},
9961068
TestCase {
9971069
name: "One component layer and one file with inlined content",
@@ -1012,6 +1084,7 @@ mod test {
10121084
}]),
10131085
expected_layer_count: 1,
10141086
expected_error: None,
1087+
compose_mode: ComposeMode::Skip,
10151088
},
10161089
TestCase {
10171090
name: "One component layer and one dependency component layer",
@@ -1036,6 +1109,7 @@ mod test {
10361109
}]),
10371110
expected_layer_count: 2,
10381111
expected_error: None,
1112+
compose_mode: ComposeMode::Skip,
10391113
},
10401114
TestCase {
10411115
name: "Component has no source",
@@ -1050,6 +1124,7 @@ mod test {
10501124
}]),
10511125
expected_layer_count: 0,
10521126
expected_error: Some("Invalid URL: \"\""),
1127+
compose_mode: ComposeMode::Skip,
10531128
},
10541129
TestCase {
10551130
name: "Duplicate component sources",
@@ -1070,6 +1145,7 @@ mod test {
10701145
}}]),
10711146
expected_layer_count: 1,
10721147
expected_error: None,
1148+
compose_mode: ComposeMode::Skip,
10731149
},
10741150
TestCase {
10751151
name: "Duplicate file paths",
@@ -1107,6 +1183,7 @@ mod test {
11071183
}]),
11081184
expected_layer_count: 4,
11091185
expected_error: None,
1186+
compose_mode: ComposeMode::Skip,
11101187
},
11111188
]
11121189
.to_vec();
@@ -1137,7 +1214,7 @@ mod test {
11371214
assert_eq!(
11381215
e,
11391216
client
1140-
.assemble_layers(&mut locked, AssemblyMode::Simple)
1217+
.assemble_layers(&mut locked, AssemblyMode::Simple, tc.compose_mode)
11411218
.await
11421219
.unwrap_err()
11431220
.to_string(),
@@ -1149,7 +1226,7 @@ mod test {
11491226
assert_eq!(
11501227
tc.expected_layer_count,
11511228
client
1152-
.assemble_layers(&mut locked, AssemblyMode::Simple)
1229+
.assemble_layers(&mut locked, AssemblyMode::Simple, tc.compose_mode)
11531230
.await
11541231
.unwrap()
11551232
.len(),

crates/oci/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub mod client;
66
mod loader;
77
pub mod utils;
88

9-
pub use client::Client;
9+
pub use client::{Client, ComposeMode};
1010
pub use loader::OciLoader;
1111

1212
/// URL scheme used for the locked app "origin" metadata field for OCI-sourced apps.

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+
compose_mode,
97108
)
98109
.await?;
99110
match digest {

0 commit comments

Comments
 (0)