diff options
author | softprops <d.tangren@gmail.com> | 2016-01-18 01:45:44 -0500 |
---|---|---|
committer | softprops <d.tangren@gmail.com> | 2016-01-18 01:45:44 -0500 |
commit | 7f9b3e4b77c8dcdbf5e6b7274cb30a4d2086fccb (patch) | |
tree | 29516dcf506a4d4779bf4319da68e5141272726e | |
parent | 89740ae3c4ece331d8dbb731de14d60ee7f8a0b5 (diff) |
massive progress
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | build.tar | bin | 0 -> 3072 bytes | |||
-rw-r--r-- | build.tgz | bin | 0 -> 186 bytes | |||
l--------- | examples/.#imagebuild.rs | 1 | ||||
-rw-r--r-- | examples/imagebuild.rs | 6 | ||||
-rw-r--r-- | examples/imagecreate.rs | 16 | ||||
-rw-r--r-- | src/builder.rs | 5 | ||||
-rw-r--r-- | src/lib.rs | 121 | ||||
-rw-r--r-- | src/rep.rs | 29 | ||||
-rw-r--r-- | src/tarball.rs | 51 | ||||
-rw-r--r-- | src/transport.rs | 19 |
11 files changed, 185 insertions, 64 deletions
@@ -11,6 +11,7 @@ keywords = ["docker", "unix", "containers", "hyper", "ship"] license = "MIT" [dependencies] +flate2 = "0.2" hyperlocal = "0.1" log = "0.3" jed = "0.1" diff --git a/build.tar b/build.tar Binary files differnew file mode 100644 index 0000000..0025909 --- /dev/null +++ b/build.tar diff --git a/build.tgz b/build.tgz Binary files differnew file mode 100644 index 0000000..5d793b7 --- /dev/null +++ b/build.tgz diff --git a/examples/.#imagebuild.rs b/examples/.#imagebuild.rs new file mode 120000 index 0000000..b5ce94d --- /dev/null +++ b/examples/.#imagebuild.rs @@ -0,0 +1 @@ +dougtangren@Dougs-MacBook-Air.local.58180
\ No newline at end of file diff --git a/examples/imagebuild.rs b/examples/imagebuild.rs index ed08949..6de23be 100644 --- a/examples/imagebuild.rs +++ b/examples/imagebuild.rs @@ -7,8 +7,10 @@ fn main() { let docker = Docker::new(); if let Some(path) = env::args().nth(1) { let image = docker.images() - .build(&BuildOptions::builder(path).build()) + .build(&BuildOptions::builder(path).tag("shiplift_test").build()) .unwrap(); - println!("{:?}", image); + for output in image { + println!("{:?}", output); + } } } diff --git a/examples/imagecreate.rs b/examples/imagecreate.rs new file mode 100644 index 0000000..2b1b895 --- /dev/null +++ b/examples/imagecreate.rs @@ -0,0 +1,16 @@ +extern crate shiplift; + +use shiplift::Docker; +use std::env; + +fn main() { + let docker = Docker::new(); + if let Some(img) = env::args().nth(1) { + let image = docker.images() + .create(&img[..]) + .unwrap(); + for output in image { + println!("{:?}", output); + } + } +} diff --git a/src/builder.rs b/src/builder.rs index a436a1e..0bc0fdf 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -4,7 +4,6 @@ use self::super::Result; use std::collections::{BTreeMap, HashMap}; use rustc_serialize::json::{self, Json, ToJson}; use url::form_urlencoded; -use self::super::hyper::client::Body; #[derive(Default)] pub struct BuildOptions { @@ -13,9 +12,12 @@ pub struct BuildOptions { } impl BuildOptions { + /// return a new instance of a builder for options pub fn builder<S>(path: S) -> BuildOptionsBuilder where S: Into<String> { BuildOptionsBuilder::new(path) } + + /// serialize options as a string. returns None if no options are defined pub fn serialize(&self) -> Option<String> { if self.params.is_empty() { None @@ -220,6 +222,7 @@ impl EventsOptions { EventsOptionsBuilder::new() } + /// serialize options as a string. returns None if no options are defined pub fn serialize(&self) -> Option<String> { if self.params.is_empty() { None @@ -15,7 +15,11 @@ #[macro_use] extern crate log; +#[macro_use] +extern crate mime; +#[macro_use] extern crate hyper; +extern crate flate2; extern crate hyperlocal; extern crate jed; extern crate openssl; @@ -28,29 +32,29 @@ pub mod rep; pub mod transport; pub mod errors; -use std::fs::{self, DirEntry, File}; -use tar::Archive; +mod tarball; pub use errors::Error; pub use builder::{BuildOptions, ContainerOptions, ContainerListOptions, ContainerFilter, EventsOptions, ImageFilter, ImageListOptions, LogsOptions}; use hyper::{Client, Url}; +use hyper::header::ContentType; use hyper::net::{HttpsConnector, Openssl}; use hyper::method::Method; use hyperlocal::UnixSocketConnector; use openssl::x509::X509FileType; use openssl::ssl::{SslContext, SslMethod}; use rep::Image as ImageRep; -use rep::{Change, ContainerCreateInfo, ContainerDetails, Container as ContainerRep, Event, Exit, History, ImageDetails, +use rep::{PullOutput, PullInfo, BuildOutput, Change, ContainerCreateInfo, ContainerDetails, Container as ContainerRep, Event, Exit, History, ImageDetails, Info, SearchResult, Stats, Status, Top, Version}; use rustc_serialize::json::{self, Json}; use std::env::{self, VarError}; -use std::io::{self, Read}; +use std::io::Read; use std::iter::IntoIterator; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use transport::{Transport}; +use transport::{tar, Transport}; use hyper::client::Body; use url::{form_urlencoded, Host, RelativeSchemeData, SchemeData}; @@ -133,39 +137,35 @@ impl<'a> Images<'a> { Images { docker: docker } } - pub fn build(&self, opts: &BuildOptions) -> Result<String> { + pub fn build(&self, opts: &BuildOptions) -> Result<Box<Iterator<Item = BuildOutput>>> { let mut path = vec!["/build".to_owned()]; if let Some(query) = opts.serialize() { path.push(query) } - - let file = File::create("build.tar").unwrap(); - let mut a = Archive::new(file); - - fn visit_dirs(dir: &Path, cb: &Fn(&DirEntry)) -> io::Result<()> { - if try!(fs::metadata(dir)).is_dir() { - for entry in try!(fs::read_dir(dir)) { - let entry = try!(entry); - if try!(fs::metadata(entry.path())).is_dir() { - try!(visit_dirs(&entry.path(), cb)); - } else { - cb(&entry); - } - } - } - Ok(()) - } - - { - let append = |e: &DirEntry| { - a.append_path(e.path()); - }; - visit_dirs(Path::new(&opts.path[..]), &append); - a.finish(); - } - - self.docker.post(&path.join("?"), Some(Body::ChunkedBody(&mut a.into_inner()))) + let mut f = try!(tarball::dir(&opts.path[..])); + let raw = try!(self.docker.stream_post(&path.join("?"), Some((Body::ChunkedBody(&mut f), tar())))); + let it = jed::Iter::new(raw).into_iter().map(|j| { + // fixme: better error handling + debug!("{:?}", j); + let obj = j.as_object().expect("expected json object"); + obj.get("stream") + .map(|stream| BuildOutput::Stream( + stream.as_string() + .expect("expected stream to be a string") + .to_owned() + ) + ) + .or(obj.get("error") + .map(|err| BuildOutput::Err( + err.as_string() + .expect("expected error to be a string") + .to_owned() + ) + ) + ).expect("expected build output stream or error") + }); + Ok(Box::new(it)) } /// Lists the docker images on the current docker host @@ -191,9 +191,25 @@ impl<'a> Images<'a> { } /// Create a new docker images from an existing image - pub fn create(&self, from: &str) -> Result<Box<Read>> { + pub fn create(&self, from: &str) -> Result<Box<Iterator<Item = PullOutput>>> { let query = form_urlencoded::serialize(vec![("fromImage", from)]); - self.docker.stream_post(&format!("/images/create?{}", query)[..]) + let raw = try!(self.docker.stream_post(&format!("/images/create?{}", query)[..], None as Option<(&'a str, ContentType)>)); + let it = jed::Iter::new(raw).into_iter().map(|j| { + // fixme: better error handling + debug!("{:?}",j); + let s = json::encode(&j).unwrap(); + json::decode::<PullInfo>(&s) + .map(|info| PullOutput::Status(info)).ok() + .or(j.as_object().expect("expected json object").get("error") + .map(|err| PullOutput::Err( + err.as_string() + .expect("expected error to be a string") + .to_owned() + ) + ) + ).expect("expected pull status or error") + }); + Ok(Box::new(it)) } /// exports a collection of named images, @@ -269,6 +285,7 @@ impl<'a, 'b> Container<'a, 'b> { let raw = try!(self.docker.stream_get(&format!("/containers/{}/stats", self.id)[..])); let it = jed::Iter::new(raw).into_iter().map(|j| { // fixme: better error handling + debug!("{:?}", j); let s = json::encode(&j).unwrap(); json::decode::<Stats>(&s).unwrap() }); @@ -277,7 +294,7 @@ impl<'a, 'b> Container<'a, 'b> { /// Start the container instance pub fn start(&'a self) -> Result<()> { - self.docker.post(&format!("/containers/{}/start", self.id)[..], None as Option<&'a str>).map(|_| ()) + self.docker.post(&format!("/containers/{}/start", self.id)[..], None as Option<(&'a str, ContentType)>).map(|_| ()) } /// Stop the container instance @@ -287,7 +304,7 @@ impl<'a, 'b> Container<'a, 'b> { let encoded = form_urlencoded::serialize(vec![("t", w.as_secs().to_string())]); path.push(encoded) } - self.docker.post(&path.join("?"), None as Option<&'a str>).map(|_| ()) + self.docker.post(&path.join("?"), None as Option<(&'a str, ContentType)>).map(|_| ()) } /// Restart the container instance @@ -297,7 +314,7 @@ impl<'a, 'b> Container<'a, 'b> { let encoded = form_urlencoded::serialize(vec![("t", w.as_secs().to_string())]); path.push(encoded) } - self.docker.post(&path.join("?"), None as Option<&'a str>).map(|_| ()) + self.docker.post(&path.join("?"), None as Option<(&'a str, ContentType)>).map(|_| ()) } /// Kill the container instance @@ -307,7 +324,7 @@ impl<'a, 'b> Container<'a, 'b> { let encoded = form_urlencoded::serialize(vec![("signal", sig.to_owned())]); path.push(encoded) } - self.docker.post(&path.join("?"), None as Option<&'a str>).map(|_| ()) + self.docker.post(&path.join("?"), None as Option<(&'a str, ContentType)>).map(|_| ()) } /// Rename the container instance @@ -315,26 +332,23 @@ impl<'a, 'b> Container<'a, 'b> { let query = form_urlencoded::serialize(vec![("name", name)]); self.docker .post(&format!("/containers/{}/rename?{}", self.id, query)[..], - None as Option<&'a str>) + None as Option<(&'a str, ContentType)>) .map(|_| ()) } /// Pause the container instance pub fn pause(&self) -> Result<()> { - let empty: Option<&'a str> = None; - self.docker.post(&format!("/containers/{}/pause", self.id)[..], empty).map(|_| ()) + self.docker.post(&format!("/containers/{}/pause", self.id)[..], None as Option<(&'a str, ContentType)>).map(|_| ()) } /// Unpause the container instance pub fn unpause(&self) -> Result<()> { - let empty: Option<&'a str> = None; - self.docker.post(&format!("/containers/{}/unpause", self.id)[..], empty).map(|_| ()) + self.docker.post(&format!("/containers/{}/unpause", self.id)[..], None as Option<(&'a str, ContentType)>).map(|_| ()) } /// Wait until the container stops pub fn wait(&self) -> Result<Exit> { - let empty: Option<&'a str> = None; - let raw = try!(self.docker.post(&format!("/containers/{}/wait", self.id)[..], empty)); + let raw = try!(self.docker.post(&format!("/containers/{}/wait", self.id)[..], None as Option<(&'a str, ContentType)>)); Ok(try!(json::decode::<Exit>(&raw))) } @@ -377,7 +391,7 @@ impl<'a> Containers<'a> { let data = try!(opts.serialize()); let mut bytes = data.as_bytes(); let raw = try!(self.docker.post("/containers/create", - Some(&mut bytes))); + Some((&mut bytes, ContentType::json())))); Ok(try!(json::decode::<ContainerCreateInfo>(&raw))) } } @@ -487,6 +501,7 @@ impl Docker { } let raw = try!(self.stream_get(&path.join("?")[..])); let it = jed::Iter::new(raw).into_iter().map(|j| { + debug!("{:?}", j); // fixme: better error handling let s = json::encode(&j).unwrap(); json::decode::<Event>(&s).unwrap() @@ -495,22 +510,22 @@ impl Docker { } fn get<'a>(&self, endpoint: &str) -> Result<String> { - self.transport.request(Method::Get, endpoint, None as Option<&'a str>) + self.transport.request(Method::Get, endpoint, None as Option<(&'a str, ContentType)>) } - fn post<'a, B>(&'a self, endpoint: &str, body: Option<B>) -> Result<String> where B: Into<Body<'a>> { + fn post<'a, B>(&'a self, endpoint: &str, body: Option<(B, ContentType)>) -> Result<String> where B: Into<Body<'a>> { self.transport.request(Method::Post, endpoint, body) } fn delete<'a>(&self, endpoint: &str) -> Result<String> { - self.transport.request(Method::Delete, endpoint, None as Option<&'a str>) + self.transport.request(Method::Delete, endpoint, None as Option<(&'a str, ContentType)>) } - fn stream_post<'a>(&self, endpoint: &str) -> Result<Box<Read>> { - self.transport.stream(Method::Post, endpoint, None as Option<&'a str>) + fn stream_post<'a, B>(&'a self, endpoint: &str, body:Option<(B, ContentType)>) -> Result<Box<Read>> where B: Into<Body<'a>> { + self.transport.stream(Method::Post, endpoint, body) } fn stream_get<'a>(&self, endpoint: &str) -> Result<Box<Read>> { - self.transport.stream(Method::Get, endpoint, None as Option<&'a str>) + self.transport.stream(Method::Get, endpoint, None as Option<(&'a str, ContentType)>) } } @@ -373,6 +373,35 @@ pub struct Event { } #[derive(Debug)] +pub enum BuildOutput { + Stream(String), + Err(String) +} + +// fixme: all fields are options because PullInfo.progressDefault is sometimes an empty object instead of a null/absent value +#[derive(Debug, RustcDecodable)] +pub struct ProgressDetail { + current: Option<u64>, + total: Option<u64>, + status: Option<String> // fixme: it looks like this field isn't deserializing properly +} + +#[derive(Debug, RustcDecodable)] +#[allow(non_snake_case)] +pub struct PullInfo { + id: Option<String>, + status: String, + progress: Option<String>, + progressDetail: Option<ProgressDetail> +} + +#[derive(Debug)] +pub enum PullOutput { + Status(PullInfo), + Err(String) +} + +#[derive(Debug)] pub enum Status { Untagged(String), Deleted(String), diff --git a/src/tarball.rs b/src/tarball.rs new file mode 100644 index 0000000..f13c49f --- /dev/null +++ b/src/tarball.rs @@ -0,0 +1,51 @@ + +use flate2::Compression; +use flate2::write::GzEncoder; +use std::fs::{self, File, OpenOptions}; +use std::path::{Path, MAIN_SEPARATOR}; +use std::io; +use tar::Archive; + +// todo: factor this into its own crate +pub fn dir(path: &str) -> io::Result<File> { + let file = OpenOptions::new().read(true).write(true).create(true).open("build.tgz").unwrap(); + let zipper = GzEncoder::new(file, Compression::Best); + let archive = Archive::new(zipper); + fn bundle(dir: &Path, cb: &Fn(&Path), bundle_dir: bool) -> io::Result<()> { + if try!(fs::metadata(dir)).is_dir() { + if bundle_dir { + cb(&dir); + } + for entry in try!(fs::read_dir(dir)) { + let entry = try!(entry); + if try!(fs::metadata(entry.path())).is_dir() { + try!(bundle(&entry.path(), cb, true)); + } else { + cb(&entry.path().as_path()); + } + } + } + Ok(()) + } + + { + let base_path = Path::new(path).canonicalize().unwrap(); + let mut base_path_str = base_path.to_str().unwrap().to_owned(); + if base_path_str.chars().last().unwrap() != MAIN_SEPARATOR { + base_path_str.push(MAIN_SEPARATOR) + } + + let append = |path: &Path| { + let canonical = path.canonicalize().unwrap(); + let relativized = canonical.to_str().unwrap().trim_left_matches(&base_path_str[..]); + if path.is_dir() { + archive.append_dir(Path::new(relativized), &canonical).unwrap(); + } else { + archive.append_file(Path::new(relativized), &mut File::open(&canonical).unwrap()).unwrap(); + } + }; + try!(bundle(Path::new(path), &append, false)); + try!(archive.finish()); + } + File::open("build.tgz") +} diff --git a/src/transport.rs b/src/transport.rs index c035fa5..6795f85 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -4,7 +4,6 @@ extern crate hyper; extern crate mime; use hyper::Client; -use hyper::client; use hyper::client::Body; use self::super::{Error, Result}; use self::hyper::buffer::BufReader; @@ -12,10 +11,17 @@ use self::hyper::header::ContentType; use self::hyper::status::StatusCode; use hyper::method::Method; use std::fmt; -use std::ops::DerefMut; use std::io::{Read, Write}; use hyperlocal::DomainUrl; +pub fn tar() -> ContentType { + ContentType( + mime::Mime( + mime::TopLevel::Application, + mime::SubLevel::Ext(String::from("tar")), + vec![])) +} + /// Transports are types which define the means of communication /// with the docker daemon pub enum Transport { @@ -41,7 +47,7 @@ impl fmt::Debug for Transport { } impl Transport { - pub fn request<'a, B>(&'a self, method: Method, endpoint: &str, body: Option<B>) -> Result<String> where B: Into<Body<'a>> { + pub fn request<'a, B>(&'a self, method: Method, endpoint: &str, body: Option<(B, ContentType)>) -> Result<String> where B: Into<Body<'a>> { let mut res = match self.stream(method, endpoint, body) { Ok(r) => r, Err(e) => panic!("failed request {:?}", e), @@ -53,7 +59,7 @@ impl Transport { } pub fn stream<'c, B>( - &'c self, method: Method, endpoint: &str, body: Option<B> + &'c self, method: Method, endpoint: &str, body: Option<(B, ContentType)> ) -> Result<Box<Read>> where B: Into<Body<'c>> { let req = match *self { Transport::Tcp { ref client, ref host } => { @@ -65,10 +71,7 @@ impl Transport { }; let embodied = match body { - Some(b) => {//Body { read: r, size: l }) => { - //let reader: &mut Read = *r.deref_mut(); - req.header(ContentType::json()).body(b)//client::Body::SizedBody(reader, l)) - } + Some((b, c)) => req.header(c).body(b), _ => req, }; let res = try!(embodied.send()); |