Compare commits

...

5 Commits

Author SHA1 Message Date
27c8932a05 Updated comments 2022-06-25 21:29:52 +02:00
f71dff4e3d Fixed image name parser 2022-06-25 21:03:32 +02:00
c44452648d Fetch an image from the Docker Registry 2022-06-25 20:56:28 +02:00
9f33ac46b2 Process isolation 2022-06-23 16:16:16 +02:00
5e73e7d33e Filesystem isolation 2022-06-23 16:04:15 +02:00
3 changed files with 141 additions and 59 deletions

View File

@ -1,52 +1,14 @@
This is a starting point for Rust solutions to the
["Build Your Own Docker" Challenge](https://codecrafters.io/challenges/docker).
# codecrafters-docker-rust
My code for CodeCrafter's ["Build Your Own Docker" Challenge](https://codecrafters.io/challenges/docker).
In this challenge, you'll build a program that can pull an image from
[Docker Hub](https://hub.docker.com/) and execute commands in it. Along the way,
we'll learn about [chroot](https://en.wikipedia.org/wiki/Chroot),
[kernel namespaces](https://en.wikipedia.org/wiki/Linux_namespaces), the
[docker registry API](https://docs.docker.com/registry/spec/api/) and much more.
**Note**: If you're viewing this repo on GitHub, head over to
[codecrafters.io](https://codecrafters.io) to signup for early access.
# Passing the first stage
The entry point for your Docker implementation is `src/main.rs`. Study and
uncomment the relevant code, and push your changes to pass the first stage:
```sh
git add .
git commit -m "pass 1st stage" # any msg
git push origin master
```
That's all!
# Stage 2 & beyond
Note: This section is for stages 2 and beyond.
You'll use linux-specific syscalls in this challenge. so we'll run your code
_inside_ a Docker container.
Please ensure you have [Docker installed](https://docs.docker.com/get-docker/)
locally.
Next, add a [shell alias](https://shapeshed.com/unix-alias/):
## Requirements
- docker
## Getting started
```sh
alias mydocker='docker build -t mydocker . && docker run --cap-add="SYS_ADMIN" mydocker'
mydocker run debian:latest /bin/sh -c "ls -la /"
```
(The `--cap-add="SYS_ADMIN"` flag is required to create
[PID Namespaces](https://man7.org/linux/man-pages/man7/pid_namespaces.7.html))
You can now execute your program like this:
```sh
mydocker run ubuntu:latest /usr/local/bin/docker-explorer echo hey
```
This command compiles your Rust project, so it might be slow the first time you
run it. Subsequent runs will be fast.
Note: The `--cap-add="SYS_ADMIN"` flag is required to create
[PID Namespaces](https://man7.org/linux/man-pages/man7/pid_namespaces.7.html)

View File

@ -1,24 +1,52 @@
use std::{self, ffi::CString, path};
mod registry;
// Usage: your_docker.sh run <image> <command> <arg1> <arg2> ...
fn main() {
// You can use print statements as follows for debugging, they'll be visible when running tests.
// println!("Logs from your program will appear here!");
// Uncomment this block to pass the first stage!
let args: Vec<_> = std::env::args().collect();
let image_name = &args[2];
let command = &args[3];
let command_args = &args[4..];
let base_path = std::env::temp_dir().join("docker");
// prevent cryptic "no such file or directory" error inside chroot
std::fs::create_dir_all(base_path.join("dev")).unwrap();
std::fs::File::create(base_path.join("dev/null")).unwrap();
let image = registry::ImageIdentifier::from_string(image_name);
let mut reg = registry::Registry::default();
reg.pull(&image, base_path.to_str().unwrap());
// copy over binary into chroot directory
let command_path = path::Path::new(command).strip_prefix("/").unwrap();
std::fs::create_dir_all(base_path.join(command_path.parent().unwrap()))
.expect("Failed to create directory for executed binary");
std::fs::copy(command, base_path.join(command_path))
.expect("Failed copying executed binary to chroot directory");
// create and change into chroot directory
let cbase_path = CString::new(base_path.to_str().unwrap().to_owned()).unwrap();
unsafe {
libc::chroot(cbase_path.as_ptr());
}
// ensure that directory changed to root of jail
std::env::set_current_dir("/").expect("Failed to change to root dir");
// `unshare` puts the next created process in a seperate PID namespace
unsafe {
libc::unshare(libc::CLONE_NEWPID);
}
let output = std::process::Command::new(command)
.args(command_args)
.output()
.unwrap();
let std_out = std::str::from_utf8(&output.stdout).unwrap();
print!("{}", std_out);
let std_err = std::str::from_utf8(&output.stderr).unwrap();
eprint!("{}", std_err);
match output.status.code() {
Some(code) => std::process::exit(code),
None => std::process::exit(1)
}
print!("{}", std::str::from_utf8(&output.stdout).unwrap());
eprint!("{}", std::str::from_utf8(&output.stderr).unwrap());
std::process::exit(output.status.code().unwrap_or(1));
}

92
src/registry.rs Normal file
View File

@ -0,0 +1,92 @@
// TODO: Enable derive feature and convert dynamic to typed json
pub struct Registry {
http_client: reqwest::blocking::Client,
}
impl Default for Registry {
fn default() -> Self {
return Registry {
http_client: reqwest::blocking::Client::new(),
};
}
}
impl Registry {
pub fn pull(&mut self, image: &ImageIdentifier, destination: &str) {
// Perform the little auth dance
let auth_url = format!("https://auth.docker.io/token?service=registry.docker.io&scope=repository%3A{}%2F{}%3Apull", image.author, image.name);
let auth: serde_json::Value = self
.http_client
.get(&auth_url)
.send()
.unwrap()
.json()
.unwrap();
let access_token = auth["token"].as_str().unwrap();
// Download the image manifest
let auth_header = format!("Bearer {}", access_token);
let image_url = format!(
"https://registry.hub.docker.com/v2/{}/{}/manifests/{}",
image.author, image.name, image.tag
);
let image_manifest: serde_json::Value = self
.http_client
.get(&image_url)
.header(reqwest::header::AUTHORIZATION, auth_header.to_owned())
.send()
.unwrap()
.json()
.unwrap();
// Download the image layers and extracts them
let temp_path = std::env::temp_dir();
for layer in image_manifest["fsLayers"].as_array().unwrap() {
let digest = layer["blobSum"].as_str().unwrap();
let blob_url = format!(
"https://registry.hub.docker.com/v2/{}/{}/blobs/{}",
image.author, image.name, digest
);
let blob = self
.http_client
.get(&blob_url)
.header(reqwest::header::AUTHORIZATION, auth_header.to_owned())
.send()
.unwrap()
.bytes()
.unwrap();
let layer_path = temp_path.join(digest);
std::fs::write(layer_path.to_owned(), blob).unwrap();
// TODO: handle exit code
std::process::Command::new("tar")
.args(["-xf", layer_path.to_str().unwrap(), "-C", destination])
.output()
.unwrap();
std::fs::remove_file(layer_path).unwrap();
}
}
}
pub struct ImageIdentifier {
author: String,
name: String,
tag: String,
}
impl ImageIdentifier {
pub fn from_string(image: &String) -> Self {
let mut iter = image.splitn(2, ':');
let mut loc_iter = iter.next().unwrap().split('/').rev();
let name = loc_iter
.next()
.expect("No image name was supplied")
.to_string();
let author = loc_iter.next().unwrap_or("library").to_string();
let tag = iter.next().unwrap_or("latest").to_string();
return ImageIdentifier { author, name, tag };
}
}