controller: make image downloads async

This commit is contained in:
Alex Zenla
2024-02-25 05:38:23 +00:00
parent f66ab25521
commit 973db87b98
8 changed files with 112 additions and 66 deletions

View File

@ -72,6 +72,9 @@ features = ["derive"]
version = "1.35.1" version = "1.35.1"
features = ["macros", "rt", "rt-multi-thread"] features = ["macros", "rt", "rt-multi-thread"]
[workspace.dependencies.reqwest]
version = "0.11.24"
[workspace.dependencies.serde] [workspace.dependencies.serde]
version = "1.0.196" version = "1.0.196"
features = ["derive"] features = ["derive"]

View File

@ -18,6 +18,7 @@ serde_json = { workspace = true }
sha256 = { workspace = true } sha256 = { workspace = true }
url = { workspace = true } url = { workspace = true }
ureq = { workspace = true } ureq = { workspace = true }
reqwest = { workspace = true }
path-clean = { workspace = true } path-clean = { workspace = true }
termion = { workspace = true } termion = { workspace = true }
cli-tables = { workspace = true } cli-tables = { workspace = true }
@ -28,6 +29,7 @@ uuid = { workspace = true }
ipnetwork = { workspace = true } ipnetwork = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
bytes = { workspace = true }
[dependencies.krata] [dependencies.krata]
path = "../shared" path = "../shared"

View File

@ -37,7 +37,7 @@ impl ControllerLaunch<'_> {
pub async fn perform(&mut self, request: ControllerLaunchRequest<'_>) -> Result<(Uuid, u32)> { pub async fn perform(&mut self, request: ControllerLaunchRequest<'_>) -> Result<(Uuid, u32)> {
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let name = format!("krata-{uuid}"); let name = format!("krata-{uuid}");
let image_info = self.compile(request.image)?; let image_info = self.compile(request.image).await?;
let mut gateway_mac = MacAddr6::random(); let mut gateway_mac = MacAddr6::random();
gateway_mac.set_local(true); gateway_mac.set_local(true);
@ -220,9 +220,9 @@ impl ControllerLaunch<'_> {
Ok(found.unwrap()) Ok(found.unwrap())
} }
fn compile(&self, image: &str) -> Result<ImageInfo> { async fn compile(&self, image: &str) -> Result<ImageInfo> {
let image = ImageName::parse(image)?; let image = ImageName::parse(image)?;
let compiler = ImageCompiler::new(&self.context.image_cache)?; let compiler = ImageCompiler::new(&self.context.image_cache)?;
compiler.compile(&image) compiler.compile(&image).await
} }
} }

View File

