From 362122f6f17253e0f8efbb877baf328abf224b89 Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Sun, 29 Sep 2024 19:54:16 +0200 Subject: [PATCH] feat: multi pod (#1) * feat: multi pod client * fix: tests and fixes * fix: version * fix: changelog --- CHANGELOG.md | 8 + Cargo.toml | 3 +- README.md | 67 +- src/{client.rs => kube_container_fs.rs} | 75 +- src/kube_multipod_fs.rs | 1559 +++++++++++++++++++++++ src/kube_multipod_fs/path.rs | 171 +++ src/lib.rs | 69 +- 7 files changed, 1891 insertions(+), 61 deletions(-) rename src/{client.rs => kube_container_fs.rs} (97%) create mode 100644 src/kube_multipod_fs.rs create mode 100644 src/kube_multipod_fs/path.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cb0b5..5190452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ # Changelog - [Changelog](#changelog) + - [0.3.0](#030) - [0.2.0](#020) - [0.1.0](#010) --- +## 0.3.0 + +Released on 29/09/2024 + +- Added `KubeMultiPodFs` to operate on multiple pod and containers at the same time. See docs for details. +- **BREAKING ‼️** Renamed `KubeFs` to `KubeContainerFs`. + ## 0.2.0 Released on 17/07/2024 diff --git a/Cargo.toml b/Cargo.toml index a1fe9bb..99d3405 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT" name = "remotefs-kube" readme = "README.md" repository = "https://github.com/veeso/remotefs-rs-kube" -version = "0.2.0" +version = "0.3.0" [dependencies] chrono = "^0.4" @@ -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/README.md b/README.md index 9edc3ab..c31bd98 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

~ Remotefs kube client ~

Developed by @veeso

-

Current version: 0.2.0 (17/07/2024)

+

Current version: 0.3.0 (29/09/2024)

= 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,37 +1799,22 @@ mod test { } #[cfg(feature = "integration-tests")] - fn finalize_client(pods: Api, mut client: KubeFs) { - // Get working directory - - use kube::api::DeleteParams; - use kube::ResourceExt as _; - let wrkdir = client.pwd().ok().unwrap(); - // Remove directory - assert!(client.remove_dir_all(wrkdir.as_path()).is_ok()); + fn finalize_client(_pods: Api, mut client: KubeContainerFs) { assert!(client.disconnect().is_ok()); - - // cleanup pods - let pod_name = client.pod_name; - client.runtime.block_on(async { - let dp = DeleteParams::default(); - pods.delete(&pod_name, &dp).await.unwrap().map_left(|pdel| { - info!("Deleting {pod_name} pod started: {:?}", pdel); - assert_eq!(pdel.name_any(), pod_name); - }); - }) } #[cfg(feature = "integration-tests")] fn generate_pod_name() -> String { - use rand::distributions::{Alphanumeric, DistString}; - use rand::thread_rng; - let random_string: String = Alphanumeric - .sample_string(&mut thread_rng(), 8) - .chars() + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng as _}; + + let mut rng = thread_rng(); + let random_string: String = std::iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) .filter(|c| c.is_alphabetic()) .map(|c| c.to_ascii_lowercase()) - .take(8) + .take(12) .collect(); format!("test-{}", random_string) diff --git a/src/kube_multipod_fs.rs b/src/kube_multipod_fs.rs new file mode 100644 index 0000000..12d52b3 --- /dev/null +++ b/src/kube_multipod_fs.rs @@ -0,0 +1,1559 @@ +//! ## 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 there is no pod, there is no container + if self.kube.pod_name.is_empty() { + return None; + } + if self.kube.container.is_empty() { + None + } else { + Some(&self.kube.container) + } + } + + /// Get the kube path from a path + fn kube_path(&self, path: &Path) -> KubePath { + KubePath::from_path(self.pod_name(), self.container_name(), path) + } + + /// 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()) + } + } + + /// Files coming from the container client has the absolute path relative to the container fs. + /// + /// The absolute path must be changed to `/pod-name/container-name/path/to/file` + fn fix_absolute_path(&self, mut f: File) -> File { + if self.pod_name().is_none() || self.container_name().is_none() { + return f; + } + + let mut p = PathBuf::from("/"); + p.push(self.pod_name().unwrap()); + p.push(self.container_name().unwrap()); + + let relative_path = f.path.strip_prefix("/").unwrap_or(f.path.as_path()); + p.push(relative_path); + + f.path = p; + f + } + + /// 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); + + 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); + } + + // push as relative + let pwd = self.kube.pwd()?; + let pwd_as_relative = pwd.strip_prefix("/").unwrap_or(pwd.as_path()); + p.push(pwd_as_relative); + + Ok(p) + } + + fn change_dir(&mut self, dir: &Path) -> RemoteResult { + let path = self.kube_path(dir); + debug!("Changing directory to {path}"); + + 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), + )); + } + } else { + self.kube.pod_name = "".to_string(); + } + + 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), + )); + } + } else { + self.kube.container = "".to_string(); + } + + let res = if let Some(path) = path.path { + self.kube.change_dir(&path) + } else { + self.kube.wrkdir = PathBuf::from("/"); + Ok(PathBuf::from("/")) + }; + + // 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 = self.kube_path(path); + + self.path_dispatch( + path, + |fs| fs.list_pods(), + |fs, pod| fs.list_containers(pod), + |fs, _| { + fs.kube + .list_dir(Path::new("/")) + .map(|files| files.into_iter().map(|f| fs.fix_absolute_path(f)).collect()) + }, + |fs, path| { + fs.kube + .list_dir(path) + .map(|files| files.into_iter().map(|f| fs.fix_absolute_path(f)).collect()) + }, + ) + } + + fn stat(&mut self, path: &Path) -> RemoteResult { + let path = self.kube_path(path); + + self.path_dispatch( + path, + |fs| fs.stat_root(), + |fs, pod| fs.stat_pod(pod), + |fs, container| { + fs.stat_container(container) + .map(|f| fs.fix_absolute_path(f)) + }, + |fs, path| fs.kube.stat(path).map(|f| fs.fix_absolute_path(f)), + ) + } + + fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> { + let path = self.kube_path(path); + + self.path_dispatch( + path, + |_| Ok(()), + |_, _| Ok(()), + |_, _| Ok(()), + |fs, path| fs.kube.setstat(path, metadata), + ) + } + + fn exists(&mut self, path: &Path) -> RemoteResult { + let path = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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 = self.kube_path(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), + ) + } +} + +#[cfg(test)] +mod test { + + #[cfg(feature = "integration-tests")] + use std::io::Cursor; + + #[cfg(feature = "integration-tests")] + use pretty_assertions::assert_eq; + + #[cfg(feature = "integration-tests")] + use super::*; + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_append_to_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + // Append to file + let file_data = "Hello, world!\n"; + let reader = Cursor::new(file_data.as_bytes()); + assert!(client + .append_file(p, &Metadata::default(), Box::new(reader)) + .is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_change_directory() { + crate::log_init(); + let (pods, mut client) = setup_client(); + let pwd = client.pwd().ok().unwrap(); + + let pod = client.pod_name().unwrap().to_string(); + let container = client.container_name().unwrap().to_string(); + + let mut p = PathBuf::from("/"); + p.push(&pod); + p.push(&container); + p.push("tmp"); + + assert!(client.change_dir(&p).is_ok()); + assert!(client.change_dir(pwd.as_path()).is_ok()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_change_directory_relative() { + crate::log_init(); + let (pods, mut client) = setup_client(); + assert!(client + .create_dir( + Path::new("should_change_directory_relative"), + UnixPex::from(0o755) + ) + .is_ok()); + assert!(client + .change_dir(Path::new("should_change_directory_relative/")) + .is_ok()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_change_directory() { + crate::log_init(); + let (pods, mut client) = setup_client(); + assert!(client + .change_dir(Path::new("/tmp/sdfghjuireghiuergh/useghiyuwegh")) + .is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_copy_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + assert!(client.copy(p, Path::new("b.txt")).is_ok()); + + assert!(client.stat(p).is_ok()); + assert!(client.stat(Path::new("b.txt")).is_ok()); + + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_copy_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + assert!(client.copy(p, Path::new("aaa/bbbb/ccc/b.txt")).is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_create_directory() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // create directory + assert!(client + .create_dir(Path::new("mydir"), UnixPex::from(0o755)) + .is_ok()); + let p = PathBuf::from(format!("{}/mydir", client.pwd().unwrap().display())); + assert!(client.exists(&p).unwrap()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_create_directory_cause_already_exists() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // create directory + assert!(client + .create_dir(Path::new("mydir"), UnixPex::from(0o755)) + .is_ok()); + assert_eq!( + client + .create_dir(Path::new("mydir"), UnixPex::from(0o755)) + .err() + .unwrap() + .kind, + RemoteErrorType::DirectoryAlreadyExists + ); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_create_directory() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // create directory + assert!(client + .create_dir( + Path::new("/tmp/werfgjwerughjwurih/iwerjghiwgui"), + UnixPex::from(0o755) + ) + .is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_create_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert_eq!( + client.create_file(p, &metadata, Box::new(reader)).unwrap(), + 10 + ); + // Verify size + assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_create_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("/tmp/ahsufhauiefhuiashf/hfhfhfhf"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_exec_command() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + assert_eq!( + client.exec("echo 5").ok().unwrap(), + (0, String::from("5\n")) + ); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_tell_whether_file_exists() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + // Verify size + assert_eq!(client.exists(p).ok().unwrap(), true); + assert_eq!(client.exists(Path::new("b.txt")).ok().unwrap(), false); + + assert_eq!(client.exists(Path::new("/tmp/ppppp")).ok().unwrap(), false); + + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_list_dir() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let wrkdir = client.pwd().ok().unwrap(); + debug!( + "Working directory: {}; pod {:?}; container {:?}", + wrkdir.display(), + client.pod_name(), + client.container_name() + ); + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + // Verify size + let file = client + .list_dir(wrkdir.as_path()) + .ok() + .unwrap() + .get(0) + .unwrap() + .clone(); + assert_eq!(file.name().as_str(), "a.txt"); + let mut expected_path = wrkdir; + expected_path.push(p); + assert_eq!(file.path.as_path(), expected_path.as_path()); + assert_eq!(file.extension().as_deref().unwrap(), "txt"); + assert_eq!(file.metadata.size, 10); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_list_dir() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + assert!(client.list_dir(Path::new("/tmp/auhhfh/hfhjfhf/")).is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_move_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + // Verify size + let dest = Path::new("b.txt"); + assert!(client.mov(p, dest).is_ok()); + assert_eq!(client.exists(p).ok().unwrap(), false); + assert_eq!(client.exists(dest).ok().unwrap(), true); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_move_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + // Verify size + let dest = Path::new("/tmp/wuefhiwuerfh/whjhh/b.txt"); + assert!(client.mov(p, dest).is_err()); + assert!(client + .mov(Path::new("/tmp/wuefhiwuerfh/whjhh/b.txt"), p) + .is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_open_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let metadata = Metadata::default().size(file_data.len() as u64); + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + // Verify size + let buffer: Box = Box::new(Vec::with_capacity(512)); + assert_eq!(client.open_file(p, buffer).ok().unwrap(), 10); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_open_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Verify size + let buffer: Box = Box::new(Vec::with_capacity(512)); + assert!(client + .open_file(Path::new("/tmp/aashafb/hhh"), buffer) + .is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_print_working_directory() { + crate::log_init(); + let (pods, mut client) = setup_client(); + assert!(client.pwd().is_ok()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_remove_dir_all() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create dir + let mut dir_path = client.pwd().ok().unwrap(); + dir_path.push(Path::new("test/")); + assert!(client + .create_dir(dir_path.as_path(), UnixPex::from(0o775)) + .is_ok()); + // Create file + let mut file_path = dir_path.clone(); + file_path.push(Path::new("a.txt")); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client + .create_file(file_path.as_path(), &metadata, Box::new(reader)) + .is_ok()); + // Remove dir + assert!(client.remove_dir_all(dir_path.as_path()).is_ok()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_remove_dir_all() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Remove dir + assert!(client + .remove_dir_all(Path::new("/tmp/aaaaaa/asuhi")) + .is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_remove_dir() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create dir + let mut dir_path = client.pwd().ok().unwrap(); + dir_path.push(Path::new("test/")); + assert!(client + .create_dir(dir_path.as_path(), UnixPex::from(0o775)) + .is_ok()); + assert!(client.remove_dir(dir_path.as_path()).is_ok()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_remove_dir() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create dir + let mut dir_path = client.pwd().ok().unwrap(); + dir_path.push(Path::new("test/")); + assert!(client + .create_dir(dir_path.as_path(), UnixPex::from(0o775)) + .is_ok()); + // Create file + let mut file_path = dir_path.clone(); + file_path.push(Path::new("a.txt")); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client + .create_file(file_path.as_path(), &metadata, Box::new(reader)) + .is_ok()); + // Remove dir + assert!(client.remove_dir(dir_path.as_path()).is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_remove_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.txt"); + let file_data = "test data\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + assert!(client.remove_file(p).is_ok()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_setstat_file() { + use std::time::SystemTime; + + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.sh"); + let file_data = "echo 5\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + + assert!(client + .setstat( + p, + Metadata { + accessed: Some(SystemTime::UNIX_EPOCH), + created: None, + file_type: FileType::File, + gid: Some(1000), + mode: Some(UnixPex::from(0o755)), + modified: Some(SystemTime::UNIX_EPOCH), + size: 7, + symlink: None, + uid: Some(1000), + } + ) + .is_ok()); + let entry = client.stat(p).ok().unwrap(); + let stat = entry.metadata(); + assert_eq!(stat.accessed, None); + assert_eq!(stat.created, None); + assert_eq!(stat.modified, Some(SystemTime::UNIX_EPOCH)); + assert_eq!(stat.mode.unwrap(), UnixPex::from(0o755)); + assert_eq!(stat.size, 7); + + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_setstat_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("bbbbb/cccc/a.sh"); + assert!(client + .setstat( + p, + Metadata { + accessed: None, + created: None, + file_type: FileType::File, + gid: Some(1), + mode: Some(UnixPex::from(0o755)), + modified: None, + size: 7, + symlink: None, + uid: Some(1), + } + ) + .is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_stat_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.sh"); + let file_data = "echo 5\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert_eq!( + client + .create_file(p, &metadata, Box::new(reader)) + .ok() + .unwrap(), + 7 + ); + let entry = client.stat(p).ok().unwrap(); + assert_eq!(entry.name(), "a.sh"); + let mut expected_path = client.pwd().ok().unwrap(); + expected_path.push("a.sh"); + assert_eq!(entry.path(), expected_path.as_path()); + let meta = entry.metadata(); + assert_eq!(meta.size, 7); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_stat_file() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.sh"); + assert!(client.stat(p).is_err()); + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_make_symlink() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.sh"); + let file_data = "echo 5\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + + let symlink = Path::new("b.sh"); + + assert!(client.symlink(symlink, p).is_ok()); + assert!(client.remove_file(symlink).is_ok()); + + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn should_not_make_symlink() { + crate::log_init(); + let (pods, mut client) = setup_client(); + // Create file + let p = Path::new("a.sh"); + let file_data = "echo 5\n"; + let reader = Cursor::new(file_data.as_bytes()); + let mut metadata = Metadata::default(); + metadata.size = file_data.len() as u64; + assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok()); + + let symlink = Path::new("b.sh"); + let file_data = "echo 5\n"; + let reader = Cursor::new(file_data.as_bytes()); + assert!(client + .create_file(symlink, &metadata, Box::new(reader)) + .is_ok()); + + assert!(client.symlink(symlink, p).is_err()); + assert!(client.remove_file(symlink).is_ok()); + assert!(client.symlink(symlink, Path::new("c.sh")).is_err()); + + finalize_client(pods, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn test_should_list_pods() { + let (api, mut client) = setup_client(); + + let files = client.list_dir(Path::new("/")).unwrap(); + assert!(files.len() >= 2); + + finalize_client(api, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn test_should_list_containers() { + let (api, mut client) = setup_client(); + + let pods = client.list_dir(Path::new("/")).unwrap(); + let pod_name = pods.get(0).unwrap().name(); + + let mut path = PathBuf::from("/"); + path.push(pod_name); + + let containers = client.list_dir(path.as_path()).unwrap(); + assert_eq!(containers.len(), 1); + assert_eq!(containers.get(0).unwrap().name(), "alpine"); + + finalize_client(api, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn test_should_enter_pod() { + let (api, mut client) = setup_client(); + + let pods = client.list_dir(Path::new("/")).unwrap(); + debug!("Pods: {pods:?}"); + let pod_name = pods.get(0).unwrap().name(); + debug!("Pod name: {pod_name}"); + + let mut path = PathBuf::from("/"); + path.push(pod_name); + debug!("Path: {path:?}"); + + assert!(client.change_dir(path.as_path()).is_ok()); + assert_eq!(client.pwd().unwrap().as_path(), path.as_path()); + + finalize_client(api, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn test_should_enter_container() { + let (api, mut client) = setup_client(); + + let pods = client.list_dir(Path::new("/")).unwrap(); + let pod_name = pods.get(0).unwrap().name(); + + let mut path = PathBuf::from("/"); + path.push(pod_name); + + let containers = client.list_dir(path.as_path()).unwrap(); + let container_name = containers.get(0).unwrap().name(); + + path.push(container_name); + + assert!(client.change_dir(path.as_path()).is_ok()); + assert_eq!(client.pwd().unwrap().as_path(), path.as_path()); + + finalize_client(api, client); + } + + #[test] + #[cfg(feature = "integration-tests")] + fn test_should_enter_root() { + let (api, mut client) = setup_client(); + + let path = PathBuf::from("/"); + + assert!(client.change_dir(path.as_path()).is_ok()); + assert_eq!(client.pwd().unwrap().as_path(), path.as_path()); + + finalize_client(api, client); + } + + #[cfg(feature = "integration-tests")] + fn setup_client() -> (Api, KubeMultiPodFs) { + crate::log_init(); + // setup pod with random name + + use kube::api::PostParams; + use kube::config::AuthInfo; + use kube::ResourceExt as _; + + let runtime = Arc::new( + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(), + ); + + let minikube_ip = std::env::var("MINIKUBE_IP").unwrap(); + + // setup pod + debug!("setting up pod"); + // config for minikube + let mut auth_info = AuthInfo::default(); + auth_info.username = Some("minikube".to_string()); + // get home + let home = std::env::var("HOME").unwrap(); + auth_info.client_certificate = + Some(format!("{home}/.minikube/profiles/minikube/client.crt")); + auth_info.client_key = Some(format!("{home}/.minikube/profiles/minikube/client.key")); + + debug!("Auth info: {auth_info:?}"); + + let config = Config { + cluster_url: format!("https://{minikube_ip}:8443").parse().unwrap(), + default_namespace: "default".to_string(), + read_timeout: None, + root_cert: None, + connect_timeout: None, + write_timeout: None, + accept_invalid_certs: true, + auth_info, + proxy_url: None, + tls_server_name: None, + }; + + let pod_names = (0..2) + .into_iter() + .map(|_| generate_pod_name()) + .collect::>(); + + // generate 2 pods + let pods = runtime.block_on(async { + let client = Client::try_from(config.clone()).unwrap(); + let pods: Api = Api::default_namespaced(client); + + for pod_name in &pod_names { + debug!("Pod name: {pod_name}"); + + let p: Pod = serde_json::from_value(serde_json::json!({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { "name": pod_name }, + "spec": { + "containers": [{ + "name": "alpine", + "image": "alpine" , + "command": ["tail", "-f", "/dev/null"], + }], + } + })) + .unwrap(); + + let pp = PostParams::default(); + match pods.create(&pp, &p).await { + Ok(o) => { + let name = o.name_any(); + assert_eq!(p.name_any(), name); + info!("Created {}", name); + } + Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance + Err(e) => panic!("failed to create: {e}"), // any other case is probably bad + } + + debug!("Pod created"); + + let establish = kube::runtime::wait::await_condition( + pods.clone(), + &pod_name, + kube::runtime::conditions::is_pod_running(), + ); + + info!("Waiting for pod to be running..."); + let _ = tokio::time::timeout(std::time::Duration::from_secs(30), establish) + .await + .expect("pod timeout"); + } + + pods + }); + + let mut client = KubeMultiPodFs::new(&runtime).config(config.clone()); + client.connect().expect("connection failed"); + + let mut tempdir = PathBuf::from("/"); + tempdir.push(&pod_names[0]); + tempdir.push("alpine"); + tempdir.push(generate_tempdir()); + println!("Tempdir: {}", tempdir.display()); + // Create wrkdir + client + .create_dir(tempdir.as_path(), UnixPex::from(0o775)) + .expect("failed to create tempdir"); + // Change directory + client + .change_dir(tempdir.as_path()) + .expect("failed to enter tempdir"); + (pods, client) + } + + #[cfg(feature = "integration-tests")] + fn finalize_client(_pods: Api, mut client: KubeMultiPodFs) { + assert!(client.disconnect().is_ok()); + } + + #[cfg(feature = "integration-tests")] + fn generate_pod_name() -> String { + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng as _}; + + let mut rng = thread_rng(); + let random_string: String = std::iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .filter(|c| c.is_alphabetic()) + .map(|c| c.to_ascii_lowercase()) + .take(12) + .collect(); + + format!("test-{}", random_string) + } + + #[cfg(feature = "integration-tests")] + fn generate_tempdir() -> String { + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng}; + let mut rng = thread_rng(); + let name: String = std::iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(8) + .collect(); + format!("tmp/temp_{}", name) + } +} diff --git a/src/kube_multipod_fs/path.rs b/src/kube_multipod_fs/path.rs new file mode 100644 index 0000000..c1d3558 --- /dev/null +++ b/src/kube_multipod_fs/path.rs @@ -0,0 +1,171 @@ +use std::fmt; +use std::path::{Path, PathBuf}; + +#[derive(Default, Clone)] +pub struct KubePath { + pub pod: Option, + pub container: Option, + pub path: Option, +} + +impl fmt::Display for KubePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut path = String::from("/"); + if let Some(pod) = &self.pod { + path.push_str(pod); + path.push('/'); + } + if let Some(container) = &self.container { + path.push_str(container); + path.push('/'); + } + if let Some(p) = &self.path { + path.push_str(p.to_string_lossy().as_ref()); + } + write!(f, "{}", path) + } +} + +impl KubePath { + /// Get Kube Path from a path, using the current pod and container. + pub fn from_path(pod: Option<&str>, container: Option<&str>, path: &Path) -> Self { + if path.is_absolute() { + Self::from_absolute_path(path) + } else { + Self::from_relative_path(pod, container, path) + } + } + + /// Get Kube Path from an absolute resource path. + /// + /// The syntax is `/pod/container/path/to/file` + fn from_absolute_path(path: &Path) -> Self { + let mut p = KubePath::default(); + + let mut parts = path.iter(); + 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()); + } + + // path must be absolute in this case + let mut path = PathBuf::from("/"); + for part in parts { + path.push(part); + } + // if the path is not empty, set it + if path != Path::new("/") { + p.path = Some(path); + } + p + } + + /// Get Kube Path from a relative path, using the current pod and container. + fn from_relative_path(pod: Option<&str>, container: Option<&str>, path: &Path) -> Self { + let mut p = KubePath::default(); + + if pod.is_none() && container.is_some() { + panic!("Cannot specify a container without a pod"); + } + + let mut parts = path.iter(); + if let Some(pod) = pod { + p.pod = Some(pod.to_string()); + } else if let Some(pod) = parts.next() { + p.pod = Some(pod.to_string_lossy().trim_matches('/').to_string()); + } + + if let Some(container) = container { + p.container = Some(container.to_string()); + } else if let Some(container) = parts.next() { + p.container = Some(container.to_string_lossy().trim_matches('/').to_string()); + } + + // if pod and container are not specified, the path must be trated as absolute + let path = if container.is_none() { + let mut p = PathBuf::from("/"); + for part in parts { + p.push(part); + } + + p + } else { + parts.collect::() + }; + // if the path is not empty, set it + if path != Path::new("/") { + p.path = Some(path); + } + + p + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_from_absolute_path() { + let path = Path::new("/pod/container/path/to/file"); + let p = KubePath::from_path(None, None, 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(None, None, 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(None, None, 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(None, None, 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("path/to/file"); + let p = KubePath::from_path(Some("pod"), Some("container"), 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("container/path/to/file"); + let p = KubePath::from_path(Some("pod"), None, 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/path/to/file"); + let p = KubePath::from_path(None, None, 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"))); + } + + #[test] + #[should_panic] + fn test_relative_path_panic() { + let path = Path::new("path/to/file"); + KubePath::from_path(None, Some("container"), path); + } +} diff --git a/src/lib.rs b/src/lib.rs index babc31f..ed55e3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ //! //! ```toml //! remotefs = "^0.2" -//! remotefs-kube = "^0.2" +//! remotefs-kube = "^0.3" //! ``` //! //! these features are supported: @@ -21,15 +21,63 @@ //! - `no-log`: disable logging. By default, this library will log via the `log` crate. //! //! -//! ### Kube client +//! ### Kube multi pod client //! -//! Here is a basic usage example, with the `Kube` client, which is very similiar to the `Scp` client. +//! The MultiPod client gives access to all the pods with their own containers in a namespace. +//! +//! This client creates an abstract file system with the following structure +//! +//! - / (root) +//! - pod-a +//! - container-a +//! - / (container-a root) +//! - /bin +//! - /home +//! - ... +//! - container-b +//! - / (container-b root) +//! - ... +//! - pod-b +//! - container-c +//! - / (container-c root) +//! - ... +//! +//! So paths have the following structure: `/pod-name/container-name/path/to/file`. +//! +//! ```rust,ignore +//! +//! // import remotefs trait and client +//! use remotefs::RemoteFs; +//! use remotefs_kube::KubeMultiPodFs; +//! use std::path::Path; +//! +//! let rt = Arc::new( +//! tokio::runtime::Builder::new_current_thread() +//! .enable_all() +//! .build() +//! .unwrap(), +//! ); +//! let mut client: KubeMultiPodFs = KubeMultiPodFs::new(&rt); +//! +//! // connect +//! assert!(client.connect().is_ok()); +//! // get working directory +//! println!("Wrkdir: {}", client.pwd().ok().unwrap().display()); +//! // change working directory +//! assert!(client.change_dir(Path::new("/my-pod/alpine/tmp")).is_ok()); +//! // disconnect +//! assert!(client.disconnect().is_ok()); +//! ``` +//! +//! ### Kube container client +//! +//! Here is a basic usage example, with the `KubeContainerFs` client, which is used to connect and interact with a single container on a certain pod. This client gives the entire access to the container file system. //! //! ```rust,ignore //! //! // import remotefs trait and client //! use remotefs::RemoteFs; -//! use remotefs_ssh::{SshConfigParseRule, SftpFs, SshOpts}; +//! use remotefs_kube::KubeContainerFs; //! use std::path::Path; //! //! let rt = Arc::new( @@ -38,7 +86,7 @@ //! .build() //! .unwrap(), //! ); -//! let mut client: KubeFs = KubeFs::new("my-pod", &rt); +//! let mut client: KubeContainerFs = KubeContainerFs::new("my-pod", "container-name", &rt); //! //! // connect //! assert!(client.connect().is_ok()); @@ -65,14 +113,19 @@ 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)] pub fn log_init() { - let _ = env_logger::builder().is_test(true).try_init(); + let _ = env_logger::builder() + .is_test(true) + .filter_module("remotefs_kube", log::LevelFilter::Debug) + .try_init(); }