Skip to content

Commit

Permalink
Unix socket support
Browse files Browse the repository at this point in the history
  • Loading branch information
blazzy committed Jul 1, 2024
1 parent f863056 commit 9d245a8
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 77 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ serde_json = "1.0.118"
thiserror = "1.0.61"
tokio = "1.38.0"
tower-service = "0.3.2"

[dev-dependencies]
tokio-test = "0.4.4"
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,33 @@

Provides an interface for querying the podman rest api. Most of the interface is generated from
the the official podman swagger file. This crate adds a layer to make it possible to connect to
podman over ssh. This is commonly necessary on macOs where the container runtime runs in a
virtual machine you connect to over ssh.
the podman rest api over ssh to a unix socket and directl to a unix socket. Connections over
ssh are commonly necessary on macOs where the container runtime runs in a virtual machine
accessible over ssh.

### Connecting via a unix socket on Linux

```rust
let client = PodmanRestCli
use podman_rest_client::PodmanRestClient;

let uri = "unix://path_to_unix_socket";

let client = PodmanRestClient::new(uri, None).await.unwrap();

let images = client.images_api().image_list_libpod(None,None).await.unwrap();
```

### Connecting via ssh from macOS

```rust
use podman_rest_client::PodmanRestClient;

let uri = "ssh://[email protected]:63169/run/user/501/podman/podman.sock";
let key: Option<String> = Some("/path/to/key".into());

let client = PodmanRestClient::new(uri, None).await.unwrap();

let images = client.images_api().image_list_libpod(None,None).await.unwrap();
```