@ -1,56 +1,57 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use bytes::Bytes;
use oci_spec::image::{Arch, Descriptor, ImageIndex, ImageManifest, MediaType, Os, ToDockerV2S2}; use oci_spec::image::{Arch, Descriptor, ImageIndex, ImageManifest, MediaType, Os, ToDockerV2S2};
use std::io::copy; use reqwest::{Client, RequestBuilder, Response};
use std::io::{Read, Write}; use tokio::{fs::File, io::AsyncWriteExt};
use std::ops::DerefMut;
use ureq::{Agent, Request, Response};
use url::Url; use url::Url;
const MANIFEST_PICKER_PLATFORM: Os = Os::Linux; const MANIFEST_PICKER_PLATFORM: Os = Os::Linux;
const MANIFEST_PICKER_ARCHITECTURE: Arch = Arch::Amd64; const MANIFEST_PICKER_ARCHITECTURE: Arch = Arch::Amd64;
pub struct RegistryClient { pub struct RegistryClient {
agent: Agent, agent: Client,
url: Url, url: Url,
} }
impl RegistryClient { impl RegistryClient {
pub fn new(url: Url) -> Result<RegistryClient> { pub fn new(url: Url) -> Result<RegistryClient> {
Ok(RegistryClient { Ok(RegistryClient {
agent: Agent::new(), agent: Client::new(),
url, url,
}) })
} }
fn call(&mut self, req: Request) -> Result<Response> { async fn call(&mut self, req: RequestBuilder) -> Result<Response> {
Ok(req.call()?) self.agent.execute(req.build()?).await.map_err(|x| x.into())
} }
pub fn get_blob(&mut self, name: &str, descriptor: &Descriptor) -> Result<Vec<u8>> { pub async fn get_blob(&mut self, name: &str, descriptor: &Descriptor) -> Result<Bytes> {
let url = self let url = self
.url .url
.join(&format!("/v2/{}/blobs/{}", name, descriptor.digest()))?; .join(&format!("/v2/{}/blobs/{}", name, descriptor.digest()))?;
let response = self.call(self.agent.get(url.as_str()))?; let response = self.call(self.agent.get(url.as_str())).await?;
let mut buffer: Vec<u8> = Vec::new(); Ok(response.bytes().await?)
response.into_reader().read_to_end(&mut buffer)?;
Ok(buffer)
} }
pub fn write_blob( pub async fn write_blob_to_file(
&mut self, &mut self,
name: &str, name: &str,
descriptor: &Descriptor, descriptor: &Descriptor,
dest: &mut dyn Write, mut dest: File,
) -> Result<u64> { ) -> Result<u64> {
let url = self let url = self
.url .url
.join(&format!("/v2/{}/blobs/{}", name, descriptor.digest()))?; .join(&format!("/v2/{}/blobs/{}", name, descriptor.digest()))?;
let response = self.call(self.agent.get(url.as_str()))?; let mut response = self.call(self.agent.get(url.as_str())).await?;
let mut reader = response.into_reader(); let mut size: u64 = 0;
Ok(copy(reader.deref_mut(), dest)?) while let Some(chunk) = response.chunk().await? {
dest.write_all(&chunk).await?;
size += chunk.len() as u64;
}
Ok(size)
} }
pub fn get_manifest_with_digest( async fn get_raw_manifest_with_digest(
&mut self, &mut self,
name: &str, name: &str,
reference: &str, reference: &str,
@ -65,24 +66,60 @@ impl RegistryClient {
MediaType::ImageIndex, MediaType::ImageIndex,
MediaType::ImageIndex.to_docker_v2s2()?, MediaType::ImageIndex.to_docker_v2s2()?,
); );
let response = self.call(self.agent.get(url.as_str()).set("Accept", &accept))?; let response = self
.call(self.agent.get(url.as_str()).header("Accept", &accept))
.await?;
let digest = response
.headers()
.get("Docker-Content-Digest")
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?
.to_str()?
.to_string();
let manifest = serde_json::from_str(&response.text().await?)?;
Ok((manifest, digest))
}
pub async fn get_manifest_with_digest(
&mut self,
name: &str,
reference: &str,
) -> Result<(ImageManifest, String)> {
let url = self
.url
.join(&format!("/v2/{}/manifests/{}", name, reference))?;
let accept = format!(
"{}, {}, {}, {}",
MediaType::ImageManifest.to_docker_v2s2()?,
MediaType::ImageManifest,
MediaType::ImageIndex,
MediaType::ImageIndex.to_docker_v2s2()?,
);
let response = self
.call(self.agent.get(url.as_str()).header("Accept", &accept))
.await?;
let content_type = response let content_type = response
.header("Content-Type") .headers()
.ok_or_else(|| anyhow!("registry response did not have a Content-Type header"))?; .get("Content-Type")
.ok_or_else(|| anyhow!("registry response did not have a Content-Type header"))?
.to_str()?;
if content_type == MediaType::ImageIndex.to_string() if content_type == MediaType::ImageIndex.to_string()
|| content_type == MediaType::ImageIndex.to_docker_v2s2()? || content_type == MediaType::ImageIndex.to_docker_v2s2()?
{ {
let index = ImageIndex::from_reader(response.into_reader())?; let index = serde_json::from_str(&response.text().await?)?;
let descriptor = self let descriptor = self
.pick_manifest(index) .pick_manifest(index)
.ok_or_else(|| anyhow!("unable to pick manifest from index"))?; .ok_or_else(|| anyhow!("unable to pick manifest from index"))?;
return self.get_manifest_with_digest(name, descriptor.digest()); return self
.get_raw_manifest_with_digest(name, descriptor.digest())
.await;
} }
let digest = response let digest = response
.header("Docker-Content-Digest") .headers()
.get("Docker-Content-Digest")
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))? .ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?
.to_str()?
.to_string(); .to_string();
let manifest = ImageManifest::from_reader(response.into_reader())?; let manifest = serde_json::from_str(&response.text().await?)?;
Ok((manifest, digest)) Ok((manifest, digest))
} }

