diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..fa1050b --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +Dockerfile diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..f8697af --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "metaparticle" +version = "0.0.1" + +[[example]] +name = "hello" +path = "examples/hello.rs" + +[[example]] +name = "web" +path = "examples/web.rs" + +[dependencies] \ No newline at end of file diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..80f44d4 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,52 @@ +# Metaparticle for Rust +Metaparticle/Package is a collection of libraries intended to +make building and deploying containers a seamless and idiomatic +experience for developers. + +This is the implementation for Rust. + +## Introduction +Metaparticle/Package simplifies and centralizes the task of +building and deploying a container image. + +Here is a quick example. + +Consider this simple Rust application: +```rust +fn main() { + println!("Hello World!"); +} +``` + +To containerize this application, you need to use the `metaparticle` crate and +the `containerize` wrapper function like this: + +```rust +fn run() { + println!("Hello World!"); +} + +fn main() { + let runtime = metaparticle::Runtime{ + ..Default::default() + }; + let package = metaparticle::Package{ + name: "hello".to_string(), + ..Default::default() + }; + metaparticle::containerize(run, runtime, package) +} +``` + +When you run this application, instead of printing "Hello world", it first packages itself as a container, and +then (optionally) deploys itself inside that container. + +## Tutorial + +```bash +git clone https://github.com/metaparticle-io/package/ +cd package/rust + +cargo build --example hello +./target/debug/examples/hello +``` \ No newline at end of file diff --git a/rust/examples/hello.rs b/rust/examples/hello.rs new file mode 100644 index 0000000..8d0145b --- /dev/null +++ b/rust/examples/hello.rs @@ -0,0 +1,17 @@ +extern crate metaparticle; + +fn run() { + println!("Hello World!"); +} + +fn main() { + let runtime = metaparticle::Runtime{ + ..Default::default() + }; + let package = metaparticle::Package{ + name: "hello".to_string(), + repository: "brendanburns".to_string(), + ..Default::default() + }; + metaparticle::containerize(run, runtime, package) +} \ No newline at end of file diff --git a/rust/examples/web.rs b/rust/examples/web.rs new file mode 100644 index 0000000..8a9645a --- /dev/null +++ b/rust/examples/web.rs @@ -0,0 +1,61 @@ +extern crate metaparticle; + +use std::net::{TcpStream, TcpListener}; +use std::io::{Read, Write}; +use std::thread; + + +fn handle_read(mut stream: &TcpStream) { + let mut buf = [0u8 ;4096]; + match stream.read(&mut buf) { + Ok(_) => { + let req_str = String::from_utf8_lossy(&buf); + println!("{}", req_str); + }, + Err(e) => println!("Unable to read stream: {}", e), + } +} + +fn handle_write(mut stream: TcpStream) { + let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\nHello world\r\n"; + match stream.write(response) { + Ok(_) => println!("Response sent"), + Err(e) => println!("Failed sending response: {}", e), + } +} + +fn handle_client(stream: TcpStream) { + handle_read(&stream); + handle_write(stream); +} + +fn run() { + let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); + println!("Listening for connections on port {}", 8080); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(|| { + handle_client(stream) + }); + } + Err(e) => { + println!("Unable to connect: {}", e); + } + } + } +} + +fn main() { + let runtime = metaparticle::Runtime{ + ports: Some(8080), + ..Default::default() + }; + let package = metaparticle::Package{ + name: "web".to_string(), + repository: "brendanburns".to_string(), + ..Default::default() + }; + metaparticle::containerize(run, runtime, package) +} \ No newline at end of file diff --git a/rust/src/builder/docker.rs b/rust/src/builder/docker.rs new file mode 100644 index 0000000..83f6e7e --- /dev/null +++ b/rust/src/builder/docker.rs @@ -0,0 +1,13 @@ +use super::run_docker_process; +use Builder; + +pub struct DockerBuilder{} + +impl Builder for DockerBuilder { + fn build(&self, dir: &str, image: &str) { + run_docker_process(vec!["docker", "build", &*format!("-t{}", image), dir]); + } + fn push(&self, image: &str) { + run_docker_process(vec!["docker", "push", image]); + } +} \ No newline at end of file diff --git a/rust/src/builder/mod.rs b/rust/src/builder/mod.rs new file mode 100644 index 0000000..17e443f --- /dev/null +++ b/rust/src/builder/mod.rs @@ -0,0 +1,8 @@ +pub mod docker; +use super::run_docker_process; + + +pub trait Builder { + fn build(&self, dir: &str, image: &str); + fn push(&self, image: &str); +} \ No newline at end of file diff --git a/rust/src/executor/docker.rs b/rust/src/executor/docker.rs new file mode 100644 index 0000000..31610af --- /dev/null +++ b/rust/src/executor/docker.rs @@ -0,0 +1,31 @@ +use super::run_docker_process; +use super::Runtime; +use Executor; + +pub struct DockerExecutor{} + +impl Executor for DockerExecutor { + fn cancel(&self, name: &str) { + run_docker_process(vec!["docker", "stop", name]); + run_docker_process(vec!["docker", "rm", "-f", name]); + } + + fn logs(&self, name: &str) { + run_docker_process(vec!["docker", "logs", "-f", name]); + } + + fn run(&self, image: &str, name: &str, config: Runtime) { + let mut ports = String::new(); + let mut args = vec!["docker", "run", "-d", "--rm", "--name", name]; + + if let Some(port) = config.ports { + ports.push_str(&format!("-p {port}", port=port)); + args.push(&ports); + } + + args.push(image); + + run_docker_process(args); + } +} + diff --git a/rust/src/executor/mod.rs b/rust/src/executor/mod.rs new file mode 100644 index 0000000..9221027 --- /dev/null +++ b/rust/src/executor/mod.rs @@ -0,0 +1,9 @@ +pub mod docker; +use super::Runtime; +use super::run_docker_process; + +pub trait Executor { + fn cancel(&self, name: &str); + fn logs(&self, name: &str); + fn run(&self, image: &str, name: &str, config: Runtime); +} \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..532752f --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,166 @@ + +mod builder; +mod executor; + +use builder::Builder; +use executor::Executor; + +use std::env; +use std::error::Error; +use std::fs::File; +use std::ffi::OsStr; +use std::io::prelude::*; +use std::path::Path; +use std::process; + +#[derive(Debug)] +pub struct Runtime { + pub replicas: Option, + pub shards: Option, + pub url_shard_pattern: Option, + pub executor: String, + pub ports: Option, + pub public_address: Option, +} + + +impl Default for Runtime { + fn default() -> Runtime { + Runtime { + replicas: None, + shards: None, + url_shard_pattern: Some("".to_string()), + executor: "docker".to_string(), + ports: None, + public_address: Some(false) + } + } +} + +#[derive(Debug)] +pub struct Package { + pub name: String, + pub repository: String, + pub verbose: Option, + pub quiet: Option, + pub builder: String, + pub publish: Option, +} + + +impl Default for Package { + fn default() -> Package { + Package { + name: "".to_string(), + repository: "".to_string(), + verbose: Some(false), + quiet: Some(false), + builder: "docker".to_string(), + publish: Some(false) + } + } +} + +pub fn run_docker_process(args: Vec<&str>) { + let name = args[1].clone(); + let cmd = args.join(" "); + + let mut child = process::Command::new("sh") + .arg("-c") + .arg(cmd) + .spawn() + .expect(&format!("failed to execute 'docker {name}'", name=name)); + + let status = child.wait() + .ok().expect(&format!("couldn't wait for 'docker {name}'", name=name)); + + if !status.success() { + match status.code() { + Some(code) => panic!("'docker {}' failed with code {:?}", name, code), + None => panic!("'docker {}' failed", name) + } + } +} + +fn in_docker_container() -> bool { + let env_var_key = OsStr::new("METAPARTICLE_IN_CONTAINER"); + let env_var = env::var(env_var_key); + if let Ok(value) = env_var { + return value == "true" || value == "1"; + } + + let mut buffer = String::with_capacity(256); // kind of a guess on initial capacity + + if let Ok(mut file) = File::open("/proc/1/cgroup") { + if let Ok(_) = file.read_to_string(&mut buffer) { + return buffer.contains("docker"); + } + } + false +} + +fn executor_from_runtime(executor_name: String) -> Box { + let executor : Box = match executor_name.as_ref() { + "docker" => Box::new(executor::docker::DockerExecutor{}), + _ => panic!("Unsupported executor type {}", executor_name), + }; + return executor; +} + +fn build_from_runtime(builder_name: String) -> Box { + let builder : Box = match builder_name.as_ref() { + "docker" => Box::new(builder::docker::DockerBuilder{}), + _ => panic!("Unsupported builder type {}", builder_name), + }; + builder + +} + +fn write_dockerfile(name: &str, dest: &Path) { + let dockerfile = &format!("FROM ubuntu:16.04 + COPY ./{name} /tmp/{name} + CMD /tmp/{name} + ", name=name); + let file = Path::new("Dockerfile"); + let path = dest.join(&file); + let display = path.display(); + + let mut file = match File::create(&path) { + Err(why) => panic!("couldn't create {}: {}", + display, + why.description()), + Ok(file) => file, + }; + + if let Err(why) = file.write_all(dockerfile.as_bytes()) { + panic!("Could not write dockerfile at {} because {}", display, why.description()); + } +} + +pub fn containerize(f: F, runtime: Runtime, package: Package) where F: Fn() { + if in_docker_container() { + f(); + } else { + + if package.repository.len() == 0 { + panic!("A package must be given a 'repository' value"); + } + if package.name.len() == 0 { + panic!("A package must be given a 'name' value"); + } + let image = &format!("{repo}/{name}:latest", repo=package.repository, name=package.name); + + let arg_0 = env::args().nth(0).unwrap(); + let path = Path::new(&arg_0); + let docker_context = Path::new(path.parent().unwrap()); + + write_dockerfile(&package.name, docker_context); + let builder = build_from_runtime(package.builder.clone()); + + builder.build(docker_context.to_str().unwrap(), image); + + let executor = executor_from_runtime(runtime.executor.clone()); + executor.run(image, &package.name, runtime); + executor.logs(&package.name); + } +} \ No newline at end of file