Compare commits
9 Commits
ef7b0dc038
...
master
Author | SHA1 | Date | |
---|---|---|---|
4a9beec9f4 | |||
cb1b3af0df | |||
478af6b23a | |||
cad83660e9 | |||
27c8932a05 | |||
f71dff4e3d | |||
c44452648d | |||
9f33ac46b2 | |||
5e73e7d33e |
54
README.md
54
README.md
@ -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)
|
||||
|
54
src/main.rs
54
src/main.rs
@ -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));
|
||||
}
|
||||
|
184
src/registry.rs
Normal file
184
src/registry.rs
Normal file
@ -0,0 +1,184 @@
|
||||
// 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 {
|
||||
fn build_challenge_url(auth_header: &str) -> reqwest::Url {
|
||||
let challenge_part = auth_header.split(" ").nth(1).unwrap();
|
||||
let mut field_map = std::collections::HashMap::<&str, &str>::new();
|
||||
for field in challenge_part.split(",") {
|
||||
let mut field_split = field.split("=");
|
||||
let key = field_split.next().unwrap();
|
||||
let value = field_split.next().unwrap();
|
||||
field_map.insert(key, &value[1..value.len() - 1]);
|
||||
}
|
||||
let realm = field_map.get("realm").unwrap().to_owned();
|
||||
field_map.remove("realm").unwrap();
|
||||
let mut url = reqwest::Url::parse(realm).unwrap();
|
||||
|
||||
{
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
for (key, value) in field_map {
|
||||
query_pairs.append_pair(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
fn fetch_manifest(
|
||||
&self,
|
||||
image: &ImageIdentifier,
|
||||
additional_headers: Option<reqwest::header::HeaderMap>,
|
||||
) -> reqwest::blocking::Response {
|
||||
let image_url = format!(
|
||||
"https://registry.hub.docker.com/v2/{}/{}/manifests/{}",
|
||||
image.author, image.name, image.tag
|
||||
);
|
||||
let image_manifest = self
|
||||
.http_client
|
||||
.get(&image_url)
|
||||
.headers(additional_headers.unwrap_or_default())
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
return image_manifest;
|
||||
}
|
||||
|
||||
pub fn pull(&mut self, image: &ImageIdentifier, destination: &str) {
|
||||
let mut header_map = reqwest::header::HeaderMap::new();
|
||||
|
||||
let mut manifest_resp = self.fetch_manifest(image, None);
|
||||
|
||||
// Perform the little auth dance if necessary
|
||||
if manifest_resp.status() != reqwest::StatusCode::OK {
|
||||
let auth_header = manifest_resp
|
||||
.headers()
|
||||
.get(reqwest::header::WWW_AUTHENTICATE)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let challenge_url = Registry::build_challenge_url(auth_header);
|
||||
let challenge_body: serde_json::Value = self
|
||||
.http_client
|
||||
.get(challenge_url)
|
||||
.send()
|
||||
.unwrap()
|
||||
.json()
|
||||
.unwrap();
|
||||
let access_token = challenge_body["token"].as_str().unwrap();
|
||||
header_map.append(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("Bearer {}", access_token).parse().unwrap(),
|
||||
);
|
||||
|
||||
manifest_resp = self.fetch_manifest(image, Some(header_map.to_owned()));
|
||||
}
|
||||
|
||||
let image_manifest: serde_json::Value = manifest_resp.json().unwrap();
|
||||
|
||||
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)
|
||||
.headers(header_map.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_image_name() {
|
||||
assert_eq!(
|
||||
ImageIdentifier::from_string(&"library/ubuntu:latest".to_string()),
|
||||
ImageIdentifier {
|
||||
author: "library".to_string(),
|
||||
name: "ubuntu".to_string(),
|
||||
tag: "latest".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ImageIdentifier::from_string(&"alpine".to_string()),
|
||||
ImageIdentifier {
|
||||
author: "library".to_string(),
|
||||
name: "alpine".to_string(),
|
||||
tag: "latest".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ImageIdentifier::from_string(&"ghcr.io/dusk-labs/dim:dev".to_string()),
|
||||
ImageIdentifier {
|
||||
author: "dusk-labs".to_string(),
|
||||
name: "dim".to_string(),
|
||||
tag: "dev".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ImageIdentifier::from_string(&"bitnami/redis:7.0".to_string()),
|
||||
ImageIdentifier {
|
||||
author: "bitnami".to_string(),
|
||||
name: "redis".to_string(),
|
||||
tag: "7.0".to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user