summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsoftprops <d.tangren@gmail.com>2016-01-18 01:45:44 -0500
committersoftprops <d.tangren@gmail.com>2016-01-18 01:45:44 -0500
commit7f9b3e4b77c8dcdbf5e6b7274cb30a4d2086fccb (patch)
tree29516dcf506a4d4779bf4319da68e5141272726e
parent89740ae3c4ece331d8dbb731de14d60ee7f8a0b5 (diff)
massive progress
-rw-r--r--Cargo.toml1
-rw-r--r--build.tarbin0 -> 3072 bytes
-rw-r--r--build.tgzbin0 -> 186 bytes
l---------examples/.#imagebuild.rs1
-rw-r--r--examples/imagebuild.rs6
-rw-r--r--examples/imagecreate.rs16
-rw-r--r--src/builder.rs5
-rw-r--r--src/lib.rs121
-rw-r--r--src/rep.rs29
-rw-r--r--src/tarball.rs51
-rw-r--r--src/transport.rs19
11 files changed, 185 insertions, 64 deletions
diff --git a/Cargo.toml b/Cargo.toml
index c23ac25..d213cf4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
new file mode 100644
index 0000000..0025909
--- /dev/null
+++ b/build.tar
Binary files differ
diff --git a/build.tgz b/build.tgz
new file mode 100644
index 0000000..5d793b7
--- /dev/null
+++ b/build.tgz
Binary files differ
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
diff --git a/src/lib.rs b/src/lib.rs
index 1cf96a8..1fa6f1e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)>)
}
}
diff --git a/src/rep.rs b/src/rep.rs
index 2f8e9bd..9b9f1d6 100644
--- a/src/rep.rs
+++ b/src/rep.rs
@@ -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());