feat: implement kernel / initrd oci image support (#103)

* feat: implement kernel / initrd oci image support

* fix: implement image urls more faithfully
This commit is contained in:
Alex Zenla
2024-04-22 12:48:45 -07:00
committed by GitHub
parent 1b90eedbcd
commit 82576df7b7
18 changed files with 470 additions and 262 deletions

1
Cargo.lock generated
View File

@ -1451,6 +1451,7 @@ dependencies = [
"krata", "krata",
"krata-oci", "krata-oci",
"krata-runtime", "krata-runtime",
"krata-tokio-tar",
"log", "log",
"prost", "prost",
"redb", "redb",

View File

@ -64,6 +64,10 @@ pub struct LauchCommand {
help = "Wait for the guest to start, implied by --attach" help = "Wait for the guest to start, implied by --attach"
)] )]
wait: bool, wait: bool,
#[arg(short = 'k', long, help = "OCI kernel image for guest to use")]
kernel: Option<String>,
#[arg(short = 'I', long, help = "OCI initrd image for guest to use")]
initrd: Option<String>,
#[arg(help = "Container image for guest to use")] #[arg(help = "Container image for guest to use")]
oci: String, oci: String,
#[arg( #[arg(
@ -80,27 +84,41 @@ impl LauchCommand {
mut client: ControlServiceClient<Channel>, mut client: ControlServiceClient<Channel>,
events: EventStream, events: EventStream,
) -> Result<()> { ) -> Result<()> {
let response = client let image = self
.pull_image(PullImageRequest { .pull_image(
image: self.oci.clone(), &mut client,
format: match self.image_format { &self.oci,
LaunchImageFormat::Squashfs => OciImageFormat::Squashfs.into(), match self.image_format {
LaunchImageFormat::Erofs => OciImageFormat::Erofs.into(), LaunchImageFormat::Squashfs => OciImageFormat::Squashfs,
LaunchImageFormat::Erofs => OciImageFormat::Erofs,
}, },
overwrite_cache: self.pull_overwrite_cache, )
})
.await?; .await?;
let reply = pull_interactive_progress(response.into_inner()).await?;
let kernel = if let Some(ref kernel) = self.kernel {
let kernel_image = self
.pull_image(&mut client, kernel, OciImageFormat::Tar)
.await?;
Some(kernel_image)
} else {
None
};
let initrd = if let Some(ref initrd) = self.initrd {
let kernel_image = self
.pull_image(&mut client, initrd, OciImageFormat::Tar)
.await?;
Some(kernel_image)
} else {
None
};
let request = CreateGuestRequest { let request = CreateGuestRequest {
spec: Some(GuestSpec { spec: Some(GuestSpec {
name: self.name.unwrap_or_default(), name: self.name.unwrap_or_default(),
image: Some(GuestImageSpec { image: Some(image),
image: Some(Image::Oci(GuestOciImageSpec { kernel,
digest: reply.digest, initrd,
format: reply.format,
})),
}),
vcpus: self.cpus, vcpus: self.cpus,
mem: self.mem, mem: self.mem,
task: Some(GuestTaskSpec { task: Some(GuestTaskSpec {
@ -146,6 +164,28 @@ impl LauchCommand {
StdioConsoleStream::restore_terminal_mode(); StdioConsoleStream::restore_terminal_mode();
std::process::exit(code.unwrap_or(0)); std::process::exit(code.unwrap_or(0));
} }
async fn pull_image(
&self,
client: &mut ControlServiceClient<Channel>,
image: &str,
format: OciImageFormat,
) -> Result<GuestImageSpec> {
let response = client
.pull_image(PullImageRequest {
image: image.to_string(),
format: format.into(),
overwrite_cache: self.pull_overwrite_cache,
})
.await?;
let reply = pull_interactive_progress(response.into_inner()).await?;
Ok(GuestImageSpec {
image: Some(Image::Oci(GuestOciImageSpec {
digest: reply.digest,
format: reply.format,
})),
})
}
} }
async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> { async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> {

View File

@ -27,6 +27,7 @@ scopeguard = { workspace = true }
signal-hook = { workspace = true } signal-hook = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }
krata-tokio-tar = { workspace = true }
tonic = { workspace = true, features = ["tls"] } tonic = { workspace = true, features = ["tls"] }
uuid = { workspace = true } uuid = { workspace = true }

View File

@ -378,7 +378,7 @@ impl ControlService for DaemonControlService {
format: match packed.format { format: match packed.format {
OciPackedFormat::Squashfs => OciImageFormat::Squashfs.into(), OciPackedFormat::Squashfs => OciImageFormat::Squashfs.into(),
OciPackedFormat::Erofs => OciImageFormat::Erofs.into(), OciPackedFormat::Erofs => OciImageFormat::Erofs.into(),
_ => OciImageFormat::Unknown.into(), OciPackedFormat::Tar => OciImageFormat::Tar.into(),
}, },
}; };
yield reply; yield reply;

View File

@ -1,6 +1,6 @@
use std::{net::SocketAddr, path::PathBuf, str::FromStr}; use std::{net::SocketAddr, path::PathBuf, str::FromStr};
use anyhow::Result; use anyhow::{anyhow, Result};
use console::{DaemonConsole, DaemonConsoleHandle}; use console::{DaemonConsole, DaemonConsoleHandle};
use control::DaemonControlService; use control::DaemonControlService;
use db::GuestStore; use db::GuestStore;
@ -74,9 +74,11 @@ impl Daemon {
generated generated
}; };
let packer = OciPackerService::new(None, &image_cache_dir, OciPlatform::current()).await?; let initrd_path = detect_guest_file(&store, "initrd")?;
let kernel_path = detect_guest_file(&store, "kernel")?;
let runtime = Runtime::new(store.clone()).await?; let packer = OciPackerService::new(None, &image_cache_dir, OciPlatform::current()).await?;
let runtime = Runtime::new().await?;
let glt = GuestLookupTable::new(0, host_uuid); let glt = GuestLookupTable::new(0, host_uuid);
let guests_db_path = format!("{}/guests.db", store); let guests_db_path = format!("{}/guests.db", store);
let guests = GuestStore::open(&PathBuf::from(guests_db_path))?; let guests = GuestStore::open(&PathBuf::from(guests_db_path))?;
@ -97,6 +99,8 @@ impl Daemon {
runtime_for_reconciler, runtime_for_reconciler,
packer.clone(), packer.clone(),
guest_reconciler_notify.clone(), guest_reconciler_notify.clone(),
kernel_path,
initrd_path,
)?; )?;
let guest_reconciler_task = guest_reconciler.launch(guest_reconciler_receiver).await?; let guest_reconciler_task = guest_reconciler.launch(guest_reconciler_receiver).await?;
@ -181,3 +185,16 @@ impl Drop for Daemon {
self.generator_task.abort(); self.generator_task.abort();
} }
} }
fn detect_guest_file(store: &str, name: &str) -> Result<PathBuf> {
let mut path = PathBuf::from(format!("{}/guest/{}", store, name));
if path.is_file() {
return Ok(path);
}
path = PathBuf::from(format!("/usr/share/krata/guest/{}", name));
if path.is_file() {
return Ok(path);
}
Err(anyhow!("unable to find required guest file: {}", name))
}

View File

@ -1,20 +1,17 @@
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::{hash_map::Entry, HashMap},
path::PathBuf,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use anyhow::{anyhow, Result}; use anyhow::Result;
use krata::launchcfg::LaunchPackedFormat;
use krata::v1::{ use krata::v1::{
common::{ common::{Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState, GuestState, GuestStatus},
guest_image_spec::Image, Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState,
GuestState, GuestStatus, OciImageFormat,
},
control::GuestChangedEvent, control::GuestChangedEvent,
}; };
use krataoci::packer::{service::OciPackerService, OciPackedFormat}; use krataoci::packer::service::OciPackerService;
use kratart::{launch::GuestLaunchRequest, GuestInfo, Runtime}; use kratart::{GuestInfo, Runtime};
use log::{error, info, trace, warn}; use log::{error, info, trace, warn};
use tokio::{ use tokio::{
select, select,
@ -33,6 +30,10 @@ use crate::{
glt::GuestLookupTable, glt::GuestLookupTable,
}; };
use self::start::GuestStarter;
mod start;
const PARALLEL_LIMIT: u32 = 5; const PARALLEL_LIMIT: u32 = 5;
#[derive(Debug)] #[derive(Debug)]
@ -59,12 +60,15 @@ pub struct GuestReconciler {
events: DaemonEventContext, events: DaemonEventContext,
runtime: Runtime, runtime: Runtime,
packer: OciPackerService, packer: OciPackerService,
kernel_path: PathBuf,
initrd_path: PathBuf,
tasks: Arc<Mutex<HashMap<Uuid, GuestReconcilerEntry>>>, tasks: Arc<Mutex<HashMap<Uuid, GuestReconcilerEntry>>>,
guest_reconciler_notify: Sender<Uuid>, guest_reconciler_notify: Sender<Uuid>,
reconcile_lock: Arc<RwLock<()>>, reconcile_lock: Arc<RwLock<()>>,
} }
impl GuestReconciler { impl GuestReconciler {
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
glt: GuestLookupTable, glt: GuestLookupTable,
guests: GuestStore, guests: GuestStore,
@ -72,6 +76,8 @@ impl GuestReconciler {
runtime: Runtime, runtime: Runtime,
packer: OciPackerService, packer: OciPackerService,
guest_reconciler_notify: Sender<Uuid>, guest_reconciler_notify: Sender<Uuid>,
kernel_path: PathBuf,
initrd_path: PathBuf,
) -> Result<Self> { ) -> Result<Self> {
Ok(Self { Ok(Self {
glt, glt,
@ -79,6 +85,8 @@ impl GuestReconciler {
events, events,
runtime, runtime,
packer, packer,
kernel_path,
initrd_path,
tasks: Arc::new(Mutex::new(HashMap::new())), tasks: Arc::new(Mutex::new(HashMap::new())),
guest_reconciler_notify, guest_reconciler_notify,
reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)), reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)),
@ -246,76 +254,14 @@ impl GuestReconciler {
} }
async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> { async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
let Some(ref spec) = guest.spec else { let starter = GuestStarter {
return Err(anyhow!("guest spec not specified")); kernel_path: &self.kernel_path,
initrd_path: &self.initrd_path,
packer: &self.packer,
glt: &self.glt,
runtime: &self.runtime,
}; };
starter.start(uuid, guest).await
let Some(ref image) = spec.image else {
return Err(anyhow!("image spec not provided"));
};
let oci = match image.image {
Some(Image::Oci(ref oci)) => oci,
None => {
return Err(anyhow!("oci spec not specified"));
}
};
let task = spec.task.as_ref().cloned().unwrap_or_default();
let image = self
.packer
.recall(
&oci.digest,
match oci.format() {
OciImageFormat::Unknown => OciPackedFormat::Squashfs,
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
OciImageFormat::Erofs => OciPackedFormat::Erofs,
OciImageFormat::Tar => {
return Err(anyhow!("tar image format is not supported for guests"));
}
},
)
.await?;
let Some(image) = image else {
return Err(anyhow!(
"image {} in the requested format did not exist",
oci.digest
));
};
let info = self
.runtime
.launch(GuestLaunchRequest {
format: LaunchPackedFormat::Squashfs,
uuid: Some(uuid),
name: if spec.name.is_empty() {
None
} else {
Some(spec.name.clone())
},
image,
vcpus: spec.vcpus,
mem: spec.mem,
env: task
.environment
.iter()
.map(|x| (x.key.clone(), x.value.clone()))
.collect::<HashMap<_, _>>(),
run: empty_vec_optional(task.command.clone()),
debug: false,
})
.await?;
self.glt.associate(uuid, info.domid).await;
info!("started guest {}", uuid);
guest.state = Some(GuestState {
status: GuestStatus::Started.into(),
network: Some(guestinfo_to_networkstate(&info)),
exit_info: None,
error_info: None,
host: self.glt.host_uuid().to_string(),
domid: info.domid,
});
Ok(GuestReconcilerResult::Changed { rerun: false })
} }
async fn exited(&self, guest: &mut Guest) -> Result<GuestReconcilerResult> { async fn exited(&self, guest: &mut Guest) -> Result<GuestReconcilerResult> {
@ -390,15 +336,7 @@ impl GuestReconciler {
} }
} }
fn empty_vec_optional<T>(value: Vec<T>) -> Option<Vec<T>> { pub fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState {
if value.is_empty() {
None
} else {
Some(value)
}
}
fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState {
GuestNetworkState { GuestNetworkState {
guest_ipv4: info.guest_ipv4.map(|x| x.to_string()).unwrap_or_default(), guest_ipv4: info.guest_ipv4.map(|x| x.to_string()).unwrap_or_default(),
guest_ipv6: info.guest_ipv6.map(|x| x.to_string()).unwrap_or_default(), guest_ipv6: info.guest_ipv6.map(|x| x.to_string()).unwrap_or_default(),

View File

@ -0,0 +1,182 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use futures::StreamExt;
use krata::launchcfg::LaunchPackedFormat;
use krata::v1::common::GuestOciImageSpec;
use krata::v1::common::{guest_image_spec::Image, Guest, GuestState, GuestStatus, OciImageFormat};
use krataoci::packer::{service::OciPackerService, OciPackedFormat};
use kratart::{launch::GuestLaunchRequest, Runtime};
use log::info;
use tokio::fs::{self, File};
use tokio::io::AsyncReadExt;
use tokio_tar::Archive;
use uuid::Uuid;
use crate::{
glt::GuestLookupTable,
reconcile::guest::{guestinfo_to_networkstate, GuestReconcilerResult},
};
// if a kernel is >= 100MB, that's kinda scary.
const OCI_SPEC_TAR_FILE_MAX_SIZE: usize = 100 * 1024 * 1024;
pub struct GuestStarter<'a> {
pub kernel_path: &'a Path,
pub initrd_path: &'a Path,
pub packer: &'a OciPackerService,
pub glt: &'a GuestLookupTable,
pub runtime: &'a Runtime,
}
impl GuestStarter<'_> {
pub async fn oci_spec_tar_read_file(
&self,
file: &Path,
oci: &GuestOciImageSpec,
) -> Result<Vec<u8>> {
if oci.format() != OciImageFormat::Tar {
return Err(anyhow!(
"oci image spec for {} is required to be in tar format",
oci.digest
));
}
let image = self
.packer
.recall(&oci.digest, OciPackedFormat::Tar)
.await?;
let Some(image) = image else {
return Err(anyhow!("image {} was not found in tar format", oci.digest));
};
let mut archive = Archive::new(File::open(&image.path).await?);
let mut entries = archive.entries()?;
while let Some(entry) = entries.next().await {
let mut entry = entry?;
let path = entry.path()?;
if entry.header().size()? as usize > OCI_SPEC_TAR_FILE_MAX_SIZE {
return Err(anyhow!(
"file {} in image {} is larger than the size limit",
file.to_string_lossy(),
oci.digest
));
}
if path == file {
let mut buffer = Vec::new();
entry.read_to_end(&mut buffer).await?;
return Ok(buffer);
}
}
Err(anyhow!(
"unable to find file {} in image {}",
file.to_string_lossy(),
oci.digest
))
}
pub async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
let Some(ref spec) = guest.spec else {
return Err(anyhow!("guest spec not specified"));
};
let Some(ref image) = spec.image else {
return Err(anyhow!("image spec not provided"));
};
let oci = match image.image {
Some(Image::Oci(ref oci)) => oci,
None => {
return Err(anyhow!("oci spec not specified"));
}
};
let task = spec.task.as_ref().cloned().unwrap_or_default();
let image = self
.packer
.recall(
&oci.digest,
match oci.format() {
OciImageFormat::Unknown => OciPackedFormat::Squashfs,
OciImageFormat::Squashfs => OciPackedFormat::Squashfs,
OciImageFormat::Erofs => OciPackedFormat::Erofs,
OciImageFormat::Tar => {
return Err(anyhow!("tar image format is not supported for guests"));
}
},
)
.await?;
let Some(image) = image else {
return Err(anyhow!(
"image {} in the requested format did not exist",
oci.digest
));
};
let kernel = if let Some(ref spec) = spec.kernel {
let Some(Image::Oci(ref oci)) = spec.image else {
return Err(anyhow!("kernel image spec must be an oci image"));
};
self.oci_spec_tar_read_file(&PathBuf::from("kernel/image"), oci)
.await?
} else {
fs::read(&self.kernel_path).await?
};
let initrd = if let Some(ref spec) = spec.initrd {
let Some(Image::Oci(ref oci)) = spec.image else {
return Err(anyhow!("initrd image spec must be an oci image"));
};
self.oci_spec_tar_read_file(&PathBuf::from("krata/initrd"), oci)
.await?
} else {
fs::read(&self.initrd_path).await?
};
let info = self
.runtime
.launch(GuestLaunchRequest {
format: LaunchPackedFormat::Squashfs,
uuid: Some(uuid),
name: if spec.name.is_empty() {
None
} else {
Some(spec.name.clone())
},
image,
kernel,
initrd,
vcpus: spec.vcpus,
mem: spec.mem,
env: task
.environment
.iter()
.map(|x| (x.key.clone(), x.value.clone()))
.collect::<HashMap<_, _>>(),
run: empty_vec_optional(task.command.clone()),
debug: false,
})
.await?;
self.glt.associate(uuid, info.domid).await;
info!("started guest {}", uuid);
guest.state = Some(GuestState {
status: GuestStatus::Started.into(),
network: Some(guestinfo_to_networkstate(&info)),
exit_info: None,
error_info: None,
host: self.glt.host_uuid().to_string(),
domid: info.domid,
});
Ok(GuestReconcilerResult::Changed { rerun: false })
}
}
fn empty_vec_optional<T>(value: Vec<T>) -> Option<Vec<T>> {
if value.is_empty() {
None
} else {
Some(value)
}
}

View File

@ -17,10 +17,14 @@ message Guest {
message GuestSpec { message GuestSpec {
string name = 1; string name = 1;
GuestImageSpec image = 2; GuestImageSpec image = 2;
uint32 vcpus = 3; // If not specified, defaults to the daemon default kernel.
uint64 mem = 4; GuestImageSpec kernel = 3;
GuestTaskSpec task = 5; // If not specified, defaults to the daemon default initrd.
repeated GuestSpecAnnotation annotations = 6; GuestImageSpec initrd = 4;
uint32 vcpus = 5;
uint64 mem = 6;
GuestTaskSpec task = 7;
repeated GuestSpecAnnotation annotations = 8;
} }
message GuestImageSpec { message GuestImageSpec {

View File

@ -217,6 +217,13 @@ impl OciImageFetcher {
continue; continue;
} }
} }
if let Some(ref digest) = image.digest {
if digest != manifest.digest() {
continue;
}
}
found = Some(manifest); found = Some(manifest);
break; break;
} }
@ -240,7 +247,7 @@ impl OciImageFetcher {
let mut client = OciRegistryClient::new(image.registry_url()?, self.platform.clone())?; let mut client = OciRegistryClient::new(image.registry_url()?, self.platform.clone())?;
let (manifest, descriptor, digest) = client let (manifest, descriptor, digest) = client
.get_manifest_with_digest(&image.name, &image.reference) .get_manifest_with_digest(&image.name, image.reference.as_ref(), image.digest.as_ref())
.await?; .await?;
let descriptor = descriptor.unwrap_or_else(|| { let descriptor = descriptor.unwrap_or_else(|| {
DescriptorBuilder::default() DescriptorBuilder::default()

View File

@ -2,33 +2,39 @@ use anyhow::Result;
use std::fmt; use std::fmt;
use url::Url; use url::Url;
const DOCKER_HUB_MIRROR: &str = "mirror.gcr.io";
const DEFAULT_IMAGE_TAG: &str = "latest";
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ImageName { pub struct ImageName {
pub hostname: String, pub hostname: String,
pub port: Option<u16>, pub port: Option<u16>,
pub name: String, pub name: String,
pub reference: String, pub reference: Option<String>,
pub digest: Option<String>,
} }
impl fmt::Display for ImageName { impl fmt::Display for ImageName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() { let mut suffix = String::new();
if let Some(ref reference) = self.reference {
suffix.push(':');
suffix.push_str(reference);
}
if let Some(ref digest) = self.digest {
suffix.push('@');
suffix.push_str(digest);
}
if ImageName::DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() {
if self.name.starts_with("library/") { if self.name.starts_with("library/") {
write!(f, "{}:{}", &self.name[8..], self.reference) write!(f, "{}{}", &self.name[8..], suffix)
} else { } else {
write!(f, "{}:{}", self.name, self.reference) write!(f, "{}{}", self.name, suffix)
} }
} else if let Some(port) = self.port { } else if let Some(port) = self.port {
write!( write!(f, "{}:{}/{}{}", self.hostname, port, self.name, suffix)
f,
"{}:{}/{}:{}",
self.hostname, port, self.name, self.reference
)
} else { } else {
write!(f, "{}/{}:{}", self.hostname, self.name, self.reference) write!(f, "{}/{}{}", self.hostname, self.name, suffix)
} }
} }
} }
@ -41,13 +47,21 @@ impl Default for ImageName {
} }
impl ImageName { impl ImageName {
pub const DOCKER_HUB_MIRROR: &'static str = "registry.docker.io";
pub const DEFAULT_IMAGE_TAG: &'static str = "latest";
pub fn parse(name: &str) -> Result<Self> { pub fn parse(name: &str) -> Result<Self> {
let full_name = name.to_string(); let full_name = name.to_string();
let name = full_name.clone(); let name = full_name.clone();
let (mut hostname, mut name) = name let (mut hostname, mut name) = name
.split_once('/') .split_once('/')
.map(|x| (x.0.to_string(), x.1.to_string())) .map(|x| (x.0.to_string(), x.1.to_string()))
.unwrap_or_else(|| (DOCKER_HUB_MIRROR.to_string(), format!("library/{}", name))); .unwrap_or_else(|| {
(
ImageName::DOCKER_HUB_MIRROR.to_string(),
format!("library/{}", name),
)
});
// heuristic to find any docker hub image formats // heuristic to find any docker hub image formats
// that may be in the hostname format. for example: // that may be in the hostname format. for example:
@ -55,7 +69,7 @@ impl ImageName {
// and neither will abc/hello/xyz:latest // and neither will abc/hello/xyz:latest
if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 { if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 {
name = format!("{}/{}", hostname, name); name = format!("{}/{}", hostname, name);
hostname = DOCKER_HUB_MIRROR.to_string(); hostname = ImageName::DOCKER_HUB_MIRROR.to_string();
} }
let (hostname, port) = if let Some((hostname, port)) = hostname let (hostname, port) = if let Some((hostname, port)) = hostname
@ -66,15 +80,54 @@ impl ImageName {
} else { } else {
(hostname, None) (hostname, None)
}; };
let (name, reference) = name
.split_once(':') let name_has_digest = if name.contains('@') {
.map(|x| (x.0.to_string(), x.1.to_string())) let digest_start = name.chars().position(|c| c == '@');
.unwrap_or((name.to_string(), DEFAULT_IMAGE_TAG.to_string())); let ref_start = name.chars().position(|c| c == ':');
if let (Some(digest_start), Some(ref_start)) = (digest_start, ref_start) {
digest_start < ref_start
} else {
true
}
} else {
false
};
let (name, digest) = if name_has_digest {
name.split_once('@')
.map(|(name, digest)| (name.to_string(), Some(digest.to_string())))
.unwrap_or_else(|| (name, None))
} else {
(name, None)
};
let (name, reference) = if name.contains(':') {
name.split_once(':')
.map(|(name, reference)| (name.to_string(), Some(reference.to_string())))
.unwrap_or((name, None))
} else {
(name, None)
};
let (reference, digest) = if let Some(reference) = reference {
if let Some(digest) = digest {
(Some(reference), Some(digest))
} else {
reference
.split_once('@')
.map(|(reff, digest)| (Some(reff.to_string()), Some(digest.to_string())))
.unwrap_or_else(|| (Some(reference), None))
}
} else {
(None, digest)
};
Ok(ImageName { Ok(ImageName {
hostname, hostname,
port, port,
name, name,
reference, reference,
digest,
}) })
} }

View File

@ -159,7 +159,9 @@ impl OciPackerCache {
let image_name = packed.name.to_string(); let image_name = packed.name.to_string();
annotations.insert(ANNOTATION_IMAGE_NAME.to_string(), image_name); annotations.insert(ANNOTATION_IMAGE_NAME.to_string(), image_name);
let image_ref = packed.name.reference.clone(); let image_ref = packed.name.reference.clone();
if let Some(image_ref) = image_ref {
annotations.insert(ANNOTATION_REF_NAME.to_string(), image_ref); annotations.insert(ANNOTATION_REF_NAME.to_string(), image_ref);
}
descriptor.set_annotations(Some(annotations)); descriptor.set_annotations(Some(annotations));
manifests.push(descriptor.clone()); manifests.push(descriptor.clone());
index.set_manifests(manifests); index.set_manifests(manifests);

View File

@ -61,6 +61,10 @@ impl OciPackerService {
digest: &str, digest: &str,
format: OciPackedFormat, format: OciPackedFormat,
) -> Result<Option<OciPackedImage>> { ) -> Result<Option<OciPackedImage>> {
if digest.contains('/') || digest.contains('\\') || digest.contains("..") {
return Ok(None);
}
self.cache self.cache
.recall(ImageName::parse("cached:latest")?, digest, format) .recall(ImageName::parse("cached:latest")?, digest, format)
.await .await

View File

@ -7,7 +7,7 @@ use reqwest::{Client, RequestBuilder, Response, StatusCode};
use tokio::{fs::File, io::AsyncWriteExt}; use tokio::{fs::File, io::AsyncWriteExt};
use url::Url; use url::Url;
use crate::{progress::OciBoundProgress, schema::OciSchema}; use crate::{name::ImageName, progress::OciBoundProgress, schema::OciSchema};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct OciPlatform { pub struct OciPlatform {
@ -176,7 +176,7 @@ impl OciRegistryClient {
let url = self.url.join(&format!( let url = self.url.join(&format!(
"/v2/{}/manifests/{}", "/v2/{}/manifests/{}",
name.as_ref(), name.as_ref(),
reference.as_ref() reference.as_ref(),
))?; ))?;
let accept = format!( let accept = format!(
"{}, {}, {}, {}", "{}, {}, {}, {}",
@ -202,13 +202,20 @@ impl OciRegistryClient {
pub async fn get_manifest_with_digest<N: AsRef<str>, R: AsRef<str>>( pub async fn get_manifest_with_digest<N: AsRef<str>, R: AsRef<str>>(
&mut self, &mut self,
name: N, name: N,
reference: R, reference: Option<R>,
digest: Option<N>,
) -> Result<(OciSchema<ImageManifest>, Option<Descriptor>, String)> { ) -> Result<(OciSchema<ImageManifest>, Option<Descriptor>, String)> {
let url = self.url.join(&format!( let what = digest
"/v2/{}/manifests/{}", .as_ref()
name.as_ref(), .map(|x| x.as_ref().to_string())
reference.as_ref() .unwrap_or_else(|| {
))?; reference
.map(|x| x.as_ref().to_string())
.unwrap_or_else(|| ImageName::DEFAULT_IMAGE_TAG.to_string())
});
let url = self
.url
.join(&format!("/v2/{}/manifests/{}", name.as_ref(), what,))?;
let accept = format!( let accept = format!(
"{}, {}, {}, {}", "{}, {}, {}, {}",
MediaType::ImageManifest.to_docker_v2s2()?, MediaType::ImageManifest.to_docker_v2s2()?,
@ -239,9 +246,10 @@ impl OciRegistryClient {
let digest = response let digest = response
.headers() .headers()
.get("Docker-Content-Digest") .get("Docker-Content-Digest")
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))? .and_then(|x| x.to_str().ok())
.to_str()? .map(|x| x.to_string())
.to_string(); .or_else(|| digest.map(|x: N| x.as_ref().to_string()))
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?;
let bytes = response.bytes().await?; let bytes = response.bytes().await?;
let manifest = serde_json::from_slice(&bytes)?; let manifest = serde_json::from_slice(&bytes)?;
Ok((OciSchema::new(bytes.to_vec(), manifest), None, digest)) Ok((OciSchema::new(bytes.to_vec(), manifest), None, digest))

View File

@ -23,6 +23,8 @@ use super::{GuestInfo, GuestState};
pub struct GuestLaunchRequest { pub struct GuestLaunchRequest {
pub format: LaunchPackedFormat, pub format: LaunchPackedFormat,
pub kernel: Vec<u8>,
pub initrd: Vec<u8>,
pub uuid: Option<Uuid>, pub uuid: Option<Uuid>,
pub name: Option<String>, pub name: Option<String>,
pub vcpus: u32, pub vcpus: u32,
@ -173,22 +175,22 @@ impl GuestLauncher {
let config = DomainConfig { let config = DomainConfig {
backend_domid: 0, backend_domid: 0,
name: &xen_name, name: xen_name,
max_vcpus: request.vcpus, max_vcpus: request.vcpus,
mem_mb: request.mem, mem_mb: request.mem,
kernel_path: &context.kernel, kernel: request.kernel,
initrd_path: &context.initrd, initrd: request.initrd,
cmdline: &cmdline, cmdline,
use_console_backend: Some("krata-console"), use_console_backend: Some("krata-console".to_string()),
disks: vec![ disks: vec![
DomainDisk { DomainDisk {
vdev: "xvda", vdev: "xvda".to_string(),
block: &image_squashfs_loop, block: image_squashfs_loop.clone(),
writable: false, writable: false,
}, },
DomainDisk { DomainDisk {
vdev: "xvdb", vdev: "xvdb".to_string(),
block: &cfgblk_squashfs_loop, block: cfgblk_squashfs_loop.clone(),
writable: false, writable: false,
}, },
], ],
@ -197,7 +199,7 @@ impl GuestLauncher {
initialized: false, initialized: false,
}], }],
vifs: vec![DomainNetworkInterface { vifs: vec![DomainNetworkInterface {
mac: &guest_mac_string, mac: guest_mac_string.clone(),
mtu: 1500, mtu: 1500,
bridge: None, bridge: None,
script: None, script: None,

View File

@ -1,9 +1,4 @@
use std::{ use std::{fs, path::PathBuf, str::FromStr, sync::Arc};
fs,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
@ -52,43 +47,17 @@ pub struct GuestInfo {
pub struct RuntimeContext { pub struct RuntimeContext {
pub autoloop: AutoLoop, pub autoloop: AutoLoop,
pub xen: XenClient, pub xen: XenClient,
pub kernel: String,
pub initrd: String,
} }
impl RuntimeContext { impl RuntimeContext {
pub async fn new(store: String) -> Result<Self> { pub async fn new() -> Result<Self> {
let mut image_cache_path = PathBuf::from(&store);
image_cache_path.push("cache");
fs::create_dir_all(&image_cache_path)?;
let xen = XenClient::open(0).await?; let xen = XenClient::open(0).await?;
image_cache_path.push("image");
fs::create_dir_all(&image_cache_path)?;
let kernel = RuntimeContext::detect_guest_file(&store, "kernel")?;
let initrd = RuntimeContext::detect_guest_file(&store, "initrd")?;
Ok(RuntimeContext { Ok(RuntimeContext {
autoloop: AutoLoop::new(LoopControl::open()?), autoloop: AutoLoop::new(LoopControl::open()?),
xen, xen,
kernel,
initrd,
}) })
} }
fn detect_guest_file(store: &str, name: &str) -> Result<String> {
let mut path = PathBuf::from(format!("{}/guest/{}", store, name));
if path.is_file() {
return path_as_string(&path);
}
path = PathBuf::from(format!("/usr/share/krata/guest/{}", name));
if path.is_file() {
return path_as_string(&path);
}
Err(anyhow!("unable to find required guest file: {}", name))
}
pub async fn list(&self) -> Result<Vec<GuestInfo>> { pub async fn list(&self) -> Result<Vec<GuestInfo>> {
let mut guests: Vec<GuestInfo> = Vec::new(); let mut guests: Vec<GuestInfo> = Vec::new();
for domid_candidate in self.xen.store.list("/local/domain").await? { for domid_candidate in self.xen.store.list("/local/domain").await? {
@ -248,16 +217,14 @@ impl RuntimeContext {
#[derive(Clone)] #[derive(Clone)]
pub struct Runtime { pub struct Runtime {
store: Arc<String>,
context: RuntimeContext, context: RuntimeContext,
launch_semaphore: Arc<Semaphore>, launch_semaphore: Arc<Semaphore>,
} }
impl Runtime { impl Runtime {
pub async fn new(store: String) -> Result<Self> { pub async fn new() -> Result<Self> {
let context = RuntimeContext::new(store.clone()).await?; let context = RuntimeContext::new().await?;
Ok(Self { Ok(Self {
store: Arc::new(store),
context, context,
launch_semaphore: Arc::new(Semaphore::new(1)), launch_semaphore: Arc::new(Semaphore::new(1)),
}) })
@ -320,12 +287,6 @@ impl Runtime {
} }
pub async fn dupe(&self) -> Result<Runtime> { pub async fn dupe(&self) -> Result<Runtime> {
Runtime::new((*self.store).clone()).await Runtime::new().await
} }
} }
fn path_as_string(path: &Path) -> Result<String> {
path.to_str()
.ok_or_else(|| anyhow!("unable to convert path to string"))
.map(|x| x.to_string())
}

View File

@ -1,4 +1,5 @@
use std::{env, process}; use std::{env, process};
use tokio::fs;
use xenclient::error::Result; use xenclient::error::Result;
use xenclient::{DomainConfig, XenClient}; use xenclient::{DomainConfig, XenClient};
@ -16,12 +17,12 @@ async fn main() -> Result<()> {
let client = XenClient::open(0).await?; let client = XenClient::open(0).await?;
let config = DomainConfig { let config = DomainConfig {
backend_domid: 0, backend_domid: 0,
name: "xenclient-test", name: "xenclient-test".to_string(),
max_vcpus: 1, max_vcpus: 1,
mem_mb: 512, mem_mb: 512,
kernel_path: kernel_image_path.as_str(), kernel: fs::read(&kernel_image_path).await?,
initrd_path: initrd_path.as_str(), initrd: fs::read(&initrd_path).await?,
cmdline: "debug elevator=noop", cmdline: "debug elevator=noop".to_string(),
use_console_backend: None, use_console_backend: None,
disks: vec![], disks: vec![],
channels: vec![], channels: vec![],

View File

@ -107,17 +107,15 @@ impl ElfImageLoader {
ElfImageLoader::load_xz(file.as_slice()) ElfImageLoader::load_xz(file.as_slice())
} }
pub fn load_file_kernel(path: &str) -> Result<ElfImageLoader> { pub fn load_file_kernel(data: &[u8]) -> Result<ElfImageLoader> {
let file = std::fs::read(path)?; for start in find_iter(data, &[0x1f, 0x8b]) {
if let Ok(elf) = ElfImageLoader::load_gz(&data[start..]) {
for start in find_iter(file.as_slice(), &[0x1f, 0x8b]) {
if let Ok(elf) = ElfImageLoader::load_gz(&file[start..]) {
return Ok(elf); return Ok(elf);
} }
} }
for start in find_iter(file.as_slice(), &[0xfd, 0x37, 0x7a, 0x58]) { for start in find_iter(data, &[0xfd, 0x37, 0x7a, 0x58]) {
if let Ok(elf) = ElfImageLoader::load_xz(&file[start..]) { if let Ok(elf) = ElfImageLoader::load_xz(&data[start..]) {
return Ok(elf); return Ok(elf);
} }
} }

View File

@ -23,7 +23,6 @@ use boot::BootState;
use log::{debug, trace, warn}; use log::{debug, trace, warn};
use tokio::time::timeout; use tokio::time::timeout;
use std::fs::read;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
@ -40,60 +39,60 @@ pub struct XenClient {
call: XenCall, call: XenCall,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct BlockDeviceRef { pub struct BlockDeviceRef {
pub path: String, pub path: String,
pub major: u32, pub major: u32,
pub minor: u32, pub minor: u32,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct DomainDisk<'a> { pub struct DomainDisk {
pub vdev: &'a str, pub vdev: String,
pub block: &'a BlockDeviceRef, pub block: BlockDeviceRef,
pub writable: bool, pub writable: bool,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct DomainFilesystem<'a> { pub struct DomainFilesystem {
pub path: &'a str, pub path: String,
pub tag: &'a str, pub tag: String,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct DomainNetworkInterface<'a> { pub struct DomainNetworkInterface {
pub mac: &'a str, pub mac: String,
pub mtu: u32, pub mtu: u32,
pub bridge: Option<&'a str>, pub bridge: Option<String>,
pub script: Option<&'a str>, pub script: Option<String>,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct DomainChannel { pub struct DomainChannel {
pub typ: String, pub typ: String,
pub initialized: bool, pub initialized: bool,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct DomainEventChannel<'a> { pub struct DomainEventChannel {
pub name: &'a str, pub name: String,
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct DomainConfig<'a> { pub struct DomainConfig {
pub backend_domid: u32, pub backend_domid: u32,
pub name: &'a str, pub name: String,
pub max_vcpus: u32, pub max_vcpus: u32,
pub mem_mb: u64, pub mem_mb: u64,
pub kernel_path: &'a str, pub kernel: Vec<u8>,
pub initrd_path: &'a str, pub initrd: Vec<u8>,
pub cmdline: &'a str, pub cmdline: String,
pub disks: Vec<DomainDisk<'a>>, pub disks: Vec<DomainDisk>,
pub use_console_backend: Option<&'a str>, pub use_console_backend: Option<String>,
pub channels: Vec<DomainChannel>, pub channels: Vec<DomainChannel>,
pub vifs: Vec<DomainNetworkInterface<'a>>, pub vifs: Vec<DomainNetworkInterface>,
pub filesystems: Vec<DomainFilesystem<'a>>, pub filesystems: Vec<DomainFilesystem>,
pub event_channels: Vec<DomainEventChannel<'a>>, pub event_channels: Vec<DomainEventChannel>,
pub extra_keys: Vec<(String, String)>, pub extra_keys: Vec<(String, String)>,
pub extra_rw_paths: Vec<String>, pub extra_rw_paths: Vec<String>,
} }
@ -117,7 +116,7 @@ impl XenClient {
Ok(XenClient { store, call }) Ok(XenClient { store, call })
} }
pub async fn create(&self, config: &DomainConfig<'_>) -> Result<CreatedDomain> { pub async fn create(&self, config: &DomainConfig) -> Result<CreatedDomain> {
let mut domain = CreateDomain { let mut domain = CreateDomain {
max_vcpus: config.max_vcpus, max_vcpus: config.max_vcpus,
..Default::default() ..Default::default()
@ -143,7 +142,7 @@ impl XenClient {
&self, &self,
domid: u32, domid: u32,
domain: &CreateDomain, domain: &CreateDomain,
config: &DomainConfig<'_>, config: &DomainConfig,
) -> Result<CreatedDomain> { ) -> Result<CreatedDomain> {
trace!( trace!(
"XenClient init domid={} domain={:?} config={:?}", "XenClient init domid={} domain={:?} config={:?}",
@ -237,9 +236,9 @@ impl XenClient {
&Uuid::from_bytes(domain.handle).to_string(), &Uuid::from_bytes(domain.handle).to_string(),
) )
.await?; .await?;
tx.write_string(format!("{}/name", dom_path).as_str(), config.name) tx.write_string(format!("{}/name", dom_path).as_str(), &config.name)
.await?; .await?;
tx.write_string(format!("{}/name", vm_path).as_str(), config.name) tx.write_string(format!("{}/name", vm_path).as_str(), &config.name)
.await?; .await?;
for (key, value) in &config.extra_keys { for (key, value) in &config.extra_keys {
@ -257,7 +256,7 @@ impl XenClient {
self.call.set_max_vcpus(domid, config.max_vcpus).await?; self.call.set_max_vcpus(domid, config.max_vcpus).await?;
self.call.set_max_mem(domid, config.mem_mb * 1024).await?; self.call.set_max_mem(domid, config.mem_mb * 1024).await?;
let image_loader = ElfImageLoader::load_file_kernel(config.kernel_path)?; let image_loader = ElfImageLoader::load_file_kernel(&config.kernel)?;
let xenstore_evtchn: u32; let xenstore_evtchn: u32;
let xenstore_mfn: u64; let xenstore_mfn: u64;
@ -270,18 +269,17 @@ impl XenClient {
let mut arch = Box::new(X86BootSetup::new()) as Box<dyn ArchBootSetup + Send + Sync>; let mut arch = Box::new(X86BootSetup::new()) as Box<dyn ArchBootSetup + Send + Sync>;
#[cfg(target_arch = "aarch64")] #[cfg(target_arch = "aarch64")]
let mut arch = Box::new(Arm64BootSetup::new()) as Box<dyn ArchBootSetup + Send + Sync>; let mut arch = Box::new(Arm64BootSetup::new()) as Box<dyn ArchBootSetup + Send + Sync>;
let initrd = read(config.initrd_path)?;
state = boot state = boot
.initialize( .initialize(
&mut arch, &mut arch,
&image_loader, &image_loader,
initrd.as_slice(), &config.initrd,
config.max_vcpus, config.max_vcpus,
config.mem_mb, config.mem_mb,
1, 1,
) )
.await?; .await?;
boot.boot(&mut arch, &mut state, config.cmdline).await?; boot.boot(&mut arch, &mut state, &config.cmdline).await?;
xenstore_evtchn = state.store_evtchn; xenstore_evtchn = state.store_evtchn;
xenstore_mfn = boot.phys.p2m[state.xenstore_segment.pfn as usize]; xenstore_mfn = boot.phys.p2m[state.xenstore_segment.pfn as usize];
p2m = boot.phys.p2m; p2m = boot.phys.p2m;
@ -291,19 +289,9 @@ impl XenClient {
let tx = self.store.transaction().await?; let tx = self.store.transaction().await?;
tx.write_string(format!("{}/image/os_type", vm_path).as_str(), "linux") tx.write_string(format!("{}/image/os_type", vm_path).as_str(), "linux")
.await?; .await?;
tx.write_string(
format!("{}/image/kernel", vm_path).as_str(),
config.kernel_path,
)
.await?;
tx.write_string(
format!("{}/image/ramdisk", vm_path).as_str(),
config.initrd_path,
)
.await?;
tx.write_string( tx.write_string(
format!("{}/image/cmdline", vm_path).as_str(), format!("{}/image/cmdline", vm_path).as_str(),
config.cmdline, &config.cmdline,
) )
.await?; .await?;
@ -352,7 +340,8 @@ impl XenClient {
&DomainChannel { &DomainChannel {
typ: config typ: config
.use_console_backend .use_console_backend
.unwrap_or("xenconsoled") .clone()
.unwrap_or("xenconsoled".to_string())
.to_string(), .to_string(),
initialized: true, initialized: true,
}, },
@ -429,7 +418,7 @@ impl XenClient {
.await?; .await?;
let channel_path = format!("{}/evtchn/{}", dom_path, channel.name); let channel_path = format!("{}/evtchn/{}", dom_path, channel.name);
self.store self.store
.write_string(&format!("{}/name", channel_path), channel.name) .write_string(&format!("{}/name", channel_path), &channel.name)
.await?; .await?;
self.store self.store
.write_string(&format!("{}/channel", channel_path), &id.to_string()) .write_string(&format!("{}/channel", channel_path), &id.to_string())
@ -447,7 +436,7 @@ impl XenClient {
backend_domid: u32, backend_domid: u32,
domid: u32, domid: u32,
index: usize, index: usize,
disk: &DomainDisk<'_>, disk: &DomainDisk,
) -> Result<()> { ) -> Result<()> {
let id = (202 << 8) | (index << 4) as u64; let id = (202 << 8) | (index << 4) as u64;
let backend_items: Vec<(&str, String)> = vec![ let backend_items: Vec<(&str, String)> = vec![
@ -567,7 +556,7 @@ impl XenClient {
backend_domid: u32, backend_domid: u32,
domid: u32, domid: u32,
index: usize, index: usize,
filesystem: &DomainFilesystem<'_>, filesystem: &DomainFilesystem,
) -> Result<()> { ) -> Result<()> {
let id = 90 + index as u64; let id = 90 + index as u64;
let backend_items: Vec<(&str, String)> = vec![ let backend_items: Vec<(&str, String)> = vec![
@ -605,7 +594,7 @@ impl XenClient {
backend_domid: u32, backend_domid: u32,
domid: u32, domid: u32,
index: usize, index: usize,
vif: &DomainNetworkInterface<'_>, vif: &DomainNetworkInterface,
) -> Result<()> { ) -> Result<()> {
let id = 20 + index as u64; let id = 20 + index as u64;
let mut backend_items: Vec<(&str, String)> = vec![ let mut backend_items: Vec<(&str, String)> = vec![
@ -619,12 +608,12 @@ impl XenClient {
]; ];
if vif.bridge.is_some() { if vif.bridge.is_some() {
backend_items.extend_from_slice(&[("bridge", vif.bridge.unwrap().to_string())]); backend_items.extend_from_slice(&[("bridge", vif.bridge.clone().unwrap())]);
} }
if vif.script.is_some() { if vif.script.is_some() {
backend_items.extend_from_slice(&[ backend_items.extend_from_slice(&[
("script", vif.script.unwrap().to_string()), ("script", vif.script.clone().unwrap()),
("hotplug-status", "".to_string()), ("hotplug-status", "".to_string()),
]); ]);
} else { } else {