diff --git a/tests/go-e2e-statfs/go.mod b/tests/go-e2e-statfs/go.mod new file mode 100644 index 00000000000..c4f729fcd23 --- /dev/null +++ b/tests/go-e2e-statfs/go.mod @@ -0,0 +1,3 @@ +module dir_go + +go 1.19 diff --git a/tests/go-e2e-statfs/go.sum b/tests/go-e2e-statfs/go.sum new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/go-e2e-statfs/main.go b/tests/go-e2e-statfs/main.go new file mode 100644 index 00000000000..0ecd49c8181 --- /dev/null +++ b/tests/go-e2e-statfs/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "fmt" + _ "net" // for dynamic linking + "syscall" +) + +func main() { + rootPath := "/" + var statfs syscall.Statfs_t + err := syscall.Statfs(rootPath, &statfs) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Convert struct to a JSON-friendly format + data := map[string]interface{}{ + "bavail": statfs.Bavail, + "bfree": statfs.Bfree, + "blocks": statfs.Blocks, + "bsize": statfs.Bsize, + "ffree": statfs.Ffree, + "files": statfs.Files, + "flags": statfs.Flags, + "frsize": statfs.Frsize, + "fsid": []int64{int64(statfs.Fsid.X__val[0]), int64(statfs.Fsid.X__val[1])}, // Convert fsid to list + "namelen": statfs.Namelen, + "spare": statfs.Spare, + "type": statfs.Type, + } + + // Convert to JSON + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + fmt.Println("JSON Encoding Error:", err) + return + } + + // Print JSON + fmt.Println(string(jsonData)) +} diff --git a/tests/src/file_ops.rs b/tests/src/file_ops.rs index 4b67092b974..6bd56bd8209 100644 --- a/tests/src/file_ops.rs +++ b/tests/src/file_ops.rs @@ -4,9 +4,14 @@ mod file_ops_tests { use std::time::Duration; + use k8s_openapi::api::core::v1::Pod; + use kube::{api::LogParams, Api, Client}; use rstest::*; + use serde::Deserialize; - use crate::utils::{run_exec_with_target, service, FileOps, KubeService}; + use crate::utils::{ + go_statfs_service, kube_client, run_exec_with_target, service, FileOps, KubeService, + }; #[cfg_attr(not(any(feature = "ephemeral", feature = "job")), ignore)] #[cfg(target_os = "linux")] @@ -210,4 +215,93 @@ mod file_ops_tests { let res = process.wait().await; assert!(res.success()); } + + #[derive(Deserialize, Debug)] + struct Statfs { + bavail: u64, + bfree: u64, + blocks: u64, + bsize: u64, + ffree: u64, + files: u64, + flags: u64, + frsize: u64, + fsid: [u64; 2], + namelen: u64, + spare: [u64; 4], + r#type: u64, + } + + impl PartialEq for Statfs { + fn eq(&self, other: &Self) -> bool { + // bavail and bfree changes constantly, so they will usually not be the same in the two + // calls, so we just check they're kind of close. + self.bavail / 1024 == other.bavail / 1024 + && self.bfree / 1024 == other.bfree / 1024 + && self.blocks == other.blocks + && self.bsize == other.bsize + && self.ffree == other.ffree + && self.files == other.files + && self.flags == other.flags + && self.frsize == other.frsize + && self.fsid == other.fsid + && self.namelen == other.namelen + && self.spare == other.spare + && self.r#type == other.r#type + } + } + + /// Test that after going through all the conversions between the agent and the user program, + /// the statfs values are correct. + /// This is to prevent a regression to a bug we had where because of `statfs`/`statfs64` + /// struct conversions, we were returning an invalid struct to go when it called SYS_statfs. + #[cfg_attr(not(any(feature = "ephemeral", feature = "job")), ignore)] + #[cfg(target_os = "linux")] + #[rstest] + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + #[timeout(Duration::from_secs(240))] + pub async fn go_statfs( + #[future] go_statfs_service: KubeService, + #[future] kube_client: Client, + ) { + let app = FileOps::GoStatfs; + let service = go_statfs_service.await; + let client = kube_client.await; + let command = app.command(); + + let mut args = vec!["--fs-mode", "read"]; + + if cfg!(feature = "ephemeral") { + args.extend(["-e"].into_iter()); + } + + let mut process = run_exec_with_target( + command, + &service.pod_container_target(), + Some(&service.namespace), + Some(args), + None, + ) + .await; + let res = process.wait().await; + assert!(res.success()); + let mirrord_statfs_output = process.get_stdout().await; + let statfs_from_mirrord: Statfs = serde_json::from_str(&mirrord_statfs_output).unwrap(); + + let pod_api = Api::::namespaced(client, &service.namespace); + let statfs_from_pod: Statfs = loop { + let logs = pod_api + .logs(&service.pod_name, &LogParams::default()) + .await + .unwrap(); + println!("{}", logs); + if let Ok(statfs) = serde_json::from_str(&logs) { + break statfs; + } + // It's possible we didn't get all the logs yet, so the json is not valid. + // Wait a bit and fetch the logs again. + tokio::time::sleep(Duration::from_secs(3)).await; + }; + assert_eq!(statfs_from_mirrord, statfs_from_pod); + } } diff --git a/tests/src/utils.rs b/tests/src/utils.rs index 7fa4b7c68e7..ed984575ccf 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -120,6 +120,7 @@ pub enum FileOps { GoDir21, GoDir22, GoDir23, + GoStatfs, } #[derive(Debug)] @@ -527,6 +528,7 @@ impl FileOps { FileOps::GoDir21 => vec!["go-e2e-dir/21.go_test_app"], FileOps::GoDir22 => vec!["go-e2e-dir/22.go_test_app"], FileOps::GoDir23 => vec!["go-e2e-dir/23.go_test_app"], + FileOps::GoStatfs => vec!["go-e2e-dir/statfs.go_test_app"], } } @@ -1625,6 +1627,20 @@ pub async fn random_namespace_self_deleting_service(#[future] kube_client: Clien .await } +#[fixture] +pub async fn go_statfs_service(#[future] kube_client: Client) -> KubeService { + service( + "default", + "ClusterIP", + // "ghcr.io/metalbear-co/mirrord-node-udp-logger:latest", + "docker.io/t4lz/go-statfs:latest", + "go-statfs", + true, + kube_client, + ) + .await +} + pub fn resolve_node_host() -> String { if (cfg!(target_os = "linux") && !wsl::is_wsl()) || std::env::var("USE_MINIKUBE").is_ok() { let output = std::process::Command::new("minikube")