<!-- cargo-rdme end -->
Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ pub enum Error {
SshKey(#[from] russh_keys::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid URI: {0}")]
InvalidUri(#[from] hyper::http::uri::InvalidUri),
#[error("SSH Authentication Failed")]
AuthenticationFailed,

#[error("Missing scheme in URI")]
InvalidScheme,
#[error("Missing SSH user name in URI")]
SshUserNameRequired,
#[error("Missing ssh key path")]
SshKeyPathRequired,
#[error("Missing SSH host in URI")]
SshHostRequired,
}
51 changes: 30 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
//! Provides an interface for querying the podman rest api. Most of the interface is generated from
//! the the official podman swagger file. This crate adds a layer to make it possible to connect to
//! podman over ssh. This is commonly necessary on macOs where the container runtime runs in a
//! virtual machine you connect to over ssh.
//! the podman rest api over ssh to a unix socket and directl to a unix socket. Connections over
//! ssh are commonly necessary on macOs where the container runtime runs in a virtual machine
//! accessible over ssh.
//!
//! ## Connecting via a unix socket on Linux
//!
//! ```no_run
//! # tokio_test::block_on(async {
//! use podman_rest_client::PodmanRestClient;
//!
//! let uri = "unix://path_to_unix_socket";
//!
//! let client = PodmanRestClient::new(uri, None).await.unwrap();
//!
//! let images = client.images_api().image_list_libpod(None,None).await.unwrap();
//! # })
//! ```
//! let client = PodmanRestCli
//! ```
//!
//! ## Connecting via ssh from macOS
//!
//! ```no_run
//! # tokio_test::block_on(async {
//! use podman_rest_client::PodmanRestClient;
//!
//! let uri = "ssh://[email protected]:63169/run/user/501/podman/podman.sock";
//! let key: Option<String> = Some("/path/to/key".into());
//!
//! let client = PodmanRestClient::new(uri, None).await.unwrap();
//!
//! let images = client.images_api().image_list_libpod(None,None).await.unwrap();
//! # })
//! ```

pub mod cli;
mod error;
mod podman_rest_client;
mod ssh;
mod unix_socket;

pub use error::Error;
pub use podman_rest_client::Config;
pub use podman_rest_client::PodmanRestClient;

/*
impl PodmanConnection {
fn into_config(&mut self) -> Result<Config, hyper::http::uri::InvalidUri> {
let uri = hyper::Uri::from_str(&self.uri)?;
let user_name = uri.authority().and_then(|authority| {
if let Some((user_name, _)) = authority.to_string().split_once('@') {
Some(user_name.to_string())
} else {
None
}
});
Ok(())
}
}*/
108 changes: 56 additions & 52 deletions src/podman_rest_client.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
use std::ops::Deref;
use std::str::FromStr;

use hyper_util::client::legacy::connect::Connect;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
use podman_api::apis::client::APIClient;
use podman_api::apis::configuration::Configuration as APIConfiguration;

use crate::error;
use crate::error::Error;
use crate::ssh;
use crate::unix_socket;

const BASE_PATH: &str = "http://d/v5.1.0";

pub struct PodmanRestClient(pub APIClient);

struct SshConfiguration {
pub user: String,
key: String,
address: String,
socket_path: String,
}
impl PodmanRestClient {
pub async fn new(uri: &str, key_path: Option<String>) -> Result<PodmanRestClient, Error> {
let uri = hyper::Uri::from_str(uri)?;

pub struct Config {
socket: Option<String>,
ssh: Option<SshConfiguration>,
}
if let Some(scheme) = uri.scheme() {
match scheme.as_str() {
"unix" => PodmanRestClient::new_unix(uri).await,
"ssh" => PodmanRestClient::new_ssh(uri, key_path).await,
_ => Err(Error::InvalidScheme),
}
} else {
Err(Error::InvalidScheme)
}
}

impl PodmanRestClient {
pub async fn new(config: Config) -> Result<PodmanRestClient, error::Error> {
let ssh_config = config.ssh.unwrap();
pub async fn new_ssh(
uri: hyper::Uri,
key_path: Option<String>,
) -> Result<PodmanRestClient, Error> {
let user_name = uri.authority().and_then(|authority| {
if let Some((user_name, _)) = authority.to_string().split_once('@') {
Some(user_name.to_string())
} else {
None
}
});

let user_name = user_name.ok_or(Error::SshUserNameRequired)?;
let key_path = key_path.ok_or(Error::SshKeyPathRequired)?;
let host = uri.host().ok_or(Error::SshHostRequired)?;
let address = uri.port().map_or(host.to_string(), |port| format!("{}:{}", host, port));

println!("{}, {}, {}", user_name, host, uri.path());

let connector = ssh::SshConnector::new(
&ssh_config.user,
&ssh_config.key,
&ssh_config.address,
&ssh_config.socket_path,
)
.await?;
let connector = ssh::SshConnector::new(&user_name, &key_path, &address, uri.path()).await?;

let client =
hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(connector);
PodmanRestClient::new_connector(connector).await
}

pub async fn new_unix(
uri: hyper::Uri,
) -> Result<PodmanRestClient, Error> {
let connector = unix_socket::UnixConnector::new(uri.to_string());

PodmanRestClient::new_connector(connector).await
}

async fn new_connector<C>(connector: C) -> Result<PodmanRestClient, Error>
where
C: Connect + Clone + Send + Sync + 'static,
{
let client = Client::builder(TokioExecutor::new()).build(connector);

let configuration = APIConfiguration {
base_path: "http://d/v5.1.0".to_string(),
base_path: BASE_PATH.to_string(),
..APIConfiguration::new(client)
};
let api_client = APIClient::new(configuration);
Expand All @@ -53,31 +85,3 @@ impl Deref for PodmanRestClient {
&self.0
}
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn it_works() {
let client = PodmanRestClient::new(Config {
socket: None,
ssh: Some(SshConfiguration {
user: "[email protected]:63169".into(),
key: "/Users/blazzy/.local/share/containers/podman/machine/machine".into(),
address: "127.0.0.1:63169".into(),
socket_path: "/run/user/501/podman/podman.sock".into(),
}),
})
.await
.unwrap();
let images = client
.images_api()
.image_list_libpod(None, None)
.await
.unwrap();
println!("{:?}", images);

assert_eq!(4, 4);
}
}
2 changes: 1 addition & 1 deletion src/ssh.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::Arc;
Expand All @@ -11,7 +12,6 @@ use russh::client::Config;
use russh::client::Msg;
use russh::ChannelStream;
use russh_keys::key;
use std::future::Future;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;

Expand Down
92 changes: 92 additions & 0 deletions src/unix_socket.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};

use hyper::rt::ReadBufCursor;
use hyper::Uri;
use hyper_util::client::legacy::connect::{Connected, Connection};
use tokio::io::AsyncRead;
use tokio::io::AsyncWrite;
use tokio::net::UnixStream;
use tower_service::Service;

use crate::error::Error;

#[derive(Clone)]
pub(crate) struct UnixConnector {
path: String,
}

pub struct UnixStreamWrapper(pub UnixStream);

impl UnixConnector {
pub fn new(path: String) -> UnixConnector {
UnixConnector { path }
}
}

impl Service<Uri> for UnixConnector {
type Response = UnixStreamWrapper;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}

fn call(&mut self, _req: Uri) -> Self::Future {
let path = self.path.clone();
let future = async move {
let stream = UnixStreamWrapper(UnixStream::connect(path).await?);

Ok(stream)
};
Box::pin(future)
}
}

impl Connection for UnixStreamWrapper {
fn connected(&self) -> Connected {
Connected::new()
}
}

impl hyper::rt::Read for UnixStreamWrapper {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: ReadBufCursor<'_>,
) -> Poll<std::io::Result<()>> {
let mut temp_buf = unsafe { tokio::io::ReadBuf::uninit(buf.as_mut()) };

match Pin::new(&mut self.get_mut().0).poll_read(cx, &mut temp_buf) {
Poll::Ready(Ok(())) => {
let n = temp_buf.filled().len();
unsafe {
buf.advance(n);
}
Poll::Ready(Ok(()))
}
other => other,
}
}
}

impl hyper::rt::Write for UnixStreamWrapper {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.get_mut().0).poll_write(cx, buf)
}

fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().0).poll_flush(cx)
}

fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().0).poll_shutdown(cx)
}
}

0 comments on commit 9d245a8

Please sign in to comment.