View File

@ -94,8 +94,8 @@ impl ImageCompiler<'_> {
Ok(ImageCompiler { cache }) Ok(ImageCompiler { cache })
} }
pub fn compile(&self, image: &ImageName) -> Result<ImageInfo> { pub async fn compile(&self, image: &ImageName) -> Result<ImageInfo> {
debug!("ImageCompiler compile image={image}"); debug!("compile image={image}");
let mut tmp_dir = std::env::temp_dir().clone(); let mut tmp_dir = std::env::temp_dir().clone();
tmp_dir.push(format!("krata-compile-{}", Uuid::new_v4())); tmp_dir.push(format!("krata-compile-{}", Uuid::new_v4()));
@ -109,12 +109,14 @@ impl ImageCompiler<'_> {
let mut squash_file = tmp_dir.clone(); let mut squash_file = tmp_dir.clone();
squash_file.push("image.squashfs"); squash_file.push("image.squashfs");
let info = self.download_and_compile(image, &layer_dir, &image_dir, &squash_file)?; let info = self
.download_and_compile(image, &layer_dir, &image_dir, &squash_file)
.await?;
fs::remove_dir_all(&tmp_dir)?; fs::remove_dir_all(&tmp_dir)?;
Ok(info) Ok(info)
} }
fn download_and_compile( async fn download_and_compile(
&self, &self,
image: &ImageName, image: &ImageName,
layer_dir: &Path, layer_dir: &Path,
@ -122,11 +124,13 @@ impl ImageCompiler<'_> {
squash_file: &PathBuf, squash_file: &PathBuf,
) -> Result<ImageInfo> { ) -> Result<ImageInfo> {
debug!( debug!(
"ImageCompiler download manifest image={image}, image_dir={}", "download manifest image={image}, image_dir={}",
image_dir.to_str().unwrap() image_dir.to_str().unwrap()
); );
let mut client = RegistryClient::new(image.registry_url()?)?; let mut client = RegistryClient::new(image.registry_url()?)?;
let (manifest, digest) = client.get_manifest_with_digest(&image.name, &image.reference)?; let (manifest, digest) = client
.get_manifest_with_digest(&image.name, &image.reference)
.await?;
let cache_key = format!( let cache_key = format!(
"manifest={}:squashfs-version={}\n", "manifest={}:squashfs-version={}\n",
digest, IMAGE_SQUASHFS_VERSION digest, IMAGE_SQUASHFS_VERSION
@ -138,21 +142,24 @@ impl ImageCompiler<'_> {
} }
debug!( debug!(
"ImageCompiler download config digest={} size={}", "download config digest={} size={}",
manifest.config().digest(), manifest.config().digest(),
manifest.config().size(), manifest.config().size(),
); );
let config_bytes = client.get_blob(&image.name, manifest.config())?; let config_bytes = client.get_blob(&image.name, manifest.config()).await?;
let config: ImageConfiguration = serde_json::from_slice(&config_bytes)?; let config: ImageConfiguration = serde_json::from_slice(&config_bytes)?;
let mut layers: Vec<LayerFile> = Vec::new(); let mut layers: Vec<LayerFile> = Vec::new();
for layer in manifest.layers() { for layer in manifest.layers() {
layers.push(self.download_layer(image, layer, layer_dir, &mut client)?); layers.push(
self.download_layer(image, layer, layer_dir, &mut client)
.await?,
);
} }
for layer in layers { for layer in layers {
debug!( debug!(
"ImageCompiler process layer digest={} compression={:?}", "process layer digest={} compression={:?}",
&layer.digest, layer.compression &layer.digest, layer.compression
); );
let mut archive = Archive::new(layer.open_reader()?); let mut archive = Archive::new(layer.open_reader()?);
@ -199,7 +206,7 @@ impl ImageCompiler<'_> {
} }
trace!( trace!(
"ImageCompiler whiteout entry layer={} path={:?}", "whiteout entry layer={} path={:?}",
&layer.digest, &layer.digest,
entry.path()? entry.path()?
); );
@ -219,7 +226,7 @@ impl ImageCompiler<'_> {
} }
} else { } else {
warn!( warn!(
"ImageCompiler whiteout entry missing locally layer={} path={:?} local={:?}", "whiteout entry missing locally layer={} path={:?} local={:?}",
&layer.digest, &layer.digest,
entry.path()?, entry.path()?,
dst, dst,
@ -231,7 +238,7 @@ impl ImageCompiler<'_> {
fs::remove_dir(&dst)?; fs::remove_dir(&dst)?;
} else { } else {
warn!( warn!(
"ImageCompiler whiteout entry missing locally layer={} path={:?} local={:?}", "whiteout entry missing locally layer={} path={:?} local={:?}",
&layer.digest, &layer.digest,
entry.path()?, entry.path()?,
dst, dst,
@ -247,7 +254,7 @@ impl ImageCompiler<'_> {
image_dir: &PathBuf, image_dir: &PathBuf,
) -> Result<()> { ) -> Result<()> {
trace!( trace!(
"ImageCompiler unpack entry layer={} path={:?} type={:?}", "unpack entry layer={} path={:?} type={:?}",
&layer.digest, &layer.digest,
entry.path()?, entry.path()?,
entry.header().entry_type() entry.header().entry_type()
@ -285,7 +292,7 @@ impl ImageCompiler<'_> {
Ok(()) Ok(())
} }
fn download_layer( async fn download_layer(
&self, &self,
image: &ImageName, image: &ImageName,
layer: &Descriptor, layer: &Descriptor,
@ -293,7 +300,7 @@ impl ImageCompiler<'_> {
client: &mut RegistryClient, client: &mut RegistryClient,
) -> Result<LayerFile> { ) -> Result<LayerFile> {
debug!( debug!(
"ImageCompiler download layer digest={} size={}", "download layer digest={} size={}",
layer.digest(), layer.digest(),
layer.size() layer.size()
); );
@ -303,8 +310,8 @@ impl ImageCompiler<'_> {
tmp_path.push(format!("{}.tmp", layer.digest())); tmp_path.push(format!("{}.tmp", layer.digest()));
{ {
let mut file = File::create(&layer_path)?; let file = tokio::fs::File::create(&layer_path).await?;
let size = client.write_blob(&image.name, layer, &mut file)?; let size = client.write_blob_to_file(&image.name, layer, file).await?;
if layer.size() as u64 != size { if layer.size() as u64 != size {
return Err(anyhow!( return Err(anyhow!(
"downloaded layer size differs from size in manifest", "downloaded layer size differs from size in manifest",
@ -344,7 +351,7 @@ impl ImageCompiler<'_> {
.to_str() .to_str()
.ok_or_else(|| anyhow!("failed to strip prefix of tmpdir"))?; .ok_or_else(|| anyhow!("failed to strip prefix of tmpdir"))?;
let rel = format!("/{}", rel); let rel = format!("/{}", rel);
trace!("ImageCompiler squash write {}", rel); trace!("squash write {}", rel);
let typ = entry.file_type(); let typ = entry.file_type();
let metadata = fs::symlink_metadata(entry.path())?; let metadata = fs::symlink_metadata(entry.path())?;
let uid = metadata.uid(); let uid = metadata.uid();
@ -400,7 +407,7 @@ impl ImageCompiler<'_> {
.ok_or_else(|| anyhow!("failed to convert squashfs string"))?; .ok_or_else(|| anyhow!("failed to convert squashfs string"))?;
let mut file = File::create(squash_file)?; let mut file = File::create(squash_file)?;
trace!("ImageCompiler squash generate: {}", squash_file_path); trace!("squash generate: {}", squash_file_path);
writer.write(&mut file)?; writer.write(&mut file)?;
Ok(()) Ok(())
} }

View File

@ -92,16 +92,13 @@ impl BootSetup<'_> {
max_vcpus: u32, max_vcpus: u32,
mem_mb: u64, mem_mb: u64,
) -> Result<BootState> { ) -> Result<BootState> {
debug!( debug!("initialize max_vcpus={:?} mem_mb={:?}", max_vcpus, mem_mb);
"BootSetup initialize max_vcpus={:?} mem_mb={:?}",
max_vcpus, mem_mb
);
let total_pages = mem_mb << (20 - arch.page_shift()); let total_pages = mem_mb << (20 - arch.page_shift());
self.initialize_memory(arch, total_pages)?; self.initialize_memory(arch, total_pages)?;
let image_info = image_loader.parse()?; let image_info = image_loader.parse()?;
debug!("BootSetup initialize image_info={:?}", image_info); debug!("initialize image_info={:?}", image_info);
self.virt_alloc_end = image_info.virt_base; self.virt_alloc_end = image_info.virt_base;
let kernel_segment = self.load_kernel_segment(arch, image_loader, &image_info)?; let kernel_segment = self.load_kernel_segment(arch, image_loader, &image_info)?;
let mut p2m_segment: Option<DomainSegment> = None; let mut p2m_segment: Option<DomainSegment> = None;
@ -152,7 +149,7 @@ impl BootSetup<'_> {
console_evtchn, console_evtchn,
shared_info_frame: 0, shared_info_frame: 0,
}; };
debug!("BootSetup initialize state={:?}", state); debug!("initialize state={:?}", state);
Ok(state) Ok(state)
} }
@ -259,7 +256,7 @@ impl BootSetup<'_> {
slice.fill(0); slice.fill(0);
segment.vend = self.virt_alloc_end; segment.vend = self.virt_alloc_end;
debug!( debug!(
"BootSetup alloc_segment {:#x} -> {:#x} (pfn {:#x} + {:#x} pages)", "alloc_segment {:#x} -> {:#x} (pfn {:#x} + {:#x} pages)",
start, segment.vend, segment.pfn, pages start, segment.vend, segment.pfn, pages
); );
Ok(segment) Ok(segment)
@ -270,7 +267,7 @@ impl BootSetup<'_> {
let pfn = self.pfn_alloc_end; let pfn = self.pfn_alloc_end;
self.chk_alloc_pages(arch, 1)?; self.chk_alloc_pages(arch, 1)?;
debug!("BootSetup alloc_page {:#x} (pfn {:#x})", start, pfn); debug!("alloc_page {:#x} (pfn {:#x})", start, pfn);
Ok(DomainSegment { Ok(DomainSegment {
vstart: start, vstart: start,
vend: (start + arch.page_size()) - 1, vend: (start + arch.page_size()) - 1,

View File

@ -254,7 +254,7 @@ impl BootImageLoader for ElfImageLoader {
let segments = elf.segments().ok_or(Error::ElfInvalidImage)?; let segments = elf.segments().ok_or(Error::ElfInvalidImage)?;
debug!( debug!(
"ElfImageLoader load dst={:#x} segments={}", "load dst={:#x} segments={}",
dst.as_ptr() as u64, dst.as_ptr() as u64,
segments.len() segments.len()
); );
@ -267,7 +267,7 @@ impl BootImageLoader for ElfImageLoader {
let segment_dst = &mut dst[base_offset as usize..]; let segment_dst = &mut dst[base_offset as usize..];
let copy_slice = &data[0..filesz as usize]; let copy_slice = &data[0..filesz as usize];
debug!( debug!(
"ElfImageLoader load copy hdr={:?} dst={:#x} len={}", "load copy hdr={:?} dst={:#x} len={}",
header, header,
copy_slice.as_ptr() as u64, copy_slice.as_ptr() as u64,
copy_slice.len() copy_slice.len()
@ -276,7 +276,7 @@ impl BootImageLoader for ElfImageLoader {
if (memsz - filesz) > 0 { if (memsz - filesz) > 0 {
let remaining = &mut segment_dst[filesz as usize..memsz as usize]; let remaining = &mut segment_dst[filesz as usize..memsz as usize];
debug!( debug!(
"ElfImageLoader load fill_zero hdr={:?} dst={:#x} len={}", "load fill_zero hdr={:?} dst={:#x} len={}",
header.p_offset, header.p_offset,
remaining.as_ptr() as u64, remaining.as_ptr() as u64,
remaining.len() remaining.len()

View File

@ -268,7 +268,7 @@ impl X86BootSetup {
} }
debug!( debug!(
"BootSetup count_pgtables {:#x}/{}: {:#x} -> {:#x}, {} tables", "count_pgtables {:#x}/{}: {:#x} -> {:#x}, {} tables",
mask, bits, map.levels[l].from, map.levels[l].to, map.levels[l].pgtables mask, bits, map.levels[l].from, map.levels[l].to, map.levels[l].pgtables
); );
map.area.pgtables += map.levels[l].pgtables; map.area.pgtables += map.levels[l].pgtables;
@ -342,7 +342,7 @@ impl ArchBootSetup for X86BootSetup {
let size = self.table.mappings[m].area.pgtables as u64 * X86_PAGE_SIZE; let size = self.table.mappings[m].area.pgtables as u64 * X86_PAGE_SIZE;
let segment = setup.alloc_segment(self, 0, size)?; let segment = setup.alloc_segment(self, 0, size)?;
debug!( debug!(
"BootSetup alloc_page_tables table={:?} segment={:?}", "alloc_page_tables table={:?} segment={:?}",
self.table, segment self.table, segment
); );
Ok(segment) Ok(segment)
@ -387,7 +387,7 @@ impl ArchBootSetup for X86BootSetup {
let mut pfn = ((max(from, lvl.from) - lvl.from) >> rhs) + lvl.pfn; let mut pfn = ((max(from, lvl.from) - lvl.from) >> rhs) + lvl.pfn;
debug!( debug!(
"BootSetup setup_page_tables lvl={} map_1={} map_2={} pfn={:#x} p_s={:#x} p_e={:#x}", "setup_page_tables lvl={} map_1={} map_2={} pfn={:#x} p_s={:#x} p_e={:#x}",
l, m1, m2, pfn, p_s, p_e l, m1, m2, pfn, p_s, p_e
); );
@ -439,7 +439,7 @@ impl ArchBootSetup for X86BootSetup {
(*info).cmdline[i] = c as c_char; (*info).cmdline[i] = c as c_char;
} }
(*info).cmdline[MAX_GUEST_CMDLINE - 1] = 0; (*info).cmdline[MAX_GUEST_CMDLINE - 1] = 0;
trace!("BootSetup setup_start_info start_info={:?}", *info); trace!("setup_start_info start_info={:?}", *info);
} }
Ok(()) Ok(())
} }
@ -456,7 +456,7 @@ impl ArchBootSetup for X86BootSetup {
for i in 0..32 { for i in 0..32 {
(*info).vcpu_info[i].evtchn_upcall_mask = 1; (*info).vcpu_info[i].evtchn_upcall_mask = 1;
} }
trace!("BootSetup setup_shared_info shared_info={:?}", *info); trace!("setup_shared_info shared_info={:?}", *info);
} }
Ok(()) Ok(())
} }
@ -620,7 +620,7 @@ impl ArchBootSetup for X86BootSetup {
vcpu.user_regs.cs = 0xe033; vcpu.user_regs.cs = 0xe033;
vcpu.kernel_ss = vcpu.user_regs.ss as u64; vcpu.kernel_ss = vcpu.user_regs.ss as u64;
vcpu.kernel_sp = vcpu.user_regs.rsp; vcpu.kernel_sp = vcpu.user_regs.rsp;
debug!("vcpu context: {:?}", vcpu); trace!("vcpu context: {:?}", vcpu);
setup.call.set_vcpu_context(setup.domid, 0, &vcpu)?; setup.call.set_vcpu_context(setup.domid, 0, &vcpu)?;
Ok(()) Ok(())
} }