├── .gitignore ├── rs-docker.sh ├── src ├── registry │ ├── .DS_Store │ ├── repository.rs │ ├── manifest │ │ ├── v1.rs │ │ ├── mod.rs │ │ └── v2.rs │ └── auth.rs ├── main.rs └── registry.rs ├── docker-compose.yml ├── Dockerfile ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Dockerfile.bak 3 | .cargo 4 | .vscode 5 | Cargo.lock 6 | .DS_Store -------------------------------------------------------------------------------- /rs-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker compose run --rm --cap-add="SYS_ADMIN" helper "$@" 4 | -------------------------------------------------------------------------------- /src/registry/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpouris/rs-docker/HEAD/src/registry/.DS_Store -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | helper: 5 | image: rs-docker 6 | build: . 7 | volumes: 8 | - ./src:/app/src 9 | ports: 10 | - "8080:8080" 11 | restart: always -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.63-buster 2 | 3 | RUN mkdir /app 4 | COPY Cargo.toml /app/Cargo.toml 5 | COPY Cargo.lock /app/Cargo.lock 6 | 7 | WORKDIR /app 8 | ADD . . 9 | 10 | RUN cargo build --release 11 | 12 | ENTRYPOINT ["/app/target/release/rs-docker"] 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rs-docker" 3 | version = "0.1.0" 4 | authors = ["dpouris "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | reqwest = { version = "^0.11", features = ["json", "blocking"] } 9 | bytes = "1.3.0" 10 | tokio = { version = "1.23.0", features = ["full"] } 11 | libc = "^0.2" 12 | serde = { version = "1.0.136", features = ["derive"] } 13 | serde_json = "1.0.79" 14 | anyhow = "1.0.59" 15 | thiserror = "1.0.32" 16 | tempfile = "3" 17 | regex = "1" 18 | flate2 = "1.0.25" 19 | tar = "0.4.38" 20 | interm = "0.1.1" 21 | async-recursion = "1.0.5" 22 | -------------------------------------------------------------------------------- /src/registry/repository.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[derive(Debug)] 4 | pub struct Repository<'r> { 5 | pub name: Cow<'r, str>, 6 | pub tag: &'r str, 7 | } 8 | 9 | impl<'r> Repository<'r> { 10 | pub fn new(image: &'r str) -> Self { 11 | if let Some((name, tag)) = image.split_once(':') { 12 | Self { 13 | name: Self::parse_repo_name(name), 14 | tag, 15 | } 16 | } else { 17 | Self { 18 | name: Self::parse_repo_name(image), 19 | tag: "latest", 20 | } 21 | } 22 | } 23 | 24 | fn parse_repo_name(name: &'r str) -> Cow<'r, str> { 25 | match name.contains('/') { 26 | true => Cow::from(name), 27 | false => Cow::from(format!("library/{}", name).as_str().to_owned()), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/registry/manifest/v1.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::{GetLayers, Layer as MainLayer}; 5 | 6 | pub type BlobSum = String; 7 | 8 | #[derive(Deserialize, Serialize, Debug)] 9 | pub struct ImageManifestV1 { 10 | #[serde(rename = "schemaVersion")] 11 | pub schema_version: u8, 12 | pub name: String, 13 | pub architecture: String, 14 | #[serde(rename = "fsLayers")] 15 | pub fs_layers: Vec, 16 | pub history: Vec, 17 | pub signatures: Vec, 18 | } 19 | 20 | #[derive(Deserialize, Serialize, Debug)] 21 | pub struct FsLayer { 22 | #[serde(rename = "blobSum")] 23 | pub blob_sum: BlobSum, 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Debug)] 27 | pub struct HistoryEntry { 28 | #[serde(rename = "v1Compatibility")] 29 | pub v1_compatibility: String, 30 | } 31 | 32 | #[derive(Deserialize, Serialize, Debug)] 33 | pub struct Signature { 34 | pub header: Header, 35 | pub signature: String, 36 | pub protected: String, 37 | } 38 | #[derive(Deserialize, Serialize, Debug)] 39 | pub struct Header { 40 | pub jwk: Jwk, 41 | pub alg: String, 42 | } 43 | 44 | #[derive(Deserialize, Serialize, Debug)] 45 | pub struct Jwk { 46 | pub crv: String, 47 | pub kid: String, 48 | pub kty: String, 49 | pub x: String, 50 | pub y: String, 51 | } 52 | 53 | impl GetLayers for ImageManifestV1 { 54 | fn get_layers(self) -> Vec { 55 | self.fs_layers.into_iter().map(MainLayer::from).collect() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/registry/manifest/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod v1; 2 | pub mod v2; 3 | use self::{ 4 | v1::{FsLayer, ImageManifestV1}, 5 | v2::{ImageManifestV2, Layer as V2Layer, ManifestDigestList}, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | /// [Digest] is a type wrapper on [String] and is typically a 10 | /// **\:\** [String] e.g `sha256:e4c58958181a5925816faa528ce959e487632f4cfd192f8132f71b32df2744b4` 11 | pub type Digest = String; 12 | /// MediaType contains the Content-Type header which is expected by the [v2] api to be present when requesting a resource from it e.g 13 | /// `application/vnd.oci.image.manifest.v1+json` **or** `application/vnd.oci.image.config.v1+json` 14 | pub type MediaType = String; 15 | pub type Size = u32; 16 | 17 | const VALID_PLATFORMS: [(&str, &str); 3] = 18 | [("linux", "amd64"), ("linux", "arm64"), ("linux", "arm")]; 19 | 20 | pub struct Layer { 21 | pub digest: Digest, 22 | pub media_type: Option, 23 | } 24 | 25 | pub trait GetLayers { 26 | fn get_layers(self) -> Vec; 27 | } 28 | 29 | impl From for Layer { 30 | fn from(value: V2Layer) -> Self { 31 | Self { 32 | digest: value.digest, 33 | media_type: Some(value.media_type), 34 | } 35 | } 36 | } 37 | 38 | impl From for Layer { 39 | fn from(value: FsLayer) -> Self { 40 | Self { 41 | digest: value.blob_sum, 42 | media_type: None, 43 | } 44 | } 45 | } 46 | 47 | #[derive(Debug, Serialize, Deserialize)] 48 | #[serde(untagged)] 49 | pub enum GetManifestResponse { 50 | ManifestDigestList(ManifestDigestList), 51 | ImageManifestV1(ImageManifestV1), 52 | ImageManifestV2(ImageManifestV2), 53 | } 54 | -------------------------------------------------------------------------------- /src/registry/auth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{self, Context}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | pub(super) struct AuthParams<'a> { 6 | pub service: &'static str, 7 | pub client_id: &'static str, 8 | pub access_type: &'static str, 9 | pub scope: &'a str, 10 | pub grant_type: Option<&'static str>, 11 | pub username: Option<&'a str>, 12 | pub password: Option<&'a str>, 13 | } 14 | 15 | #[derive(Deserialize, Serialize)] 16 | pub(super) struct AuthResponse { 17 | pub access_token: String, 18 | pub expires_in: u16, 19 | pub issued_at: String, 20 | pub refresh_token: Option, 21 | pub scope: Option, 22 | pub token: Option, 23 | } 24 | pub(super) enum ApiV2Status { 25 | Supported, 26 | NotSupport, 27 | Unauthorized(Option), 28 | } 29 | 30 | impl<'a> AuthParams<'a> { 31 | pub(super) fn new( 32 | username: Option<&'a str>, 33 | password: Option<&'a str>, 34 | scope: &'a str, 35 | ) -> Self { 36 | let grant_type = if username.is_some() { 37 | Some("password") 38 | } else { 39 | None 40 | }; 41 | Self { 42 | service: "registry.docker.io", 43 | client_id: "dockerengine", 44 | access_type: "offline", 45 | scope, 46 | grant_type, 47 | username, 48 | password, 49 | } 50 | } 51 | } 52 | 53 | impl<'a> TryInto for &AuthParams<'a> { 54 | type Error = anyhow::Error; 55 | fn try_into(self) -> std::result::Result { 56 | let auth = serde_json::to_value(self).context("tried to serialize auth")?; 57 | let auth = auth 58 | .as_object() 59 | .expect("AuthParams can be converted to object"); 60 | let mut query = Vec::with_capacity(auth.len()); 61 | for (key, val) in auth { 62 | match val.as_str() { 63 | Some(val) => query.push(format!("{key}={val}")), 64 | None => continue, 65 | } 66 | } 67 | Ok(query.join("&")) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker-Style FS and Process Isolation 2 | 3 | A lightweight implementation of Docker-style filesystem and process isolation, along with handling for registry images. It shows core Docker functionalities for educational purposes or lightweight use cases 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Features](#features) 9 | - [Prerequisites](#prerequisites) 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Supported Commands](#supported-commands) 13 | - [Using `rs-docker.sh` (Windows/MacOS)](#using-rs-dockersh-windowsmacos) 14 | - [Running the Code on System (Linux)](#running-the-code-on-system-linux) 15 | 16 | ## Introduction 17 | 18 | This project mimics essential Docker functionalities, including filesystem isolation, process isolation, and registry image management. It's designed to help users understand how Docker works under the hood and to provide a lightweight alternative for specific use cases 19 | 20 | ## Features 21 | 22 | - **Filesystem Isolation**: Implements isolated filesystems using `chroot` for containers 23 | - **Process Isolation**: Ensures processes run in separate namespaces using `unshare` 24 | - **Registry Image Handling**: Supports pulling and storing images from a registry 25 | 26 | ## Prerequisites 27 | 28 | Before you begin, ensure you have the following: 29 | 30 | - Docker installed. Follow the instructions on the [Docker website](https://docs.docker.com/get-docker/) to install Docker 31 | - A Linux environment or Windows with WSL2 for running the scripts. 32 | - I recommend using Docker (i know, trippy) in order to run in MacOS or Windows using the `rs-docker.sh` script. Currently working on a better way 33 | 34 | ## Installation 35 | 36 | 1. Clone the repository: 37 | 38 | ```bash 39 | git clone https://github.com/dpouris/rs-docker 40 | cd rs-docker 41 | ``` 42 | 43 | 2. Ensure the scripts have Unix-style line endings. If you cloned the repo on Windows, run: 44 | 45 | ```bash 46 | sed -i -e 's/\r$//' /app/rs-docker.sh 47 | ``` 48 | 49 | 50 | ## Usage 51 | 52 | ### Supported commands 53 | --- 54 | 55 | Currently the only supported command is `run` but in the future I plan to implement most, if not all, of Dockers main commands and some more helper commands. 56 | 57 | - run: `rs-docker run ...` 58 | --- 59 | 60 | ### Using `rs-docker.sh` (Windows/MacOS) 61 | 62 | Run the provided script: 63 | ```bash 64 | chmod +x ./rs-docker.sh 65 | ./rs-docker.sh run ubuntu:latest echo "hello world" 66 | ``` 67 | 68 | ### Running the code on system (Linux) 69 | 70 | 1. Build `src` 71 | 72 | ```bash 73 | cargo build --release 74 | ``` 75 | 76 | 2. Run the program using the supported commands: 77 | 78 | ```bash 79 | ./target/release/rs-docker run ubuntu:latest echo "hello world" 80 | ``` 81 | 89 | -------------------------------------------------------------------------------- /src/registry/manifest/v2.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] //remove later 2 | use anyhow::Result; 3 | use reqwest; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::{Digest, GetLayers, Layer as MainLayer, MediaType, Size, VALID_PLATFORMS}; 7 | 8 | #[derive(Deserialize, Serialize, Debug)] 9 | pub struct ManifestDigestList { 10 | pub manifests: Vec, 11 | #[serde(rename = "mediaType")] 12 | pub media_type: MediaType, 13 | #[serde(rename = "schemaVersion")] 14 | pub schema_version: u8, 15 | } 16 | 17 | #[derive(Deserialize, Serialize, Debug)] 18 | pub struct ManifestDigest { 19 | pub digest: Digest, 20 | #[serde(rename = "mediaType")] 21 | pub media_type: MediaType, 22 | pub platform: Platform, 23 | pub size: Size, 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Debug)] 27 | pub struct Platform { 28 | // Specifies the CPU architecture, for example amd64 or ppc64le. 29 | pub architecture: String, 30 | // Specifies the operating system, for example linux or windows. 31 | pub os: String, 32 | // Specifies the operating system version, for example 10.0.10586. 33 | #[serde(rename = "os.version")] 34 | pub os_version: Option, 35 | // Specifies an array of strings, each listing a required OS feature (for example on Windows win32k). 36 | #[serde(rename = "os.features")] 37 | pub os_features: Option>, 38 | // Specifies a variant of the CPU, for example v6 to specify a particular CPU variant of the ARM CPU. 39 | pub variant: Option, 40 | // Specifies an array of strings, each listing a required CPU feature (for example sse4 or aes). 41 | pub features: Option>, 42 | } 43 | 44 | #[derive(Deserialize, Serialize, Debug)] 45 | pub struct ImageManifestV2 { 46 | #[serde(rename = "mediaType")] 47 | pub media_type: MediaType, 48 | #[serde(rename = "schemaVersion")] 49 | pub schema_version: u8, 50 | pub config: Config, 51 | pub layers: Vec, 52 | } 53 | 54 | #[derive(Deserialize, Serialize, Debug)] 55 | pub struct Config { 56 | #[serde(rename = "mediaType")] 57 | pub media_type: MediaType, 58 | pub size: Size, 59 | pub digest: Digest, 60 | } 61 | 62 | #[derive(Deserialize, Serialize, Debug)] 63 | pub struct Layer { 64 | #[serde(rename = "mediaType")] 65 | pub media_type: MediaType, 66 | pub size: Size, 67 | pub digest: Digest, 68 | } 69 | 70 | impl ManifestDigestList { 71 | pub fn get_one(self) -> Option { 72 | if self.manifests.is_empty() { 73 | return None; 74 | } 75 | self.manifests 76 | .into_iter() 77 | .filter_map(|manifest| { 78 | if manifest.platform.is_valid() { 79 | Some(manifest) 80 | } else { 81 | None 82 | } 83 | }) 84 | .take(1) 85 | .next() 86 | } 87 | } 88 | 89 | impl Platform { 90 | fn is_valid(&self) -> bool { 91 | let platform = (self.os.as_str(), self.architecture.as_str()); 92 | 93 | VALID_PLATFORMS.iter().any(|&plat| plat == platform) 94 | } 95 | } 96 | 97 | impl GetLayers for ImageManifestV2 { 98 | fn get_layers(self) -> Vec { 99 | self.layers.into_iter().map(MainLayer::from).collect() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use std::{ 3 | env, fs, 4 | os::unix::fs::chroot, 5 | path::{Path, PathBuf}, 6 | process, 7 | }; 8 | mod registry; 9 | use registry::*; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | let args: Vec<_> = env::args().collect(); 14 | assert!(args.len() >= 5); 15 | let image = &args[2]; 16 | let command = &args[3]; 17 | let command_args = &args[4..]; 18 | let command_path = PathBuf::from(command); 19 | 20 | let repo = Repository::new(image); 21 | let registry = Registry::authenticate_repo(repo).await?; 22 | registry.pull().await?; 23 | 24 | setup_root()?; 25 | 26 | let output = process::Command::new(&command_path) 27 | .args(command_args) 28 | .output() 29 | .context(format!( 30 | "Tried to spawn new child process of command '{}' with arguments {:?}", 31 | command_path.display(), 32 | command_args 33 | ))?; 34 | 35 | match output.status.code() { 36 | Some(code) => { 37 | let std_out = std::str::from_utf8(&output.stdout)?; 38 | print!("{}", std_out); 39 | process::exit(code) 40 | } 41 | None => { 42 | let std_err = std::str::from_utf8(&output.stderr)?; 43 | eprint!("{}", std_err); 44 | process::exit(1) 45 | } 46 | } 47 | } 48 | 49 | fn create_dir(path: &PathBuf) -> Result<()> { 50 | if path.is_dir() { 51 | return Ok(()); 52 | } 53 | fs::DirBuilder::new() 54 | .recursive(true) 55 | .create(path) 56 | .context(format!("Attempted to create directory {}", path.display()))?; 57 | Ok(()) 58 | } 59 | 60 | fn create_file(path: &PathBuf) -> Result<()> { 61 | if path.is_file() { 62 | return Ok(()); 63 | } 64 | if path.is_dir() { 65 | return Err(anyhow!("Provided path {} is a directory", path.display())); 66 | } 67 | create_dir( 68 | &path 69 | .parent() 70 | .expect("Path always has parent dir") 71 | .to_path_buf(), 72 | )?; 73 | 74 | fs::File::create(path)?; 75 | 76 | Ok(()) 77 | } 78 | 79 | fn cp_from_path(src: &PathBuf, dst: &Path) -> Result<()> { 80 | let dst_bin = if src.is_absolute() { 81 | dst.join(src.strip_prefix("/").expect("destination is absolute path")) 82 | } else { 83 | dst.join(src) 84 | }; 85 | 86 | if !dst_bin.is_file() { 87 | create_file(&dst_bin)?; 88 | } 89 | fs::copy(src, &dst_bin).context(format!( 90 | "Attempted to copy {} to {}", 91 | src.display(), 92 | dst_bin.display() 93 | ))?; 94 | 95 | Ok(()) 96 | } 97 | 98 | fn setup_root() -> Result<()> { 99 | let root_dir = PathBuf::from("/tmp/iso_root_fs"); 100 | let root_pathname = root_dir.display(); 101 | 102 | // create home dir inside root 103 | create_dir(&root_dir.join("home/tmp"))?; 104 | // create dev/null inside root 105 | create_file(&root_dir.join("dev/null"))?; 106 | 107 | // copy the executable inside root 108 | // cp_from_path(bin_src, &root_dir)?; 109 | 110 | chroot(&root_dir).context(format!( 111 | "Attempted to chroot into directory {root_pathname}" 112 | ))?; 113 | 114 | // Unshare pid namespace to isolate processes inside chroot 115 | unshare_pid()?; 116 | 117 | env::set_current_dir("/home/tmp") 118 | .context("Attempted to change cwd inside chroot to `/home/tmp`")?; 119 | Ok(()) 120 | } 121 | 122 | fn unshare_pid() -> Result<()> { 123 | #[cfg(target_os = "linux")] 124 | unsafe { 125 | if libc::unshare(libc::CLONE_NEWPID) == 0 { 126 | return Ok(()); 127 | } 128 | Err(anyhow!("Could not unshare pid")) 129 | } 130 | 131 | #[cfg(not(target_os = "linux"))] 132 | Err(anyhow!( 133 | "Cannot use unshare. OS not linux and doesn't support namespaces" 134 | )) 135 | } 136 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use anyhow::{anyhow, Context, Result}; 4 | use async_recursion::async_recursion; 5 | use bytes::Bytes; 6 | use flate2::bufread::GzDecoder; 7 | use reqwest::Client; 8 | 9 | use self::manifest::{ 10 | v1::ImageManifestV1, 11 | v2::{ImageManifestV2, ManifestDigestList}, 12 | GetLayers, GetManifestResponse, MediaType, 13 | }; 14 | use auth::*; 15 | use manifest::Layer; 16 | pub use repository::*; 17 | 18 | mod auth; 19 | mod manifest; 20 | mod repository; 21 | 22 | #[derive(Debug, Default)] 23 | pub struct Registry<'r> { 24 | pub repository: Option>, 25 | access_token: String, 26 | client: Client, 27 | } 28 | 29 | const AUTH_URL: &str = "https://auth.docker.io"; 30 | const REGISTRY_URL: &str = "https://registry.hub.docker.com/v2/"; 31 | 32 | // TODO: on every method i need to construct a request, change that logic. We should have an instance that is alrady built and passed around that has the access_token embedded 33 | impl<'r> Registry<'r> { 34 | pub async fn authenticate_repo(repo: Repository<'r>) -> Result { 35 | let repo_auth = format!("repository:{}:pull", repo.name); 36 | let auth = AuthParams::new(None, None, &repo_auth); 37 | let mut reg = Self::authenticate(&auth).await?; 38 | reg.repository = Some(repo); 39 | Ok(reg) 40 | } 41 | 42 | #[allow(unused)] 43 | pub async fn authenticate_with_password( 44 | username: &'_ str, 45 | password: &'_ str, 46 | ) -> Result> { 47 | let auth = AuthParams::new(Some(username), Some(password), "registry"); 48 | Self::authenticate(&auth).await 49 | } 50 | 51 | async fn authenticate(auth: &AuthParams<'_>) -> Result> { 52 | let mut url = reqwest::Url::from_str(AUTH_URL)? 53 | .join("token") 54 | .context("tried to construct url")?; 55 | let auth_params: String = auth 56 | .try_into() 57 | .context("tried to turn AuthParams into String")?; 58 | url.set_query(Some(&auth_params)); 59 | let request = reqwest::Request::new(reqwest::Method::GET, url); 60 | let client = Client::new(); 61 | let res = client 62 | .execute(request) 63 | .await 64 | .context("tried to authenticate with password")?; 65 | 66 | let AuthResponse { access_token, .. } = res 67 | .json() 68 | .await 69 | .context("tried parse authentication response body to json")?; 70 | 71 | Ok(Self { 72 | access_token, 73 | client, 74 | ..Default::default() 75 | }) 76 | } 77 | 78 | pub async fn pull(&self) -> Result<()> { 79 | self.image_layers().await?; 80 | Ok(()) 81 | } 82 | 83 | #[async_recursion] 84 | async fn get_manifest_layers( 85 | &self, 86 | name: &str, 87 | reference: &str, 88 | media_type: Option, 89 | ) -> Result> { 90 | let manifest_path = format!("{}/manifests/{}", name, reference); 91 | let url = reqwest::Url::from_str(REGISTRY_URL)? 92 | .join(&manifest_path) 93 | .context("tried to construct manifest url")?; 94 | let req = self 95 | .client 96 | .request(reqwest::Method::GET, url) 97 | .bearer_auth(&self.access_token) 98 | .header("Accept", media_type.unwrap_or_default()); 99 | 100 | let res = req.send().await.context("tried to get image manifest")?; 101 | match res.json().await? { 102 | GetManifestResponse::ManifestDigestList(manifests @ ManifestDigestList { .. }) => { 103 | match manifests.get_one() { 104 | Some(image_manifest) => { 105 | self.get_manifest_layers( 106 | name, 107 | &image_manifest.digest, 108 | Some(image_manifest.media_type), 109 | ) 110 | .await 111 | } 112 | None => Err(anyhow!("Tried to get image manifest but none was found")), 113 | } 114 | } 115 | GetManifestResponse::ImageManifestV2(image @ ImageManifestV2 { .. }) => { 116 | Ok(image.get_layers()) 117 | } 118 | GetManifestResponse::ImageManifestV1(image @ ImageManifestV1 { .. }) => { 119 | Ok(image.get_layers()) 120 | } 121 | } 122 | } 123 | 124 | async fn image_layers(&self) -> Result<()> { 125 | let mut layers = match &self.repository { 126 | Some(repo) => self.get_manifest_layers(&repo.name, repo.tag, None).await?, 127 | None => return Err(anyhow!("No image layers")), 128 | }; 129 | 130 | let mut layer_blobs = Vec::with_capacity(layers.len()); 131 | for (idx, layer) in layers.iter_mut().enumerate() { 132 | let blob = self 133 | .fetch_layer_blob(layer) 134 | .await 135 | .context(anyhow!("failed to fetch {idx} layer blob"))?; 136 | layer_blobs.push(blob); 137 | } 138 | for blob in layer_blobs { 139 | let mut archive = tar::Archive::new(GzDecoder::new(&blob[..])); 140 | archive.unpack("/tmp/iso_root_fs")?; 141 | } 142 | 143 | Ok(()) 144 | } 145 | 146 | async fn fetch_layer_blob(&self, layer: &mut Layer) -> Result { 147 | let blob_path = format!( 148 | "{}/blobs/{}", 149 | self.repository 150 | .as_ref() 151 | .expect("Haven't authenticated repository") 152 | .name, 153 | layer.digest 154 | ); 155 | let url = reqwest::Url::from_str(REGISTRY_URL)? 156 | .join(&blob_path) 157 | .context("tried to construct layer blob url")?; 158 | let res = self 159 | .client 160 | .request(reqwest::Method::GET, url) 161 | .bearer_auth(&self.access_token) 162 | .header("Accept", &layer.media_type.take().unwrap_or_default()) 163 | .send() 164 | .await 165 | .context("tried to fetch layer blob")?; 166 | 167 | res.bytes().await.map_err(|err| anyhow::format_err!(err)) 168 | } 169 | 170 | // TODO: check the status of the api upon auth 171 | #[allow(unused)] 172 | async fn v2_status(&self) -> Result { 173 | let url = reqwest::Url::from_str(REGISTRY_URL)?; 174 | let res = self 175 | .client 176 | .request(reqwest::Method::GET, url) 177 | .bearer_auth(&self.access_token) 178 | .send() 179 | .await 180 | .context("tried to get api status")?; 181 | 182 | match res.status() { 183 | /* should handle the possibility that if StatusCode::Ok there might be some content in 184 | the res.body that contains the allowed/existing routes that can be accessed through the api v2 */ 185 | reqwest::StatusCode::OK => Ok(ApiV2Status::Supported), 186 | reqwest::StatusCode::NOT_FOUND => Ok(ApiV2Status::NotSupport), 187 | reqwest::StatusCode::UNAUTHORIZED => { 188 | let www_authenticate = res.headers().get("www-authenticate").map(|h| { 189 | h.to_str() 190 | .expect("www-authenticate should have content") 191 | .to_owned() 192 | }); 193 | Ok(ApiV2Status::Unauthorized(www_authenticate)) 194 | } 195 | _ => Err(anyhow!("unexpected status code returned from /v2/")), 196 | } 197 | } 198 | 199 | // TODO: fetch image config from /v2//blobs/ 200 | #[allow(unused)] 201 | pub async fn image_config(&self, _img: &str) -> Result<()> { 202 | unimplemented!() 203 | } 204 | 205 | // TODO: implement or remove this 206 | #[allow(unused)] 207 | fn extract_layer_blob(&self, blob: Bytes) -> Result<()> { 208 | unimplemented!() 209 | } 210 | } 211 | --------------------------------------------------------------------------------