2024-01-30 02:15:03 -08:00
|
|
|
use anyhow::{anyhow, Result};
|
2024-02-25 05:38:23 +00:00
|
|
|
use bytes::Bytes;
|
2024-01-18 10:16:59 -08:00
|
|
|
use oci_spec::image::{Arch, Descriptor, ImageIndex, ImageManifest, MediaType, Os, ToDockerV2S2};
|
2024-02-25 05:38:23 +00:00
|
|
|
use reqwest::{Client, RequestBuilder, Response};
|
|
|
|
use tokio::{fs::File, io::AsyncWriteExt};
|
2024-01-18 10:16:59 -08:00
|
|
|
use url::Url;
|
|
|
|
|
2024-02-24 22:05:06 +00:00
|
|
|
const MANIFEST_PICKER_PLATFORM: Os = Os::Linux;
|
|
|
|
const MANIFEST_PICKER_ARCHITECTURE: Arch = Arch::Amd64;
|
|
|
|
|
2024-01-18 10:16:59 -08:00
|
|
|
pub struct RegistryClient {
|
2024-02-25 05:38:23 +00:00
|
|
|
agent: Client,
|
2024-01-18 10:16:59 -08:00
|
|
|
url: Url,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RegistryClient {
|
|
|
|
pub fn new(url: Url) -> Result<RegistryClient> {
|
|
|
|
Ok(RegistryClient {
|
2024-02-25 05:38:23 +00:00
|
|
|
agent: Client::new(),
|
2024-01-18 10:16:59 -08:00
|
|
|
url,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-02-25 05:38:23 +00:00
|
|
|
async fn call(&mut self, req: RequestBuilder) -> Result<Response> {
|
|
|
|
self.agent.execute(req.build()?).await.map_err(|x| x.into())
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
|
2024-02-25 05:38:23 +00:00
|
|
|
pub async fn get_blob(&mut self, name: &str, descriptor: &Descriptor) -> Result<Bytes> {
|
2024-01-18 10:16:59 -08:00
|
|
|
let url = self
|
|
|
|
.url
|
|
|
|
.join(&format!("/v2/{}/blobs/{}", name, descriptor.digest()))?;
|
2024-02-25 05:38:23 +00:00
|
|
|
let response = self.call(self.agent.get(url.as_str())).await?;
|
|
|
|
Ok(response.bytes().await?)
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
|
2024-02-25 05:38:23 +00:00
|
|
|
pub async fn write_blob_to_file(
|
2024-01-20 02:41:49 -08:00
|
|
|
&mut self,
|
|
|
|
name: &str,
|
|
|
|
descriptor: &Descriptor,
|
2024-02-25 05:38:23 +00:00
|
|
|
mut dest: File,
|
2024-01-20 02:41:49 -08:00
|
|
|
) -> Result<u64> {
|
|
|
|
let url = self
|
|
|
|
.url
|
|
|
|
.join(&format!("/v2/{}/blobs/{}", name, descriptor.digest()))?;
|
2024-02-25 05:38:23 +00:00
|
|
|
let mut response = self.call(self.agent.get(url.as_str())).await?;
|
|
|
|
let mut size: u64 = 0;
|
|
|
|
while let Some(chunk) = response.chunk().await? {
|
|
|
|
dest.write_all(&chunk).await?;
|
|
|
|
size += chunk.len() as u64;
|
|
|
|
}
|
|
|
|
Ok(size)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn get_raw_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 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))
|
2024-01-20 02:41:49 -08:00
|
|
|
}
|
|
|
|
|
2024-02-25 05:38:23 +00:00
|
|
|
pub async fn get_manifest_with_digest(
|
2024-01-22 07:24:42 -08:00
|
|
|
&mut self,
|
|
|
|
name: &str,
|
|
|
|
reference: &str,
|
|
|
|
) -> Result<(ImageManifest, String)> {
|
2024-01-18 10:16:59 -08:00
|
|
|
let url = self
|
|
|
|
.url
|
|
|
|
.join(&format!("/v2/{}/manifests/{}", name, reference))?;
|
|
|
|
let accept = format!(
|
2024-01-21 04:49:31 -08:00
|
|
|
"{}, {}, {}, {}",
|
2024-01-18 10:16:59 -08:00
|
|
|
MediaType::ImageManifest.to_docker_v2s2()?,
|
|
|
|
MediaType::ImageManifest,
|
|
|
|
MediaType::ImageIndex,
|
2024-01-21 04:49:31 -08:00
|
|
|
MediaType::ImageIndex.to_docker_v2s2()?,
|
2024-01-18 10:16:59 -08:00
|
|
|
);
|
2024-02-25 05:38:23 +00:00
|
|
|
let response = self
|
|
|
|
.call(self.agent.get(url.as_str()).header("Accept", &accept))
|
|
|
|
.await?;
|
2024-01-30 02:15:03 -08:00
|
|
|
let content_type = response
|
2024-02-25 05:38:23 +00:00
|
|
|
.headers()
|
|
|
|
.get("Content-Type")
|
|
|
|
.ok_or_else(|| anyhow!("registry response did not have a Content-Type header"))?
|
|
|
|
.to_str()?;
|
2024-01-21 04:49:31 -08:00
|
|
|
if content_type == MediaType::ImageIndex.to_string()
|
|
|
|
|| content_type == MediaType::ImageIndex.to_docker_v2s2()?
|
|
|
|
{
|
2024-02-25 05:38:23 +00:00
|
|
|
let index = serde_json::from_str(&response.text().await?)?;
|
2024-01-18 10:16:59 -08:00
|
|
|
let descriptor = self
|
|
|
|
.pick_manifest(index)
|
2024-01-30 02:15:03 -08:00
|
|
|
.ok_or_else(|| anyhow!("unable to pick manifest from index"))?;
|
2024-02-25 05:38:23 +00:00
|
|
|
return self
|
|
|
|
.get_raw_manifest_with_digest(name, descriptor.digest())
|
|
|
|
.await;
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
2024-01-22 07:24:42 -08:00
|
|
|
let digest = response
|
2024-02-25 05:38:23 +00:00
|
|
|
.headers()
|
|
|
|
.get("Docker-Content-Digest")
|
2024-01-30 02:15:03 -08:00
|
|
|
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?
|
2024-02-25 05:38:23 +00:00
|
|
|
.to_str()?
|
2024-01-22 07:24:42 -08:00
|
|
|
.to_string();
|
2024-02-25 05:38:23 +00:00
|
|
|
let manifest = serde_json::from_str(&response.text().await?)?;
|
2024-01-22 07:24:42 -08:00
|
|
|
Ok((manifest, digest))
|
2024-01-18 10:16:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
fn pick_manifest(&mut self, index: ImageIndex) -> Option<Descriptor> {
|
|
|
|
for item in index.manifests() {
|
|
|
|
if let Some(platform) = item.platform() {
|
2024-02-24 22:05:06 +00:00
|
|
|
if *platform.os() == MANIFEST_PICKER_PLATFORM
|
|
|
|
&& *platform.architecture() == MANIFEST_PICKER_ARCHITECTURE
|
|
|
|
{
|
2024-01-18 10:16:59 -08:00
|
|
|
return Some(item.clone());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|