diff --git a/Cargo.toml b/Cargo.toml index a1fe9bb..8c709cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ tokio-util = "0.7" [dev-dependencies] env_logger = "^0.11" +k8s-openapi = { version = "0.22", features = ["v1_30"] } kube = { version = "0.92", features = ["client", "config", "runtime", "ws"] } pretty_assertions = "1" rand = "^0.8.4" diff --git a/src/client.rs b/src/kube_container_fs.rs similarity index 98% rename from src/client.rs rename to src/kube_container_fs.rs index 8321d3e..c9f9f49 100644 --- a/src/client.rs +++ b/src/kube_container_fs.rs @@ -1,6 +1,6 @@ -//! ## SCP +//! ## Kube Container FS //! -//! Scp remote fs implementation +//! The `KubeContainerFs` client is a client that allows you to interact with a container in a pod. use std::ops::Range; use std::path::{Path, PathBuf}; @@ -27,17 +27,17 @@ static LS_RE: Lazy = lazy_regex!( r#"^([\-ld])([\-rwxsStT]{9})\s+(\d+)\s+(.+)\s+(.+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"# ); -/// Kube "filesystem" client -pub struct KubeFs { - config: Option, - container: String, - pod_name: String, - pods: Option>, +/// Kube "filesystem" client to interact with a container in a pod +pub struct KubeContainerFs { + pub(crate) config: Option, + pub(crate) container: String, + pub(crate) pod_name: String, + pub(crate) pods: Option>, runtime: Arc, - wrkdir: PathBuf, + pub(crate) wrkdir: PathBuf, } -impl KubeFs { +impl KubeContainerFs { /// Creates a new `KubeFs` /// /// If `config()` is not called then, it will try to use the configuration from the default kubeconfig file @@ -321,7 +321,7 @@ impl KubeFs { } } -impl RemoteFs for KubeFs { +impl RemoteFs for KubeContainerFs { fn connect(&mut self) -> RemoteResult { debug!("Initializing Kube connection..."); let api = self.runtime.block_on(async { @@ -863,7 +863,7 @@ mod test { .build() .unwrap(), ); - let mut client = KubeFs::new("test", "test", &rt); + let mut client = KubeContainerFs::new("test", "test", &rt); assert!(client.config.is_none()); assert_eq!(client.is_connected(), false); } @@ -876,7 +876,7 @@ mod test { .build() .unwrap(), ); - let mut client = KubeFs::new("aaaaaa", "test", &rt); + let mut client = KubeContainerFs::new("aaaaaa", "test", &rt); assert!(client.connect().is_err()); } @@ -1490,7 +1490,7 @@ mod test { .build() .unwrap(), ); - let client = KubeFs::new("test", "test", &rt); + let client = KubeContainerFs::new("test", "test", &rt); assert_eq!( client.get_name_and_link("Cargo.toml"), (String::from("Cargo.toml"), None) @@ -1509,7 +1509,7 @@ mod test { .build() .unwrap(), ); - let client = KubeFs::new("test", "test", &rt); + let client = KubeContainerFs::new("test", "test", &rt); // File let entry = client .parse_ls_output( @@ -1550,7 +1550,7 @@ mod test { .build() .unwrap(), ); - let client = KubeFs::new("test", "test", &rt); + let client = KubeContainerFs::new("test", "test", &rt); // Directory let entry = client .parse_ls_output( @@ -1595,7 +1595,7 @@ mod test { .build() .unwrap(), ); - let client = KubeFs::new("test", "test", &rt); + let client = KubeContainerFs::new("test", "test", &rt); // File let entry = client .parse_ls_output( @@ -1623,7 +1623,7 @@ mod test { .build() .unwrap(), ); - let client = KubeFs::new("test", "test", &rt); + let client = KubeContainerFs::new("test", "test", &rt); assert!(client .parse_ls_output( Path::new("/tmp"), @@ -1660,7 +1660,7 @@ mod test { .build() .unwrap(), ); - let mut client = KubeFs::new("test", "test", &rt); + let mut client = KubeContainerFs::new("test", "test", &rt); assert!(client.change_dir(Path::new("/tmp")).is_err()); assert!(client .copy(Path::new("/nowhere"), PathBuf::from("/culonia").as_path()) @@ -1693,7 +1693,7 @@ mod test { // -- test utils #[cfg(feature = "integration-tests")] - fn setup_client() -> (Api, KubeFs) { + fn setup_client() -> (Api, KubeContainerFs) { // setup pod with random name use kube::api::PostParams; @@ -1784,7 +1784,7 @@ mod test { pods }); - let mut client = KubeFs::new(&pod_name, "alpine", &runtime).config(config.clone()); + let mut client = KubeContainerFs::new(&pod_name, "alpine", &runtime).config(config.clone()); client.connect().expect("connection failed"); // Create wrkdir let tempdir = PathBuf::from(generate_tempdir()); @@ -1799,7 +1799,7 @@ mod test { } #[cfg(feature = "integration-tests")] - fn finalize_client(pods: Api, mut client: KubeFs) { + fn finalize_client(pods: Api, mut client: KubeContainerFs) { // Get working directory use kube::api::DeleteParams; diff --git a/src/kube_multipod_fs.rs b/src/kube_multipod_fs.rs new file mode 100644 index 0000000..01c8cad --- /dev/null +++ b/src/kube_multipod_fs.rs @@ -0,0 +1,665 @@ +//! ## Kube MultiPod FS +//! +//! The `KubeMultiPodFs` client is a client that allows you to interact with multiple pods in a Kubernetes cluster. + +mod path; + +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use k8s_openapi::api::core::v1::Pod; +use kube::{Api, Client, Config}; +use remotefs::fs::{ + FileType, Metadata, ReadStream, RemoteError, RemoteErrorType, RemoteFs, RemoteResult, UnixPex, + Welcome, WriteStream, +}; +use remotefs::File; +use tokio::runtime::Runtime; + +use self::path::KubePath; +use crate::KubeContainerFs; + +/// Kube MultiPod FS +/// +/// The `KubeMultiPodFs` client is a client that allows you to interact with multiple pods in a Kubernetes cluster. +/// +/// Underneath it uses the `KubeContainerFs` client to interact with the pods, but it changes the current pod and +/// the container name under the hood, to simulate a multi-pod filesystem. +/// +/// Path are relative to the current pod and container and have the following format: +/// +/// /pod-name/container-name/path/to/file +pub struct KubeMultiPodFs { + kube: KubeContainerFs, + runtime: Arc, +} + +impl KubeMultiPodFs { + /// Create a new `KubeMultiPodFs` client + pub fn new(runtime: &Arc) -> Self { + Self { + kube: KubeContainerFs::new("", "", runtime), + runtime: runtime.clone(), + } + } + + /// Set configuration + pub fn config(mut self, config: Config) -> Self { + self.kube = self.kube.config(config); + self + } + + /// Get the current pod name + fn pod_name(&self) -> Option<&str> { + if self.kube.pod_name.is_empty() { + None + } else { + Some(&self.kube.pod_name) + } + } + + /// Returns the current container name + fn container_name(&self) -> Option<&str> { + if self.kube.container.is_empty() { + None + } else { + Some(&self.kube.container) + } + } + + /// Dispatch operations based on the path + /// + /// The `on_root` closure is called when the path is `/` + /// The `on_pod` closure is called when the path is `/pod-name` + /// The `on_container` closure is called when the path is `/pod-name/container-name` or `/pod-name/container-name/path/to/file` + /// + /// In any case, the current pod and container are set accordingly. + fn path_dispatch( + &mut self, + path: KubePath, + on_root: FR, + on_pod: FP, + on_container: FC, + on_path: FPP, + ) -> T + where + FR: FnOnce(&mut Self) -> T, + FP: FnOnce(&mut Self, &str) -> T, + FC: FnOnce(&mut Self, &str) -> T, + FPP: FnOnce(&mut Self, &Path) -> T, + { + if path.pod.is_none() { + return on_root(self); + } + if path.container.is_none() { + return on_pod(self, path.pod.as_deref().unwrap()); + } + + // temporary set pod and container + if let Some(p) = path.path { + let prev_pod = self.kube.pod_name.clone(); + let prev_container = self.kube.container.clone(); + self.kube.pod_name = path.pod.unwrap(); + self.kube.container = path.container.unwrap(); + let res = on_path(self, &p); + + // restore pod and container + self.kube.pod_name = prev_pod; + self.kube.container = prev_container; + + res + } else { + on_container(self, path.container.as_deref().unwrap()) + } + } + + /// List pods + fn list_pods(&self) -> RemoteResult> { + let api = self.kube.pods.as_ref().ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NotConnected, + "Not connected to a Kubernetes cluster", + ) + })?; + let pods = self + .runtime + .block_on(async { api.list(&Default::default()).await }) + .map_err(|err| RemoteError::new_ex(RemoteErrorType::ProtocolError, err))?; + + Ok(pods + .into_iter() + .map(|pod| File { + path: { + let mut p = PathBuf::from("/"); + p.push(pod.metadata.name.unwrap_or_default()); + p + }, + metadata: Metadata::default().file_type(FileType::Directory), + }) + .collect()) + } + + /// List containers + fn list_containers(&self, pod_name: &str) -> RemoteResult> { + let api = self.kube.pods.as_ref().ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NotConnected, + "Not connected to a Kubernetes cluster", + ) + })?; + let pod = self + .runtime + .block_on(async { api.get(pod_name).await }) + .map_err(|err| RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, err))?; + + let pod_spec = pod.spec.ok_or_else(|| { + RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, "Pod spec not found") + })?; + + Ok(pod_spec + .containers + .into_iter() + .map(|container| File { + path: { + let mut p = PathBuf::from("/"); + p.push(pod_name); + p.push(&container.name); + debug!("found container {} -> {}", container.name, p.display()); + + p + }, + metadata: Metadata::default().file_type(FileType::Directory), + }) + .collect()) + } + + /// Stat root + #[inline] + fn stat_root(&self) -> RemoteResult { + Ok(File { + path: PathBuf::from("/"), + metadata: Metadata::default().file_type(FileType::Directory), + }) + } + + /// Stat pod + fn stat_pod(&self, pod: &str) -> RemoteResult { + let pods = self.list_pods()?; + + pods.into_iter().find(|f| f.name() == pod).ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NoSuchFileOrDirectory, + format!("Pod {} not found", pod), + ) + }) + } + + /// Stat container + fn stat_container(&self, container: &str) -> RemoteResult { + let pod_name = self.pod_name().ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NoSuchFileOrDirectory, + "No pod to stat container", + ) + })?; + let containers = self.list_containers(pod_name)?; + + containers + .into_iter() + .find(|f| f.name() == container) + .ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NoSuchFileOrDirectory, + format!("Container {} not found", container), + ) + }) + } + + /// Check whether pod exists + fn exists_pod(&self, pod: &str) -> RemoteResult { + let api = self.kube.pods.as_ref().ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NotConnected, + "Not connected to a Kubernetes cluster", + ) + })?; + + Ok(self.runtime.block_on(async { api.get(pod).await.is_ok() })) + } + + /// Check whether container exists + fn exists_container(&self, container: &str) -> RemoteResult { + let pod_name = self.pod_name().ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NoSuchFileOrDirectory, + "No pod to check container existence", + ) + })?; + + let api = self.kube.pods.as_ref().ok_or_else(|| { + RemoteError::new_ex( + RemoteErrorType::NotConnected, + "Not connected to a Kubernetes cluster", + ) + })?; + + let pod = self + .runtime + .block_on(async { api.get(pod_name).await }) + .map_err(|err| RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, err))?; + + let pod_spec = pod.spec.ok_or_else(|| { + RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, "Pod spec not found") + })?; + + Ok(pod_spec.containers.iter().any(|c| c.name == container)) + } +} + +impl RemoteFs for KubeMultiPodFs { + fn connect(&mut self) -> RemoteResult { + debug!("Initializing Kube connection..."); + let api = self.runtime.block_on(async { + let client = match self.kube.config.as_ref() { + Some(config) => Client::try_from(config.clone()) + .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err)), + None => Client::try_default() + .await + .map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err)), + }?; + let api: Api = Api::default_namespaced(client); + + Ok(api) + })?; + + // Set pods + self.kube.pods = Some(api); + debug!("Getting working directory..."); + + Ok(Welcome::default()) + } + + fn disconnect(&mut self) -> RemoteResult<()> { + self.kube.disconnect() + } + + fn is_connected(&mut self) -> bool { + if self.pod_name().is_none() { + self.kube.pods.is_some() + } else { + self.kube.is_connected() + } + } + + fn pwd(&mut self) -> RemoteResult { + let mut p = PathBuf::from("/"); + + // compose path in format /pod-name/container-name/pwd + if let Some(pod_name) = self.pod_name() { + p.push(pod_name); + } else { + return Ok(p); + } + + if let Some(container_name) = self.container_name() { + p.push(container_name); + } else { + return Ok(p); + } + + p.push(self.kube.pwd()?); + + Ok(p) + } + + fn change_dir(&mut self, dir: &Path) -> RemoteResult { + let path = KubePath::from(dir); + + let prev_pod = self.pod_name().unwrap_or("").to_string(); + let prev_container = self.container_name().unwrap_or("").to_string(); + + if let Some(pod) = path.pod { + if self.exists_pod(&pod)? { + self.kube.pod_name = pod.to_string(); + } else { + return Err(RemoteError::new_ex( + RemoteErrorType::NoSuchFileOrDirectory, + format!("Pod {} does not exist", pod), + )); + } + } + if let Some(container) = path.container { + if self.exists_container(&container)? { + self.kube.container = container.to_string(); + } else { + // restore previous pod + self.kube.pod_name = prev_pod; + return Err(RemoteError::new_ex( + RemoteErrorType::NoSuchFileOrDirectory, + format!("Container {} does not exist", container), + )); + } + } + let res = if let Some(path) = path.path { + self.kube.change_dir(&path) + } else { + self.kube.change_dir(Path::new("/")) + }; + + // restore previous pod and container + if let Err(err) = res { + self.kube.pod_name = prev_pod; + self.kube.container = prev_container; + + return Err(err); + } + + self.pwd() + } + + fn list_dir(&mut self, path: &Path) -> RemoteResult> { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |fs| fs.list_pods(), + |fs, pod| fs.list_containers(pod), + |fs, _| fs.kube.list_dir(Path::new("/")), + |fs, path| fs.kube.list_dir(path), + ) + } + + fn stat(&mut self, path: &Path) -> RemoteResult { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |fs| fs.stat_root(), + |fs, pod| fs.stat_pod(pod), + |fs, container| fs.stat_container(container), + |fs, path| fs.kube.stat(path), + ) + } + + fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| Ok(()), + |_, _| Ok(()), + |_, _| Ok(()), + |fs, path| fs.kube.setstat(path, metadata), + ) + } + + fn exists(&mut self, path: &Path) -> RemoteResult { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| Ok(true), + |fs, pod| fs.exists_pod(pod), + |fs, container| fs.exists_container(container), + |fs, path| fs.kube.exists(path), + ) + } + + fn remove_file(&mut self, path: &Path) -> RemoteResult<()> { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |_, _| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |_, _| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |fs, path| fs.kube.remove_file(path), + ) + } + + fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |_, _| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |_, _| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |fs, path| fs.kube.remove_dir(path), + ) + } + + fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |_, _| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |_, _| Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)), + |fs, path| fs.kube.remove_dir_all(path), + ) + } + + fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |fs, path| fs.kube.create_dir(path, mode), + ) + } + + fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()> { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |fs, path| fs.kube.symlink(path, target), + ) + } + + fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> { + let path = KubePath::from(src); + + self.path_dispatch( + path, + |_| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |fs, path| fs.kube.copy(path, dest), + ) + } + + fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> { + let path = KubePath::from(src); + + self.path_dispatch( + path, + |_| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |fs, path| fs.kube.mov(path, dest), + ) + } + + fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)> { + if self.pod_name().is_none() || self.container_name().is_none() { + return Err(RemoteError::new_ex( + RemoteErrorType::ProtocolError, + "No pod or container to execute command on", + )); + } + + self.kube.exec(cmd) + } + + fn append(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult { + Err(RemoteError::new(RemoteErrorType::UnsupportedFeature)) + } + + fn create(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult { + Err(RemoteError::new(RemoteErrorType::UnsupportedFeature)) + } + + fn open(&mut self, _path: &Path) -> RemoteResult { + Err(RemoteError::new(RemoteErrorType::UnsupportedFeature)) + } + + fn create_file( + &mut self, + path: &Path, + metadata: &Metadata, + reader: Box, + ) -> RemoteResult { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |fs, path| fs.kube.create_file(path, metadata, reader), + ) + } + + fn append_file( + &mut self, + path: &Path, + metadata: &Metadata, + reader: Box, + ) -> RemoteResult { + let path = KubePath::from(path); + + self.path_dispatch( + path, + |_| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |fs, path| fs.kube.append_file(path, metadata, reader), + ) + } + + fn open_file(&mut self, src: &Path, dest: Box) -> RemoteResult { + let path = KubePath::from(src); + + self.path_dispatch( + path, + |_| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |_, _| { + Err(RemoteError::new_ex( + RemoteErrorType::CouldNotOpenFile, + "This operation requires a pod and a container", + )) + }, + |fs, path| fs.kube.open_file(path, dest), + ) + } +} diff --git a/src/kube_multipod_fs/path.rs b/src/kube_multipod_fs/path.rs new file mode 100644 index 0000000..bb4e335 --- /dev/null +++ b/src/kube_multipod_fs/path.rs @@ -0,0 +1,97 @@ +use std::path::{Path, PathBuf}; + +#[derive(Default, Clone)] +pub struct KubePath { + pub pod: Option, + pub container: Option, + pub path: Option, +} + +impl From<&Path> for KubePath { + fn from(path: &Path) -> Self { + let mut p = KubePath::default(); + + let mut parts = path.iter(); + if path.is_absolute() { + parts.next(); // skip the root + } + if let Some(pod) = parts.next() { + p.pod = Some(pod.to_string_lossy().trim_matches('/').to_string()); + } + if let Some(container) = parts.next() { + p.container = Some(container.to_string_lossy().trim_matches('/').to_string()); + } + let path = parts.collect::(); + if !path.as_os_str().is_empty() { + p.path = Some(path); + } + + p + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_from_path() { + let path = Path::new("/pod/container/path/to/file"); + let p = KubePath::from(path); + assert_eq!(p.pod, Some("pod".to_string())); + assert_eq!(p.container, Some("container".to_string())); + assert_eq!(p.path, Some(PathBuf::from("path/to/file"))); + + let path = Path::new("/pod/container"); + + let p = KubePath::from(path); + assert_eq!(p.pod, Some("pod".to_string())); + assert_eq!(p.container, Some("container".to_string())); + + let path = Path::new("/pod"); + + let p = KubePath::from(path); + assert_eq!(p.pod, Some("pod".to_string())); + assert!(p.container.is_none()); + assert!(p.path.is_none()); + + let path = Path::new("/"); + + let p = KubePath::from(path); + assert!(p.pod.is_none()); + assert!(p.container.is_none()); + assert!(p.path.is_none()); + } + + #[test] + fn test_relative_path() { + let path = Path::new("pod/container/path/to/file"); + let p = KubePath::from(path); + assert_eq!(p.pod, Some("pod".to_string())); + assert_eq!(p.container, Some("container".to_string())); + assert_eq!(p.path, Some(PathBuf::from("path/to/file"))); + + let path = Path::new("pod/container"); + + let p = KubePath::from(path); + assert_eq!(p.pod, Some("pod".to_string())); + assert_eq!(p.container, Some("container".to_string())); + + let path = Path::new("pod"); + + let p = KubePath::from(path); + assert_eq!(p.pod, Some("pod".to_string())); + assert!(p.container.is_none()); + assert!(p.path.is_none()); + + let path = Path::new(""); + + let p = KubePath::from(path); + assert!(p.pod.is_none()); + assert!(p.container.is_none()); + assert!(p.path.is_none()); + } +} diff --git a/src/lib.rs b/src/lib.rs index babc31f..fbd43ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,8 +29,10 @@ //! //! // import remotefs trait and client //! use remotefs::RemoteFs; -//! use remotefs_ssh::{SshConfigParseRule, SftpFs, SshOpts}; +//! use remotefs_kube::KubeContainerFs; //! use std::path::Path; +//! use std::sync::Arc; +//! use tokio::runtime::Runtime; //! //! let rt = Arc::new( //! tokio::runtime::Builder::new_current_thread() @@ -38,7 +40,7 @@ //! .build() //! .unwrap(), //! ); -//! let mut client: KubeFs = KubeFs::new("my-pod", &rt); +//! let mut client: KubeContainerFs = KubeContainerFs::new("my-pod", &rt); //! //! // connect //! assert!(client.connect().is_ok()); @@ -65,11 +67,13 @@ extern crate lazy_regex; #[macro_use] extern crate log; -mod client; +mod kube_container_fs; +mod kube_multipod_fs; mod utils; -pub use client::KubeFs; pub use kube::Config; +pub use kube_container_fs::KubeContainerFs; +pub use kube_multipod_fs::KubeMultiPodFs; // -- test logging #[cfg(test)]