1use std::collections::HashMap;
5use std::path::Path;
6
7use serde::{Deserialize, Serialize};
8
9use crate::errors::OciError;
10use crate::output::Printer;
11use crate::sha256_digest;
12
13use super::archive::create_tar_gz;
14use super::auth::RegistryAuth;
15use super::transport::{authenticated_request, upload_blob};
16use super::{
17 MEDIA_TYPE_MODULE_CONFIG, MEDIA_TYPE_MODULE_LAYER, MEDIA_TYPE_OCI_MANIFEST, OciDescriptor,
18 OciManifest, OciReference, ReferenceKind,
19};
20
21pub fn push_module(
29 dir: &Path,
30 artifact_ref: &str,
31 platform: Option<&str>,
32 printer: Option<&Printer>,
33) -> Result<String, OciError> {
34 let oci_ref = OciReference::parse(artifact_ref)?;
35 let auth = RegistryAuth::resolve(&oci_ref.registry);
36 let agent = crate::http::http_agent(crate::http::HTTP_OCI_TIMEOUT);
37 let spinner = printer.map(|p| p.spinner(format!("Pushing module to {artifact_ref}...")));
38 let (digest, _size) = push_module_inner(&agent, dir, &oci_ref, auth.as_ref(), platform)?;
39 if let Some(s) = spinner {
40 let _ = s.finish_ok(format!("Pushed module to {artifact_ref}"));
41 }
42 Ok(digest)
43}
44
45pub(super) fn push_module_inner(
48 agent: &ureq::Agent,
49 dir: &Path,
50 oci_ref: &OciReference,
51 auth: Option<&RegistryAuth>,
52 platform: Option<&str>,
53) -> Result<(String, u64), OciError> {
54 let module_yaml_path = dir.join("module.yaml");
56 if !module_yaml_path.exists() {
57 return Err(OciError::ModuleYamlNotFound {
58 dir: dir.to_path_buf(),
59 });
60 }
61 let module_yaml = std::fs::read_to_string(&module_yaml_path)?;
62
63 let config_blob = serde_json::to_vec(&serde_json::json!({
65 "moduleYaml": module_yaml,
66 }))?;
67
68 let layer_data = create_tar_gz(dir)?;
70
71 let platform_str = platform.map(String::from).unwrap_or_else(current_platform);
73
74 let config_digest = upload_blob(agent, oci_ref, auth, &config_blob, MEDIA_TYPE_MODULE_CONFIG)?;
76
77 let layer_digest = upload_blob(agent, oci_ref, auth, &layer_data, MEDIA_TYPE_MODULE_LAYER)?;
79
80 let mut annotations = HashMap::new();
82 annotations.insert(crate::OCI_ANNOTATION_PLATFORM.to_string(), platform_str);
83 annotations.insert(
84 "org.opencontainers.image.created".to_string(),
85 crate::utc_now_iso8601(),
86 );
87
88 let manifest = OciManifest {
89 schema_version: 2,
90 media_type: MEDIA_TYPE_OCI_MANIFEST.to_string(),
91 config: OciDescriptor {
92 media_type: MEDIA_TYPE_MODULE_CONFIG.to_string(),
93 digest: config_digest,
94 size: config_blob.len() as u64,
95 annotations: HashMap::new(),
96 },
97 layers: vec![OciDescriptor {
98 media_type: MEDIA_TYPE_MODULE_LAYER.to_string(),
99 digest: layer_digest,
100 size: layer_data.len() as u64,
101 annotations: HashMap::new(),
102 }],
103 annotations,
104 };
105
106 let manifest_json = serde_json::to_vec(&manifest)?;
107
108 let manifest_url = format!(
110 "{}/{}/manifests/{}",
111 oci_ref.api_base(),
112 oci_ref.repository,
113 oci_ref.reference_str(),
114 );
115
116 authenticated_request(
117 agent,
118 "PUT",
119 &manifest_url,
120 auth,
121 None,
122 Some(MEDIA_TYPE_OCI_MANIFEST),
123 Some(&manifest_json),
124 )
125 .map_err(|e| OciError::ManifestPushFailed {
126 message: format!("{e}"),
127 })?;
128
129 let manifest_size = manifest_json.len() as u64;
130 let manifest_digest = sha256_digest(&manifest_json);
131 tracing::info!(
132 reference = %oci_ref,
133 digest = %manifest_digest,
134 "module pushed"
135 );
136
137 Ok((manifest_digest, manifest_size))
138}
139
140pub(super) const MEDIA_TYPE_OCI_INDEX: &str = "application/vnd.oci.image.index.v1+json";
145
146#[derive(Debug, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub(super) struct OciIndex {
149 pub(super) schema_version: u32,
150 pub(super) media_type: String,
151 pub(super) manifests: Vec<OciPlatformManifest>,
152}
153
154#[derive(Debug, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub(super) struct OciPlatformManifest {
157 pub(super) media_type: String,
158 pub(super) digest: String,
159 pub(super) size: u64,
160 pub(super) platform: OciPlatform,
161}
162
163#[derive(Debug, Serialize, Deserialize)]
164pub(super) struct OciPlatform {
165 pub(super) os: String,
166 pub(super) architecture: String,
167}
168
169pub fn rust_arch_to_oci(arch: &str) -> &str {
171 match arch {
172 "x86_64" => "amd64",
173 "aarch64" => "arm64",
174 "arm" => "arm",
175 "s390x" => "s390x",
176 "powerpc64" => "ppc64le",
177 other => other,
178 }
179}
180
181pub fn current_platform() -> String {
183 format!(
184 "{}/{}",
185 std::env::consts::OS,
186 rust_arch_to_oci(std::env::consts::ARCH)
187 )
188}
189
190pub fn parse_platform_target(target: &str) -> Result<(&str, &str), OciError> {
192 target.split_once('/').ok_or_else(|| OciError::BuildError {
193 message: format!(
194 "invalid platform target '{target}' — expected os/arch (e.g. linux/amd64)"
195 ),
196 })
197}
198
199pub fn push_module_multiplatform(
204 builds: &[(&Path, &str)],
205 artifact_ref: &str,
206 printer: Option<&Printer>,
207) -> Result<String, OciError> {
208 let oci_ref = OciReference::parse(artifact_ref)?;
209 let auth = RegistryAuth::resolve(&oci_ref.registry);
210 let agent = crate::http::http_agent(crate::http::HTTP_OCI_TIMEOUT);
211
212 let spinner = printer.map(|p| {
213 p.spinner(format!(
214 "Pushing multi-platform module to {artifact_ref}..."
215 ))
216 });
217
218 let mut platform_manifests = Vec::new();
219
220 for (dir, platform) in builds {
221 let (os, arch) = parse_platform_target(platform)?;
222
223 let platform_tag = format!("{}-{}", oci_ref.reference_str(), platform.replace('/', "-"));
225 let platform_ref = OciReference {
226 registry: oci_ref.registry.clone(),
227 repository: oci_ref.repository.clone(),
228 reference: ReferenceKind::Tag(platform_tag),
229 };
230
231 let (digest, size) =
232 push_module_inner(&agent, dir, &platform_ref, auth.as_ref(), Some(platform))?;
233
234 platform_manifests.push(OciPlatformManifest {
235 media_type: MEDIA_TYPE_OCI_MANIFEST.to_string(),
236 digest,
237 size,
238 platform: OciPlatform {
239 os: os.to_string(),
240 architecture: arch.to_string(),
241 },
242 });
243 }
244
245 let index = OciIndex {
247 schema_version: 2,
248 media_type: MEDIA_TYPE_OCI_INDEX.to_string(),
249 manifests: platform_manifests,
250 };
251 let index_json = serde_json::to_vec(&index)?;
252
253 let index_url = format!(
254 "{}/{}/manifests/{}",
255 oci_ref.api_base(),
256 oci_ref.repository,
257 oci_ref.reference_str(),
258 );
259
260 authenticated_request(
261 &agent,
262 "PUT",
263 &index_url,
264 auth.as_ref(),
265 None,
266 Some(MEDIA_TYPE_OCI_INDEX),
267 Some(&index_json),
268 )
269 .map_err(|e| OciError::ManifestPushFailed {
270 message: format!("index push failed: {e}"),
271 })?;
272
273 let index_digest = sha256_digest(&index_json);
274
275 if let Some(s) = spinner {
276 let _ = s.finish_ok(format!("Pushed multi-platform module to {artifact_ref}"));
277 }
278
279 tracing::info!(
280 reference = %oci_ref,
281 digest = %index_digest,
282 platforms = builds.len(),
283 "multi-platform module pushed"
284 );
285
286 Ok(index_digest)
287}
288
289#[cfg(test)]
290mod tests;