summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnton Ageev <antage@gmail.com>2019-03-31 03:20:21 +0300
committerDoug Tangren <d.tangren@gmail.com>2019-03-30 20:20:21 -0400
commitac8789e257f5e4a477192bbbd8ecc2676bb485e5 (patch)
tree792377890e4d0293be366233acf510e2610bd4fd
parenteb98b1916c0220e44e2d0f3c869c01a2dd037f60 (diff)
Add a registry authentication. (#157)
-rw-r--r--Cargo.toml1
-rw-r--r--examples/imagepull_auth.rs28
-rw-r--r--src/builder.rs164
-rw-r--r--src/lib.rs27
-rw-r--r--src/transport.rs22
5 files changed, 226 insertions, 16 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 1ee57a5..23e803d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,6 +36,7 @@ tokio-io = "0.1.11"
url = "1.7.2"
serde = { version = "1.0.87", features = ["derive"] }
serde_json = "1.0.38"
+base64 = "0.10"
[dev-dependencies]
env_logger = "0.6.0"
diff --git a/examples/imagepull_auth.rs b/examples/imagepull_auth.rs
new file mode 100644
index 0000000..1c559c7
--- /dev/null
+++ b/examples/imagepull_auth.rs
@@ -0,0 +1,28 @@
+// cargo run --example imagepull_auth busybox username password
+
+use shiplift::{Docker, PullOptions, RegistryAuth};
+use std::env;
+use tokio::prelude::{Future, Stream};
+
+fn main() {
+ env_logger::init();
+ let docker = Docker::new();
+ let img = env::args()
+ .nth(1)
+ .expect("You need to specify an image name");
+ let username = env::args().nth(2).expect("You need to specify an username");
+ let password = env::args().nth(3).expect("You need to specify a password");
+ let auth = RegistryAuth::builder()
+ .username(username)
+ .password(password)
+ .build();
+ let fut = docker
+ .images()
+ .pull(&PullOptions::builder().image(img).auth(auth).build())
+ .for_each(|output| {
+ println!("{:?}", output);
+ Ok(())
+ })
+ .map_err(|e| eprintln!("Error: {}", e));
+ tokio::run(fut);
+}
diff --git a/src/builder.rs b/src/builder.rs
index 4923a24..f0c7d7e 100644
--- a/src/builder.rs
+++ b/src/builder.rs
@@ -11,8 +11,116 @@ use std::{
};
use url::form_urlencoded;
+#[derive(Clone, Serialize)]
+#[serde(untagged)]
+pub enum RegistryAuth {
+ Password {
+ username: String,
+ password: String,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ email: Option<String>,
+
+ #[serde(rename = "serveraddress")]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ server_address: Option<String>,
+ },
+ Token {
+ #[serde(rename = "identitytoken")]
+ identity_token: String,
+ },
+}
+
+impl RegistryAuth {
+ /// return a new instance with token authentication
+ pub fn token<S>(token: S) -> RegistryAuth
+ where
+ S: Into<String>,
+ {
+ RegistryAuth::Token {
+ identity_token: token.into(),
+ }
+ }
+
+ /// return a new instance of a builder for authentication
+ pub fn builder() -> RegistryAuthBuilder {
+ RegistryAuthBuilder::default()
+ }
+
+ /// serialize authentication as JSON in base64
+ pub fn serialize(&self) -> String {
+ serde_json::to_string(self)
+ .map(|c| base64::encode(&c))
+ .unwrap()
+ }
+}
+
+#[derive(Default)]
+pub struct RegistryAuthBuilder {
+ username: Option<String>,
+ password: Option<String>,
+ email: Option<String>,
+ server_address: Option<String>,
+}
+
+impl RegistryAuthBuilder {
+ pub fn username<I>(
+ &mut self,
+ username: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.username = Some(username.into());
+ self
+ }
+
+ pub fn password<I>(
+ &mut self,
+ password: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.password = Some(password.into());
+ self
+ }
+
+ pub fn email<I>(
+ &mut self,
+ email: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.email = Some(email.into());
+ self
+ }
+
+ pub fn server_address<I>(
+ &mut self,
+ server_address: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.server_address = Some(server_address.into());
+ self
+ }
+
+ pub fn build(&self) -> RegistryAuth {
+ RegistryAuth::Password {
+ username: self.username.clone().unwrap_or_else(String::new),
+ password: self.password.clone().unwrap_or_else(String::new),
+ email: self.email.clone(),
+ server_address: self.server_address.clone(),
+ }
+ }
+}
+
#[derive(Default)]
pub struct PullOptions {
+ auth: Option<RegistryAuth>,
params: HashMap<&'static str, String>,
}
@@ -34,10 +142,15 @@ impl PullOptions {
)
}
}
+
+ pub(crate) fn auth_header(&self) -> Option<String> {
+ self.auth.clone().map(|a| a.serialize())
+ }
}
#[derive(Default)]
pub struct PullOptionsBuilder {
+ auth: Option<RegistryAuth>,
params: HashMap<&'static str, String>,
}
@@ -95,8 +208,17 @@ impl PullOptionsBuilder {
self
}
- pub fn build(&self) -> PullOptions {
+ pub fn auth(
+ &mut self,
+ auth: RegistryAuth,
+ ) -> &mut Self {
+ self.auth = Some(auth);
+ self
+ }
+
+ pub fn build(&mut self) -> PullOptions {
PullOptions {
+ auth: self.auth.take(),
params: self.params.clone(),
}
}
@@ -1370,7 +1492,7 @@ impl VolumeCreateOptionsBuilder {
#[cfg(test)]
mod tests {
- use super::ContainerOptionsBuilder;
+ use super::{ContainerOptionsBuilder, RegistryAuth};
#[test]
fn container_options_simple() {
@@ -1463,4 +1585,42 @@ mod tests {
options.serialize().unwrap()
);
}
+
+ /// Test registry auth with token
+ #[test]
+ fn registry_auth_token() {
+ let options = RegistryAuth::token("abc");
+ assert_eq!(
+ base64::encode(r#"{"identitytoken":"abc"}"#),
+ options.serialize()
+ );
+ }
+
+ /// Test registry auth with username and password
+ #[test]
+ fn registry_auth_password_simple() {
+ let options = RegistryAuth::builder()
+ .username("user_abc")
+ .password("password_abc")
+ .build();
+ assert_eq!(
+ base64::encode(r#"{"username":"user_abc","password":"password_abc"}"#),
+ options.serialize()
+ );
+ }
+
+ /// Test registry auth with all fields
+ #[test]
+ fn registry_auth_password_all() {
+ let options = RegistryAuth::builder()
+ .username("user_abc")
+ .password("password_abc")
+ .email("email_abc")
+ .server_address("https://example.org")
+ .build();
+ assert_eq!(
+ base64::encode(r#"{"username":"user_abc","password":"password_abc","email":"email_abc","serveraddress":"https://example.org"}"#),
+ options.serialize()
+ );
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index 5918205..61a8bde 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,8 +29,8 @@ pub use crate::{
builder::{
BuildOptions, ContainerConnectionOptions, ContainerFilter, ContainerListOptions,
ContainerOptions, EventsOptions, ExecContainerOptions, ImageFilter, ImageListOptions,
- LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, RmContainerOptions,
- VolumeCreateOptions,
+ LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, RegistryAuth,
+ RmContainerOptions, VolumeCreateOptions,
},
errors::Error,
};
@@ -55,7 +55,7 @@ use mime::Mime;
#[cfg(feature = "tls")]
use openssl::ssl::{SslConnector, SslFiletype, SslMethod};
use serde_json::Value;
-use std::{borrow::Cow, env, path::Path, time::Duration};
+use std::{borrow::Cow, env, iter, path::Path, time::Duration};
use tokio_codec::{FramedRead, LinesCodec};
use url::form_urlencoded;
@@ -141,7 +141,11 @@ impl<'a> Images<'a> {
match tarball::dir(&mut bytes, &opts.path[..]) {
Ok(_) => Box::new(
self.docker
- .stream_post(&path.join("?"), Some((Body::from(bytes), tar())))
+ .stream_post(
+ &path.join("?"),
+ Some((Body::from(bytes), tar())),
+ None::<iter::Empty<_>>,
+ )
.and_then(|bytes| {
serde_json::from_slice::<'_, Value>(&bytes[..])
.map_err(Error::from)
@@ -194,8 +198,11 @@ impl<'a> Images<'a> {
if let Some(query) = opts.serialize() {
path.push(query);
}
+ let headers = opts
+ .auth_header()
+ .map(|a| iter::once(("X-Registry-Auth", a)));
self.docker
- .stream_post::<Body>(&path.join("?"), None)
+ .stream_post::<Body, _>(&path.join("?"), None, headers)
// todo: give this a proper enum type
.map(|r| {
futures::stream::iter_result(
@@ -479,6 +486,7 @@ impl<'a, 'b> Container<'a, 'b> {
let chunk_stream = StreamReader::new(docker2.stream_post(
&format!("/exec/{}/start", id)[..],
Some((bytes, mime::APPLICATION_JSON)),
+ None::<iter::Empty<_>>,
));
FramedRead::new(chunk_stream, decoder)
})
@@ -1053,15 +1061,18 @@ impl Docker {
})
}
- fn stream_post<B>(
+ fn stream_post<B, H>(
&self,
endpoint: &str,
body: Option<(B, Mime)>,
+ headers: Option<H>,
) -> impl Stream<Item = hyper::Chunk, Error = Error>
where
B: Into<Body>,
+ H: IntoIterator<Item = (&'static str, String)>,
{
- self.transport.stream_chunks(Method::POST, endpoint, body)
+ self.transport
+ .stream_chunks(Method::POST, endpoint, body, headers)
}
fn stream_get(
@@ -1069,7 +1080,7 @@ impl Docker {
endpoint: &str,
) -> impl Stream<Item = hyper::Chunk, Error = Error> {
self.transport
- .stream_chunks::<Body>(Method::GET, endpoint, None)
+ .stream_chunks::<Body, iter::Empty<_>>(Method::GET, endpoint, None, None)
}
fn stream_post_upgrade_multiplexed<B>(
diff --git a/src/transport.rs b/src/transport.rs
index da506b3..ce089fc 100644
--- a/src/transport.rs
+++ b/src/transport.rs
@@ -19,7 +19,7 @@ use log::debug;
use mime::Mime;
use serde::{Deserialize, Serialize};
use serde_json;
-use std::fmt;
+use std::{fmt, iter};
use tokio_io::{AsyncRead, AsyncWrite};
pub fn tar() -> Mime {
@@ -76,7 +76,7 @@ impl Transport {
B: Into<Body>,
{
let endpoint = endpoint.to_string();
- self.stream_chunks(method, &endpoint, body)
+ self.stream_chunks(method, &endpoint, body, None::<iter::Empty<_>>)
.concat2()
.and_then(|v| {
String::from_utf8(v.to_vec())
@@ -87,17 +87,19 @@ impl Transport {
}
/// Make a request and return a `Stream` of `Chunks` as they are returned.
- pub fn stream_chunks<B>(
+ pub fn stream_chunks<B, H>(
&self,
method: Method,
endpoint: &str,
body: Option<(B, Mime)>,
+ headers: Option<H>,
) -> impl Stream<Item = Chunk, Error = Error>
where
B: Into<Body>,
+ H: IntoIterator<Item = (&'static str, String)>,
{
let req = self
- .build_request(method, endpoint, body, |_| ())
+ .build_request(method, endpoint, body, headers, |_| ())
.expect("Failed to build request!");
self.send_request(req)
@@ -140,15 +142,17 @@ impl Transport {
}
/// Builds an HTTP request.
- fn build_request<B>(
+ fn build_request<B, H>(
&self,
method: Method,
endpoint: &str,
body: Option<(B, Mime)>,
+ headers: Option<H>,
f: impl FnOnce(&mut ::http::request::Builder),
) -> Result<Request<Body>>
where
B: Into<Body>,
+ H: IntoIterator<Item = (&'static str, String)>,
{
let mut builder = Request::builder();
f(&mut builder);
@@ -169,6 +173,12 @@ impl Transport {
};
let req = req.header(header::HOST, "");
+ if let Some(h) = headers {
+ for (k, v) in h.into_iter() {
+ req.header(k, v);
+ }
+ }
+
match body {
Some((b, c)) => Ok(req
.header(header::CONTENT_TYPE, &c.to_string()[..])
@@ -215,7 +225,7 @@ impl Transport {
};
let req = self
- .build_request(method, endpoint, body, |builder| {
+ .build_request(method, endpoint, body, None::<iter::Empty<_>>, |builder| {
builder
.header(header::CONNECTION, "Upgrade")
.header(header::UPGRADE, "tcp");