diff --git a/CHANGELOG.md b/CHANGELOG.md index e95f9fc558b..00d859c291b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,12 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +### Added + +- Added support for overriding a process' environment variables by setting `MIRRORD_OVERRIDE_ENV_VARS` to `true`. To filter out undesired variables, use the `MIRRORD_OVERRIDE_FILTER_ENV_VARS` configuration with arguments such as `FOO;BAR`. + ### Changed + - Removed `unwrap` from the `Future` that was waiting for Kube pod to spin up in `pod_api.rs`. (Fixes #110) - Speed up agent container image building by using a more specific base image. - CI: Remove building agent before building & running tests (duplicate) @@ -23,17 +28,22 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how - Enable the blocking feature of the `reqwest` library ## 2.2.1 + ### Changed + - Compile universal binaries for MacOS. (Fixes #131) - E2E small improvements, removing sleeps. (Fixes #99) ## 2.2.0 + ### Added + - File operations are now available behind the `MIRRORD_FILE_OPS` env variable, this means that mirrord now hooks into the following file functions: `open`, `fopen`, `fdopen`, `openat`, `read`, `fread`, `fileno`, `lseek`, and `write` to provide a mirrored file system. -- Support for running x64 (Intel) binary on arm (Silicon) macOS using mirrord. This will download and use the x64 mirrord-layer binary when needed. +- Support for running x64 (Intel) binary on arm (Silicon) macOS using mirrord. This will download and use the x64 mirrord-layer binary when needed. - Add detours for fcntl/dup system calls, closes [#51](https://github.com/metalbear-co/mirrord/issues/51) ### Changed + - Add graceful exit for library extraction logic in case of error. - Refactor the CI by splitting the building of mirrord-agent in a separate job and caching the agent image for E2E tests. - Update bug report template to apply to the latest version of mirrord. @@ -47,6 +57,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how - Fix typos ## 2.1.0 + ### Added - Prompt user to update if their version is outdated in the VS Code extension or CLI. @@ -54,6 +65,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how - Add a keep-alive to keep the agent-pod from exiting, closes [#63](https://github.com/metalbear-co/mirrord/issues/63) ## 2.0.4 + Complete refactor and re-write of everything. - The CLI/VSCode extension now use `mirrord-layer` which loads into debugged process using `LD_PRELOAD`/`DYLD_INSERT_LIBRARIES`. diff --git a/mirrord-agent/src/main.rs b/mirrord-agent/src/main.rs index 06a5ae7a969..0454411fb8c 100644 --- a/mirrord-agent/src/main.rs +++ b/mirrord-agent/src/main.rs @@ -1,23 +1,26 @@ #![feature(result_option_inspect)] -#![feature(never_type)] +#![feature(hash_drain_filter)] use std::{ borrow::Borrow, - collections::HashSet, + collections::{HashMap, HashSet}, hash::{Hash, Hasher}, net::{Ipv4Addr, SocketAddrV4}, + path::PathBuf, }; use error::AgentError; use futures::SinkExt; use mirrord_protocol::{ tcp::{DaemonTcp, LayerTcp}, - ClientMessage, ConnectionID, DaemonCodec, DaemonMessage, FileRequest, FileResponse, Port, + ClientMessage, ConnectionID, DaemonCodec, DaemonMessage, FileError, FileRequest, FileResponse, + GetEnvVarsRequest, Port, ResponseError, }; use tokio::{ + io::AsyncReadExt, net::{TcpListener, TcpStream}, select, - sync::mpsc::{self}, + sync::mpsc, }; use tokio_stream::StreamExt; use tracing::{debug, error, info, trace}; @@ -34,7 +37,7 @@ use cli::parse_args; use sniffer::{packet_worker, SnifferCommand, SnifferOutput}; use util::{IndexAllocator, Subscriptions}; -use crate::file::file_worker; +use crate::{file::file_worker, runtime::get_container_pid, sniffer::DEFAULT_RUNTIME}; type PeerID = u32; @@ -105,6 +108,86 @@ pub struct PeerMessage { peer_id: PeerID, } +/// Helper function that loads the process' environment variables, and selects only those that were +/// requested from `mirrord-layer` (ignores vars specified in `filter_env_vars`). +/// +/// Returns an error if none of the requested environment variables were found. +async fn select_env_vars( + environ_path: PathBuf, + filter_env_vars: HashSet, + select_env_vars: HashSet, +) -> Result, ResponseError> { + debug!( + "select_env_vars -> environ_path {:#?} filter_env_vars {:#?} select_env_vars {:#?}", + environ_path, filter_env_vars, select_env_vars + ); + + let mut environ_file = tokio::fs::File::open(environ_path).await.map_err(|fail| { + ResponseError::FileOperation(FileError { + operation: "open".to_string(), + raw_os_error: fail.raw_os_error(), + kind: fail.kind().into(), + }) + })?; + + let mut raw_env_vars = String::with_capacity(8192); + + // TODO: nginx doesn't play nice when we do this, it only returns a string that goes like + // "nginx -g daemon off;". + let read_amount = environ_file + .read_to_string(&mut raw_env_vars) + .await + .map_err(|fail| { + ResponseError::FileOperation(FileError { + operation: "read_to_string".to_string(), + raw_os_error: fail.raw_os_error(), + kind: fail.kind().into(), + }) + })?; + debug!( + "select_env_vars -> read {:#?} bytes with pure ENV_VARS {:#?}", + read_amount, raw_env_vars + ); + + // TODO: These are env vars that should usually be ignored. Revisit this list if a user + // ever asks for a way to NOT filter out these. + let mut default_filter = HashSet::with_capacity(2); + default_filter.insert("PATH".to_string()); + default_filter.insert("HOME".to_string()); + + let env_vars = raw_env_vars + // "DB=foo.db\0PORT=99\0HOST=\0PATH=/fake\0" + .split_terminator(char::from(0)) + // ["DB=foo.db", "PORT=99", "HOST=", "PATH=/fake"] + .map(|key_and_value| key_and_value.split_terminator('=').collect::>()) + // [["DB", "foo.db"], ["PORT", "99"], ["HOST"], ["PATH", "/fake"]] + .filter_map( + |mut keys_and_values| match (keys_and_values.pop(), keys_and_values.pop()) { + (Some(value), Some(key)) => Some((key.to_string(), value.to_string())), + _ => None, + }, + ) + .filter(|(key, _)| !default_filter.contains(key)) + // [("DB", "foo.db"), ("PORT", "99"), ("PATH", "/fake")] + .filter(|(key, _)| !filter_env_vars.contains(key)) + // [("DB", "foo.db"), ("PORT", "99")] + .filter(|(key, _)| { + select_env_vars.is_empty() + || select_env_vars.contains("*") + || select_env_vars.contains(key) + }) + // [("DB", "foo.db")] + .collect::>(); + + debug!("select_env_vars -> selected env vars found {:?}", env_vars); + + if env_vars.is_empty() { + Err(ResponseError::NotFound) + } else { + Ok(env_vars) + } +} + async fn handle_peer_messages( // TODO: Possibly refactor `state` out to be more "independent", and live in its own worker // thread. @@ -112,6 +195,8 @@ async fn handle_peer_messages( sniffer_command_tx: mpsc::Sender, file_request_tx: mpsc::Sender<(PeerID, FileRequest)>, peer_message: PeerMessage, + container_id: &Option, + container_runtime: &Option, ) -> Result<(), AgentError> { match peer_message.client_message { ClientMessage::Tcp(LayerTcp::PortUnsubscribe(port)) => { @@ -170,6 +255,37 @@ async fn handle_peer_messages( .send((peer_message.peer_id, file_request)) .await?; } + ClientMessage::GetEnvVarsRequest(GetEnvVarsRequest { + env_vars_filter, + env_vars_select, + }) => { + debug!( + "ClientMessage::GetEnvVarsRequest peer id {:?} filter {:?} select {:?}", + peer_message.peer_id, env_vars_filter, env_vars_select + ); + + let container_runtime = container_runtime + .as_ref() + .map(String::as_str) + .unwrap_or(DEFAULT_RUNTIME); + + let pid = match container_id { + Some(container_id) => get_container_pid(container_id, container_runtime).await, + None => Err(AgentError::NotFound(format!( + "handle_peer_messages -> Container ID not specified {:#?} for runtime {:#?}!", + container_id, container_runtime + ))), + }?; + + let environ_path = PathBuf::from("/proc").join(pid.to_string()).join("environ"); + let env_vars_result = + select_env_vars(environ_path, env_vars_filter, env_vars_select).await; + + let peer = state.peers.get(&peer_message.peer_id).unwrap(); + peer.channel + .send(DaemonMessage::GetEnvVarsResponse(env_vars_result)) + .await?; + } } Ok(()) @@ -202,7 +318,7 @@ async fn peer_handler( } }, message = daemon_messages_rx.recv() => { - debug!("peer_handler -> daemon_messages_rx.recv received a message {:?}", message); + debug!("peer_handler -> daemon_messages_rx.recv received a message"); match message { Some(message) => { @@ -284,7 +400,13 @@ async fn start_agent() -> Result<(), AgentError> { }, Some(peer_message) = peer_messages_rx.recv() => { - handle_peer_messages(&mut state, sniffer_command_tx.clone(), file_request_tx.clone(), peer_message).await?; + handle_peer_messages(&mut state, + sniffer_command_tx.clone(), + file_request_tx.clone(), + peer_message, + &args.container_id, + &args.container_runtime, + ).await?; }, Some((peer_id, file_response)) = file_response_rx.recv() => { diff --git a/mirrord-cli/src/config.rs b/mirrord-cli/src/config.rs new file mode 100644 index 00000000000..96c436793fe --- /dev/null +++ b/mirrord-cli/src/config.rs @@ -0,0 +1,68 @@ +use clap::{Args, Parser, Subcommand}; + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +pub(super) struct Cli { + #[clap(subcommand)] + pub(super) commands: Commands, +} + +#[derive(Subcommand)] +pub(super) enum Commands { + Exec(ExecArgs), + Extract { + #[clap(value_parser)] + path: String, + }, +} + +#[derive(Args, Debug)] +pub(super) struct ExecArgs { + /// Pod name to mirror. + #[clap(short, long, value_parser)] + pub pod_name: String, + + /// Namespace of the pod to mirror. Defaults to "default". + #[clap(short = 'n', long, value_parser)] + pub pod_namespace: Option, + + /// Namespace to place agent in. + #[clap(short = 'a', long, value_parser)] + pub agent_namespace: Option, + + /// Agent log level + #[clap(short = 'l', long, value_parser)] + pub agent_log_level: Option, + + /// Agent image + #[clap(short = 'i', long, value_parser)] + pub agent_image: Option, + + /// Enable file hooking + #[clap(short = 'f', long, value_parser)] + pub enable_fs: bool, + + /// The env vars to filter out + #[clap(short = 'x', long, value_parser)] + pub override_env_vars_exclude: Option, + + /// The env vars to select + #[clap(short = 's', long, value_parser)] + pub override_env_vars_include: Option, + + /// Binary to execute and mirror traffic into. + #[clap(value_parser)] + pub binary: String, + + /// Agent TTL + #[clap(long, value_parser)] + pub agent_ttl: Option, + + /// Accept/reject invalid certificates. + #[clap(short = 'c', long, value_parser)] + pub accept_invalid_certificates: bool, + + /// Arguments to pass to the binary. + #[clap(value_parser)] + pub(super) binary_args: Vec, +} diff --git a/mirrord-cli/src/main.rs b/mirrord-cli/src/main.rs index 9a28ff49f6a..c55011dfc4d 100644 --- a/mirrord-cli/src/main.rs +++ b/mirrord-cli/src/main.rs @@ -1,69 +1,14 @@ use std::{fs::File, io::Write, path::PathBuf, time::Duration}; use anyhow::{anyhow, Context, Result}; -use clap::{Args, Parser, Subcommand}; +use clap::Parser; +use config::*; use exec::execvp; use semver::Version; use tracing::{debug, error, info}; use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter}; -#[derive(Parser)] -#[clap(author, version, about, long_about = None)] -struct Cli { - #[clap(subcommand)] - commands: Commands, -} - -#[derive(Subcommand)] -enum Commands { - Exec(ExecArgs), - Extract { - #[clap(value_parser)] - path: String, - }, -} - -#[derive(Args, Debug)] -struct ExecArgs { - /// Pod name to mirror. - #[clap(short, long, value_parser)] - pub pod_name: String, - - /// Namespace of the pod to mirror. Defaults to "default". - #[clap(short = 'n', long, value_parser)] - pub pod_namespace: Option, - - /// Namespace to place agent in. - #[clap(short = 'a', long, value_parser)] - pub agent_namespace: Option, - - /// Agent log level - #[clap(short = 'l', long, value_parser)] - pub agent_log_level: Option, - - /// Agent image - #[clap(short = 'i', long, value_parser)] - pub agent_image: Option, - - /// Enable file hooking - #[clap(short = 'f', long, value_parser)] - pub enable_fs: bool, - - /// Binary to execute and mirror traffic into. - #[clap(value_parser)] - pub binary: String, - - /// Agent TTL - #[clap(long, value_parser)] - pub agent_ttl: Option, - - /// Accept/reject invalid certificates. - #[clap(short = 'c', long, value_parser)] - pub accept_invalid_certificates: bool, - /// Arguments to pass to the binary. - #[clap(value_parser)] - binary_args: Vec, -} +mod config; #[cfg(target_os = "linux")] const INJECTION_ENV_VAR: &str = "LD_PRELOAD"; @@ -160,9 +105,24 @@ fn exec(args: &ExecArgs) -> Result<()> { std::env::set_var("MIRRORD_FILE_OPS", true.to_string()); } + if let Some(override_env_vars_exclude) = &args.override_env_vars_exclude { + std::env::set_var( + "MIRRORD_OVERRIDE_ENV_VARS_EXCLUDE", + override_env_vars_exclude, + ); + } + + if let Some(override_env_vars_include) = &args.override_env_vars_include { + std::env::set_var( + "MIRRORD_OVERRIDE_ENV_VARS_INCLUDE", + override_env_vars_include, + ); + } + if args.accept_invalid_certificates { std::env::set_var("MIRRORD_ACCEPT_INVALID_CERTIFICATES", "true"); } + let library_path = extract_library(None)?; add_to_preload(library_path.to_str().unwrap()).unwrap(); diff --git a/mirrord-layer/src/config.rs b/mirrord-layer/src/config.rs index c36208ebf28..65489276095 100644 --- a/mirrord-layer/src/config.rs +++ b/mirrord-layer/src/config.rs @@ -1,7 +1,7 @@ use envconfig::Envconfig; -#[derive(Envconfig)] -pub struct Config { +#[derive(Envconfig, Clone)] +pub struct LayerConfig { #[envconfig(from = "MIRRORD_AGENT_RUST_LOG", default = "info")] pub agent_rust_log: String, @@ -11,6 +11,9 @@ pub struct Config { #[envconfig(from = "MIRRORD_AGENT_IMAGE")] pub agent_image: Option, + #[envconfig(from = "MIRRORD_AGENT_IMAGE_PULL_POLICY", default = "IfNotPresent")] + pub image_pull_policy: String, + #[envconfig(from = "MIRRORD_AGENT_IMPERSONATED_POD_NAME")] pub impersonated_pod_name: String, @@ -25,4 +28,12 @@ pub struct Config { #[envconfig(from = "MIRRORD_FILE_OPS", default = "false")] pub enabled_file_ops: bool, + + /// Filters out these env vars when overriding is enabled. + #[envconfig(from = "MIRRORD_OVERRIDE_ENV_VARS_EXCLUDE", default = "")] + pub override_env_vars_exclude: String, + + /// Selects these env vars when overriding is enabled. + #[envconfig(from = "MIRRORD_OVERRIDE_ENV_VARS_INCLUDE", default = "")] + pub override_env_vars_include: String, } diff --git a/mirrord-layer/src/lib.rs b/mirrord-layer/src/lib.rs index a1fa40f911b..4fbe3f38662 100644 --- a/mirrord-layer/src/lib.rs +++ b/mirrord-layer/src/lib.rs @@ -2,11 +2,9 @@ #![feature(once_cell)] #![feature(result_option_inspect)] #![feature(const_trait_impl)] -#![feature(type_alias_impl_trait)] -#![feature(generic_associated_types)] use std::{ - env, + collections::HashSet, sync::{LazyLock, Mutex, OnceLock}, }; @@ -22,9 +20,10 @@ use futures::{SinkExt, StreamExt}; use kube::api::Portforwarder; use libc::c_int; use mirrord_protocol::{ - ClientCodec, ClientMessage, CloseFileRequest, CloseFileResponse, DaemonMessage, FileRequest, - FileResponse, OpenFileRequest, OpenFileResponse, OpenRelativeFileRequest, ReadFileRequest, - ReadFileResponse, SeekFileRequest, SeekFileResponse, WriteFileRequest, WriteFileResponse, + ClientCodec, ClientMessage, CloseFileRequest, CloseFileResponse, DaemonMessage, EnvVars, + FileRequest, FileResponse, GetEnvVarsRequest, OpenFileRequest, OpenFileResponse, + OpenRelativeFileRequest, ReadFileRequest, ReadFileResponse, SeekFileRequest, SeekFileResponse, + WriteFileRequest, WriteFileResponse, }; use sockets::SOCKETS; use tcp::TcpHandler; @@ -51,12 +50,13 @@ mod sockets; mod tcp; mod tcp_mirror; -use crate::{common::HookMessage, config::Config, macros::hook}; +use crate::{common::HookMessage, config::LayerConfig, macros::hook}; static RUNTIME: LazyLock = LazyLock::new(|| Runtime::new().unwrap()); static GUM: LazyLock = LazyLock::new(|| unsafe { Gum::obtain() }); pub static mut HOOK_SENDER: Option> = None; + pub static ENABLED_FILE_OPS: OnceLock = OnceLock::new(); #[ctor] @@ -68,18 +68,10 @@ fn init() { info!("Initializing mirrord-layer!"); - let config = Config::init_from_env().unwrap(); + let config = LayerConfig::init_from_env().unwrap(); let port_forwarder = RUNTIME - .block_on(pod_api::create_agent( - &config.impersonated_pod_name, - &config.impersonated_pod_namespace, - &config.agent_namespace, - config.agent_rust_log, - config.agent_image.unwrap_or_else(|| { - concat!("ghcr.io/metalbear-co/mirrord:", env!("CARGO_PKG_VERSION")).to_string() - }), - )) + .block_on(pod_api::create_agent(config.clone())) .unwrap(); let (sender, receiver) = channel::(1000); @@ -90,7 +82,7 @@ fn init() { let enabled_file_ops = ENABLED_FILE_OPS.get_or_init(|| config.enabled_file_ops); enable_hooks(*enabled_file_ops); - RUNTIME.spawn(poll_agent(port_forwarder, receiver)); + RUNTIME.spawn(poll_agent(port_forwarder, receiver, config)); } #[allow(clippy::too_many_arguments)] @@ -341,12 +333,37 @@ async fn handle_daemon_message( panic!("Daemon: unmatched pong!"); } } + DaemonMessage::GetEnvVarsResponse(remote_env_vars) => { + debug!("DaemonMessage::GetEnvVarsResponse {:#?}!", remote_env_vars); + + match remote_env_vars { + Ok(remote_env_vars) => { + for (key, value) in remote_env_vars.into_iter() { + debug!( + "DaemonMessage::GetEnvVarsResponse set key {:#?} value {:#?}", + key, value + ); + + std::env::set_var(&key, &value); + debug_assert_eq!(std::env::var(key), Ok(value)); + } + } + Err(fail) => error!( + "Loading remote environment variables failed with {:#?}", + fail + ), + } + } DaemonMessage::Close => todo!(), DaemonMessage::LogMessage(_) => todo!(), } } -async fn poll_agent(mut pf: Portforwarder, mut receiver: Receiver) { +async fn poll_agent( + mut pf: Portforwarder, + mut receiver: Receiver, + config: LayerConfig, +) { let port = pf.take_stream(61337).unwrap(); // TODO: Make port configurable // `codec` is used to retrieve messages from the daemon (messages that are sent from -agent to @@ -369,6 +386,36 @@ async fn poll_agent(mut pf: Portforwarder, mut receiver: Receiver) let mut ping = false; + if !config.override_env_vars_exclude.is_empty() && !config.override_env_vars_include.is_empty() + { + panic!( + r#"mirrord-layer encountered an issue: + + mirrord doesn't support specifying both + `OVERRIDE_ENV_VARS_EXCLUDE` and `OVERRIDE_ENV_VARS_INCLUDE` at the same time! + + > Use either `--override_env_vars_exclude` or `--override_env_vars_include`. + >> If you want to include all, use `--override_env_vars_include="*"`."# + ); + } else { + let env_vars_filter = HashSet::from(EnvVars(config.override_env_vars_exclude)); + let env_vars_select = HashSet::from(EnvVars(config.override_env_vars_include)); + + if !env_vars_filter.is_empty() || !env_vars_select.is_empty() { + let codec_result = codec + .send(ClientMessage::GetEnvVarsRequest(GetEnvVarsRequest { + env_vars_filter, + env_vars_select, + })) + .await; + + debug!( + "ClientMessage::GetEnvVarsRequest codec_result {:#?}", + codec_result + ); + } + } + let mut tcp_mirror_handler = TcpMirrorHandler::default(); loop { diff --git a/mirrord-layer/src/pod_api.rs b/mirrord-layer/src/pod_api.rs index ddb940a6fbb..9758e01fa15 100644 --- a/mirrord-layer/src/pod_api.rs +++ b/mirrord-layer/src/pod_api.rs @@ -12,7 +12,8 @@ use rand::distributions::{Alphanumeric, DistString}; use serde_json::json; use tracing::{error, warn}; -use crate::config; +use crate::config::LayerConfig; + struct RuntimeData { container_id: String, container_runtime: String, @@ -52,14 +53,19 @@ impl RuntimeData { } } -pub async fn create_agent( - pod_name: &str, - pod_namespace: &str, - agent_namespace: &str, - log_level: String, - agent_image: String, -) -> Result { - let env_config = config::Config::init_from_env().unwrap(); +pub async fn create_agent(config: LayerConfig) -> Result { + let LayerConfig { + agent_rust_log, + agent_namespace, + agent_image, + image_pull_policy, + impersonated_pod_name, + impersonated_pod_namespace, + .. + } = config; + + let env_config = LayerConfig::init_from_env().unwrap(); + let client = if env_config.accept_invalid_certificates { let mut config = Config::infer().await.unwrap(); config.accept_invalid_certs = true; @@ -68,7 +74,14 @@ pub async fn create_agent( } else { Client::try_default().await.unwrap() }; - let runtime_data = RuntimeData::from_k8s(client.clone(), pod_name, pod_namespace).await; + + let runtime_data = RuntimeData::from_k8s( + client.clone(), + &impersonated_pod_name, + &impersonated_pod_namespace, + ) + .await; + let agent_job_name = format!( "mirrord-agent-{}", Alphanumeric @@ -76,6 +89,10 @@ pub async fn create_agent( .to_lowercase() ); + let agent_image = agent_image.unwrap_or_else(|| { + concat!("ghcr.io/metalbear-co/mirrord:", env!("CARGO_PKG_VERSION")).to_string() + }); + let agent_pod: Job = serde_json::from_value(json!({ // Only Jobs support self deletion after completion "metadata": { @@ -104,7 +121,7 @@ pub async fn create_agent( { "name": "mirrord-agent", "image": agent_image, - "imagePullPolicy": "IfNotPresent", + "imagePullPolicy": image_pull_policy, "securityContext": { "privileged": true, }, @@ -123,7 +140,7 @@ pub async fn create_agent( "-t", "30", ], - "env": [{"name": "RUST_LOG", "value": log_level}], + "env": [{"name": "RUST_LOG", "value": agent_rust_log}], } ] } @@ -133,13 +150,13 @@ pub async fn create_agent( )) .unwrap(); - let jobs_api: Api = Api::namespaced(client.clone(), agent_namespace); + let jobs_api: Api = Api::namespaced(client.clone(), &agent_namespace); jobs_api .create(&PostParams::default(), &agent_pod) .await .unwrap(); - let pods_api: Api = Api::namespaced(client.clone(), agent_namespace); + let pods_api: Api = Api::namespaced(client.clone(), &agent_namespace); let params = ListParams::default() .labels(&format!("job-name={}", agent_job_name)) .timeout(10); diff --git a/mirrord-protocol/src/codec.rs b/mirrord-protocol/src/codec.rs index 650b976173f..280ff5ac359 100644 --- a/mirrord-protocol/src/codec.rs +++ b/mirrord-protocol/src/codec.rs @@ -1,4 +1,5 @@ use std::{ + collections::{HashMap, HashSet}, io::{self, SeekFrom}, path::PathBuf, }; @@ -114,6 +115,12 @@ pub struct CloseFileRequest { pub fd: usize, } +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct GetEnvVarsRequest { + pub env_vars_filter: HashSet, + pub env_vars_select: HashSet, +} + #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum FileRequest { Open(OpenFileRequest), @@ -130,6 +137,7 @@ pub enum ClientMessage { Close, Tcp(LayerTcp), FileRequest(FileRequest), + GetEnvVarsRequest(GetEnvVarsRequest), Ping, } @@ -174,6 +182,7 @@ pub enum DaemonMessage { LogMessage(LogMessage), FileResponse(FileResponse), Pong, + GetEnvVarsResponse(Result, ResponseError>), } pub struct ClientCodec { diff --git a/mirrord-protocol/src/lib.rs b/mirrord-protocol/src/lib.rs index 750dfa5de19..120d661acc9 100644 --- a/mirrord-protocol/src/lib.rs +++ b/mirrord-protocol/src/lib.rs @@ -1,13 +1,34 @@ #![feature(const_trait_impl)] #![feature(io_error_more)] -#![feature(core_ffi_c)] pub mod codec; pub mod error; pub mod tcp; +use std::{collections::HashSet, ops::Deref}; + pub use codec::*; pub use error::*; pub type ConnectionID = u16; pub type Port = u16; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct EnvVars(pub String); + +impl From for HashSet { + fn from(env_vars: EnvVars) -> Self { + env_vars + .split_terminator(';') + .map(String::from) + .collect::>() + } +} + +impl Deref for EnvVars { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/sample/node/app.mjs b/sample/node/app.mjs index 297ec812a40..cfd49db31e8 100644 --- a/sample/node/app.mjs +++ b/sample/node/app.mjs @@ -28,7 +28,7 @@ async function debug_file_ops() { } } -debug_file_ops(); +// debug_file_ops(); const server = createServer(); server.on("connection", handleConnection); diff --git a/tests/app.yaml b/tests/app.yaml index 018f155646a..1a0a5f4fb21 100644 --- a/tests/app.yaml +++ b/tests/app.yaml @@ -19,6 +19,11 @@ spec: image: ealen/echo-server ports: - containerPort: 80 + env: + - name: MIRRORD_FAKE_VAR_FIRST + value: mirrord.is.running + - name: MIRRORD_FAKE_VAR_SECOND + value: "7777" --- apiVersion: v1 diff --git a/tests/node-e2e/remote_env/test_remote_env_vars_does_nothing_when_not_specified.mjs b/tests/node-e2e/remote_env/test_remote_env_vars_does_nothing_when_not_specified.mjs new file mode 100644 index 00000000000..c35ebb65483 --- /dev/null +++ b/tests/node-e2e/remote_env/test_remote_env_vars_does_nothing_when_not_specified.mjs @@ -0,0 +1,7 @@ +console.log(">> test_remote_env_vars_does_nothing_when_not_specified"); + +if (process.env.MIRRORD_FAKE_VAR_FIRST || process.env.MIRRORD_FAKE_VAR_SECOND) { + process.exit(-1); +} else { + process.exit(0); +} diff --git a/tests/node-e2e/remote_env/test_remote_env_vars_exclude_works.mjs b/tests/node-e2e/remote_env/test_remote_env_vars_exclude_works.mjs new file mode 100644 index 00000000000..87f6ef18983 --- /dev/null +++ b/tests/node-e2e/remote_env/test_remote_env_vars_exclude_works.mjs @@ -0,0 +1,10 @@ +console.log(">> test_remote_env_vars_exclude_works"); + +if ( + process.env.MIRRORD_FAKE_VAR_FIRST === undefined && + process.env.MIRRORD_FAKE_VAR_SECOND === "7777" +) { + process.exit(0); +} else { + process.exit(-1); +} diff --git a/tests/node-e2e/remote_env/test_remote_env_vars_include_works.mjs b/tests/node-e2e/remote_env/test_remote_env_vars_include_works.mjs new file mode 100644 index 00000000000..ebcdce7165b --- /dev/null +++ b/tests/node-e2e/remote_env/test_remote_env_vars_include_works.mjs @@ -0,0 +1,10 @@ +console.log(">> test_remote_env_vars_include_works"); + +if ( + process.env.MIRRORD_FAKE_VAR_FIRST === "mirrord.is.running" && + process.env.MIRRORD_FAKE_VAR_SECOND === undefined +) { + process.exit(0); +} else { + process.exit(-1); +} diff --git a/tests/node-e2e/remote_env/test_remote_env_vars_panics_when_both_filters_are_specified.mjs b/tests/node-e2e/remote_env/test_remote_env_vars_panics_when_both_filters_are_specified.mjs new file mode 100644 index 00000000000..f7d860022bb --- /dev/null +++ b/tests/node-e2e/remote_env/test_remote_env_vars_panics_when_both_filters_are_specified.mjs @@ -0,0 +1,7 @@ +console.log(">> test_remote_env_vars_panics_when_both_filters_are_specified"); + +if (process.env.MIRRORD_FAKE_VAR_SECOND) { + process.exit(-1); +} else { + process.exit(0); +} diff --git a/tests/src/sanity.rs b/tests/src/sanity.rs index d493d2ca6ff..e8cbce1d861 100644 --- a/tests/src/sanity.rs +++ b/tests/src/sanity.rs @@ -403,4 +403,168 @@ mod tests { .unwrap(); assert!(test.success()); } + + #[tokio::test] + pub async fn test_remote_env_vars_does_nothing_when_not_specified() { + let mirrord_bin = env!("CARGO_BIN_FILE_MIRRORD"); + let node_command = vec![ + "node", + "node-e2e/remote_env/test_remote_env_vars_does_nothing_when_not_specified.mjs", + ]; + + let client = setup_kube_client().await; + + let pod_namespace = "default"; + let mut env = HashMap::new(); + env.insert("MIRRORD_AGENT_IMAGE", "test"); + env.insert("MIRRORD_CHECK_VERSION", "false"); + + let pod_name = get_http_echo_pod_name(&client, pod_namespace) + .await + .unwrap(); + + let args: Vec<&str> = vec!["exec", "--pod-name", &pod_name, "-c", "--"] + .into_iter() + .chain(node_command.into_iter()) + .collect(); + + let test_process = Command::new(mirrord_bin) + .args(args) + .envs(&env) + .status() + .await + .unwrap(); + + assert!(test_process.success()); + } + + /// Weird one to test, as we `panic` inside a separate thread, so the main sample app will just + /// complete as normal. + #[tokio::test] + pub async fn test_remote_env_vars_panics_when_both_filters_are_specified() { + let mirrord_bin = env!("CARGO_BIN_FILE_MIRRORD"); + let node_command = vec![ + "node", + "node-e2e/remote_env/test_remote_env_vars_panics_when_both_filters_are_specified.mjs", + ]; + + let client = setup_kube_client().await; + + let pod_namespace = "default"; + let mut env = HashMap::new(); + env.insert("MIRRORD_AGENT_IMAGE", "test"); + env.insert("MIRRORD_CHECK_VERSION", "false"); + + let pod_name = get_http_echo_pod_name(&client, pod_namespace) + .await + .unwrap(); + + let args: Vec<&str> = vec![ + "exec", + "--pod-name", + &pod_name, + "-c", + "-x", + "MIRRORD_FAKE_VAR_FIRST", + "-s", + "MIRRORD_FAKE_VAR_SECOND", + "--", + ] + .into_iter() + .chain(node_command.into_iter()) + .collect(); + + let test_process = Command::new(mirrord_bin) + .args(args) + .envs(&env) + .status() + .await + .unwrap(); + + assert!(test_process.success()); + } + + #[tokio::test] + pub async fn test_remote_env_vars_exclude_works() { + let mirrord_bin = env!("CARGO_BIN_FILE_MIRRORD"); + let node_command = vec![ + "node", + "node-e2e/remote_env/test_remote_env_vars_exclude_works.mjs", + ]; + + let client = setup_kube_client().await; + + let pod_namespace = "default"; + let mut env = HashMap::new(); + env.insert("MIRRORD_AGENT_IMAGE", "test"); + env.insert("MIRRORD_CHECK_VERSION", "false"); + + let pod_name = get_http_echo_pod_name(&client, pod_namespace) + .await + .unwrap(); + + let args: Vec<&str> = vec![ + "exec", + "--pod-name", + &pod_name, + "-c", + "-x", + "MIRRORD_FAKE_VAR_FIRST", + "--", + ] + .into_iter() + .chain(node_command.into_iter()) + .collect(); + + let test_process = Command::new(mirrord_bin) + .args(args) + .envs(&env) + .status() + .await + .unwrap(); + + assert!(test_process.success()); + } + + #[tokio::test] + pub async fn test_remote_env_vars_include_works() { + let mirrord_bin = env!("CARGO_BIN_FILE_MIRRORD"); + let node_command = vec![ + "node", + "node-e2e/remote_env/test_remote_env_vars_include_works.mjs", + ]; + + let client = setup_kube_client().await; + + let pod_namespace = "default"; + let mut env = HashMap::new(); + env.insert("MIRRORD_AGENT_IMAGE", "test"); + env.insert("MIRRORD_CHECK_VERSION", "false"); + + let pod_name = get_http_echo_pod_name(&client, pod_namespace) + .await + .unwrap(); + + let args: Vec<&str> = vec![ + "exec", + "--pod-name", + &pod_name, + "-c", + "-s", + "MIRRORD_FAKE_VAR_FIRST", + "--", + ] + .into_iter() + .chain(node_command.into_iter()) + .collect(); + + let test_process = Command::new(mirrord_bin) + .args(args) + .envs(&env) + .status() + .await + .unwrap(); + + assert!(test_process.success()); + } } diff --git a/tests/test b/tests/test new file mode 100644 index 00000000000..08e00ed2916 --- /dev/null +++ b/tests/test @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file