Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/misc/asset-container/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ where
self.asset.fetch_with_progress(options)
}

fn fetch(&self, options: Self::Options) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + Sync + '_>> {
fn fetch(&self, options: Self::Options) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + '_>> {
self.asset.fetch(options)
}

Expand Down Expand Up @@ -406,7 +406,7 @@ pub trait AssetManager {
pub trait Asset: AssetManager {
type Options: Clone;
fn fetch_with_progress(&self, options: Self::Options) -> Pin<Box<dyn Stream<Item = Progress> + Send + '_>>;
fn fetch(&self, options: Self::Options) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + Sync + '_>>;
fn fetch(&self, options: Self::Options) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + '_>>;
fn name(&self) -> &str;
fn update_baseurl(&self, baseurl: &Path);
}
Expand All @@ -432,7 +432,7 @@ where
)
}

fn fetch(&self, options: Self::Options) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + Sync + '_>> {
fn fetch(&self, options: Self::Options) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + '_>> {
Box::pin(async move {
match self {
Some(a) => a.fetch(options).await,
Expand Down
4 changes: 1 addition & 3 deletions crates/misc/asset-container/tests/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,7 @@ impl Asset for LocationReference {
fn fetch(
&self,
_options: Self::Options,
) -> std::pin::Pin<
Box<dyn futures::Future<Output = std::result::Result<Vec<u8>, asset_container::Error>> + Send + Sync>,
> {
) -> std::pin::Pin<Box<dyn futures::Future<Output = std::result::Result<Vec<u8>, asset_container::Error>> + Send>> {
let mut path = self
.baseurl
.lock()
Expand Down
4 changes: 1 addition & 3 deletions crates/misc/derive-asset-container/tests/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ impl Asset for TestAsset {
fn fetch(
&self,
_options: Self::Options,
) -> std::pin::Pin<
Box<dyn futures::Future<Output = std::result::Result<Vec<u8>, asset_container::Error>> + Send + Sync>,
> {
) -> std::pin::Pin<Box<dyn futures::Future<Output = std::result::Result<Vec<u8>, asset_container::Error>> + Send>> {
let path = self.path.clone();
Box::pin(async move {
let mut file = tokio::fs::File::open(&path)
Expand Down
2 changes: 1 addition & 1 deletion crates/wick/wick-asset-reference/src/asset_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ impl Asset for AssetReference {
fn fetch(
&self,
options: OciOptions,
) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<u8>, assets::Error>> + Send + Sync>> {
) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<u8>, assets::Error>> + Send>> {
let asset = self.clone();

Box::pin(async move {
Expand Down
13 changes: 13 additions & 0 deletions crates/wick/wick-oci-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ pub enum WickPackageKind {
TYPES,
}

impl WickPackageKind {
/// Get the media type for the kind.
#[must_use]
pub const fn media_type(&self) -> &'static str {
use self::package::media_types;
match self {
Self::APPLICATION => media_types::APPLICATION,
Self::COMPONENT => media_types::COMPONENT,
Self::TYPES => media_types::TYPES,
}
}
}

/// Retrieve a manifest from an OCI url.
pub async fn fetch_image_manifest(image: &str, options: &OciOptions) -> Result<(OciImageManifest, String), OciError> {
if !options.allow_latest && image.ends_with(":latest") {
Expand Down
110 changes: 80 additions & 30 deletions crates/wick/wick-oci-utils/src/package/push.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
use std::collections::HashMap;

use oci_distribution::client::{Client, ClientConfig, ImageLayer, PushResponse};
use oci_distribution::client::{Client, ClientConfig, ImageLayer};
use oci_distribution::manifest::{OciDescriptor, OciImageManifest};
use oci_distribution::Reference;
use sha256::digest;

use super::annotations::Annotations;
use super::{annotations, media_types, PackageFile};
use crate::{Error, OciOptions};
use crate::{Error, OciOptions, WickPackageKind};

#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PushResponse {
/// Pullable url for the config
pub config_url: String,
/// Pullable url for the manifest
pub manifest_url: String,
/// The OCI reference for the pushed image
pub reference: String,
/// Pullable url for any additional tags referencing this manifest
pub tags: Vec<String>,
}

/// Push a Wick package to a registry.
pub async fn push(
reference: &str,
kind: WickPackageKind,
config_json: String,
files: Vec<PackageFile>,
annotations: Annotations,
tags: Vec<String>,
options: &OciOptions,
) -> Result<PushResponse, Error> {
let image_config = oci_distribution::client::Config {
data: config_json.as_bytes().to_vec(),
media_type: media_types::CONFIG.to_owned(),
annotations: None,
};

let mut image_layer_descriptors: Vec<OciDescriptor> = Vec::new();
let mut layers: Vec<OciDescriptor> = Vec::new();
let mut image_layers: Vec<ImageLayer> = Vec::new();

for file in files {
Expand All @@ -47,49 +58,88 @@ pub async fn push(
urls: None,
};

image_layer_descriptors.push(image_layer_descriptor);
layers.push(image_layer_descriptor);
image_layers.push(image_layer);
}

let image_annotations: HashMap<String, String> = annotations.inner().clone();

let image_manifest = OciImageManifest {
schema_version: 2,
config: OciDescriptor {
media_type: image_config.media_type.clone(),
digest: format!("sha256:{}", digest(config_json.clone())),
size: image_config.data.clone().len() as i64,
annotations: None,
urls: None,
},
layers: image_layer_descriptors,
media_type: Some(media_types::MANIFEST.to_owned()),
annotations: Some(image_annotations),
};
let (image_config, image_manifest) = gen_manifest(
&config_json,
layers,
Some(image_annotations),
Some(kind.media_type().to_owned()),
);

let (image_ref, protocol) = crate::utils::parse_reference_and_protocol(reference, &options.allow_insecure)?;
let client_config = ClientConfig {
protocol,
..Default::default()
};

let mut client = Client::new(client_config);
let auth = options.get_auth();

let result = client
let push_response = match client
.push(
&image_ref,
&image_layers,
image_config,
&auth,
Some(image_manifest.clone()),
)
.await;

match result {
Ok(push_response) => Ok(push_response),
.await
{
Ok(push_response) => push_response,
Err(e) => {
tracing::error!(manifest = %image_manifest, error = %e, "Push failed");
Err(Error::PushFailed(e.to_string()))
tracing::error!(manifest = %image_ref, error = %e, "Push failed");
return Err(Error::PushFailed(e.to_string()));
}
};

let mut pushed_tags = Vec::new();
for tag in tags {
let image_ref = Reference::with_tag(image_ref.registry().to_owned(), image_ref.repository().to_owned(), tag);

client
.push_manifest(&image_ref, &(image_manifest.clone().into()))
.await?;
pushed_tags.push(image_ref.to_string());
}

Ok(PushResponse {
config_url: push_response.config_url,
manifest_url: push_response.manifest_url,
reference: image_ref.to_string(),
tags: pushed_tags,
})
}

fn gen_manifest(
config_json: &str,
layers: Vec<OciDescriptor>,
annotations: Option<HashMap<String, String>>,
artifact_type: Option<String>,
) -> (oci_distribution::client::Config, OciImageManifest) {
let image_config = oci_distribution::client::Config {
data: config_json.as_bytes().to_vec(),
media_type: media_types::CONFIG.to_owned(),
annotations: None,
};

let manifest = OciImageManifest {
schema_version: 2,
config: OciDescriptor {
media_type: image_config.media_type.clone(),
digest: format!("sha256:{}", digest(config_json.clone())),
size: image_config.data.len() as i64,
annotations: None,
urls: None,
},
layers,
media_type: Some(media_types::MANIFEST.to_owned()),
annotations,
artifact_type,
};
(image_config, manifest)
}
35 changes: 21 additions & 14 deletions crates/wick/wick-package/src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tracing::trace;
use wick_config::config::RegistryConfig;
use wick_config::{AssetReference, WickConfiguration};
use wick_oci_utils::package::annotations::Annotations;
use wick_oci_utils::package::{media_types, PackageFile};
use wick_oci_utils::package::{media_types, PackageFile, PushResponse};
use wick_oci_utils::OciOptions;

use crate::utils::{create_tar_gz, metadata_to_annotations};
Expand Down Expand Up @@ -298,32 +298,30 @@ impl WickPackage {
self.registry.as_mut()
}

/// Pushes the WickPackage to a specified registry using the provided reference, username, and password.
///
/// The username and password are optional. If not provided, the function falls back to anonymous authentication.
pub async fn push(&mut self, reference: &str, options: &OciOptions) -> Result<String, Error> {
let kind = match self.kind {
wick_config::config::ConfigurationKind::App => wick_oci_utils::WickPackageKind::APPLICATION,
wick_config::config::ConfigurationKind::Component => wick_oci_utils::WickPackageKind::COMPONENT,
wick_config::config::ConfigurationKind::Types => wick_oci_utils::WickPackageKind::TYPES,
_ => {
return Err(Error::InvalidWickConfig(reference.to_owned()));
}
};
/// Pushes the WickPackage to a specified registry using the provided OciOptions.
pub async fn push(
&mut self,
reference: &str,
tags: Vec<String>,
options: &OciOptions,
) -> Result<PushResponse, Error> {
let kind = convert_kind(self.kind).map_err(|_| Error::InvalidWickConfig(reference.to_owned()))?;
let config = wick_oci_utils::WickOciConfig::new(kind, self.root.clone());
let image_config_contents = serde_json::to_string(&config).unwrap();
let files = self.files.drain(..).collect();

let push_response = wick_oci_utils::package::push(
reference,
kind,
image_config_contents,
files,
self.annotations.clone(),
tags,
options,
)
.await?;

Ok(push_response.manifest_url)
Ok(push_response)
}

/// This function pulls a WickPackage from a specified registry using the provided reference, username, and password.
Expand All @@ -338,3 +336,12 @@ impl WickPackage {
}
}
}

const fn convert_kind(kind: wick_config::config::ConfigurationKind) -> Result<wick_oci_utils::WickPackageKind, ()> {
Ok(match kind {
wick_config::config::ConfigurationKind::App => wick_oci_utils::WickPackageKind::APPLICATION,
wick_config::config::ConfigurationKind::Component => wick_oci_utils::WickPackageKind::COMPONENT,
wick_config::config::ConfigurationKind::Types => wick_oci_utils::WickPackageKind::TYPES,
_ => return Err(()),
})
}
2 changes: 1 addition & 1 deletion crates/wick/wick-package/tests/wick_package_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ mod integration_test {
let reference = expected
.tagged_reference(&test_timestamp.as_secs().to_string())
.unwrap();
let push_result = package.push(&reference, &options).await;
let push_result = package.push(&reference, Vec::new(), &options).await;
if push_result.is_err() {
panic!("Failed to push WickPackage: {:?}", push_result);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ mod integration_test {
let reference = expected
.tagged_reference(&test_timestamp.as_secs().to_string())
.unwrap();
let push_result = package.push(&reference, &options).await;
let push_result = package.push(&reference, Vec::new(), &options).await;
if push_result.is_err() {
panic!("Failed to push WickPackage: {:?}", push_result);
};
Expand Down
35 changes: 13 additions & 22 deletions src/commands/registry/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,29 +72,20 @@ pub(crate) async fn handle(

let mut lines = Vec::new();

let url = if !opts.tags.is_empty() {
for tag in &opts.tags {
let mut pack = package.clone();
let tagged_reference = pack.tagged_reference(tag).unwrap();
span.in_scope(|| info!(reference = &tagged_reference, "pushing tag"));
pack.push(&tagged_reference, &oci_opts).await?;
lines.push(format!("Pushed tag: {}", reference));
}

// there must be a better way than cloning the package here, feel free to fix it.
let url = package.clone().push(&reference, &oci_opts).await?;

span.in_scope(|| info!(%url, "artifact pushed"));

package.push(&reference, &oci_opts).await?;
url
} else {
package.push(&reference, &oci_opts).await?
};
let json = json!({"url":&url, "tags": opts.tags});
let response = package.push(&reference, opts.tags, &oci_opts).await?;

span.in_scope(|| info!(url=%response.reference, "manifest pushed"));
for tag in &response.tags {
span.in_scope(|| info!(url=%tag, "tag pushed"));
}

span.in_scope(|| info!(%url, "artifact pushed"));
lines.push(format!("Pushed artifact: {}", url));
let json = json!({"manifest_url":&response.manifest_url, reference: &response.reference,"tags": &response.tags});

lines.push(format!("Manifest URL: {}", response.manifest_url));
lines.push(format!("Pushed reference: {}", response.reference));
if !response.tags.is_empty() {
lines.push(format!("Pushed tags:\n{}", response.tags.join("\n")));
}

Ok(StructuredOutput::new(lines.join("\n"), json))
}