From b18c2533edaae4b57f8a9a39a380fc156b45d58f Mon Sep 17 00:00:00 2001 From: meowjesty <43983236+meowjesty@users.noreply.github.com> Date: Tue, 21 Jun 2022 03:44:16 -0300 Subject: [PATCH] Override remote env #25 (#134) * Adding remote env var loading. * Loading env vars from nginx. * Loading env vars from remote proc works, but it doesn't load the correct string? * Notes on how to proceed (nginx out). * Working override env vars. * Fix wrong pid value being used. * Clean files. * Use correct imagePullPolicy. * Simplify env vars handling. Make it select all then filter. * Fix launch.json env. * Remove unused rust features. * Put env vars override behind an enabled bool flag. * Add configuration for setting up override env vars + filter. * Refactor some layer initialization functions to take config, instead of select arguments. Changed name for env vars request/response messages. * Update changelog. Renamed env vars config. Env vars overriding now filters out some keys by default, like PATH (user cant control these defaults). * Fix comment. * local config. * Added config to change image pull policy from env. * Fix clap deprecations. * Remote env changes. * Use clap 3, instead of specifying subversion. * Update cargo.lock. * Use std::env::set_var instead of libc::setenv (plus assert the var is properly set). * Increase initial memory allocation for string buffer that receives env vars. * Change mirrord to custom image for tests. * Add remote env vars test. * Make test app crash on incorrect env vars. * Ignore unrelated tests to make CI go faster. * Env vars now filter + select properly. * Fix include string. * Fix env vars test args. * Added more tests for remote_env. Don't allow user to specify both include + exclude. Removed flag to enable/disable env vars, now controller by having values in the filters. * Fix launch.json. * Use proper test image. * Fix remote_env tests using the wrong option. * Fix comments. * Support include * as an include all env vars. * Log result of tests process. * Failure test should panic. * Less clutter in console for remote env tests. * Debug why test that should panic (and panics), doesn't trigger the test panic (wow what a phrase). * Return exit code -1 if we have an env var selected. * Fix test for panicking remote_env. Re-enable other tests. --- CHANGELOG.md | 14 +- mirrord-agent/src/main.rs | 136 ++++++++++++++- mirrord-cli/src/config.rs | 68 ++++++++ mirrord-cli/src/main.rs | 76 ++------ mirrord-layer/src/config.rs | 15 +- mirrord-layer/src/lib.rs | 85 +++++++-- mirrord-layer/src/pod_api.rs | 45 +++-- mirrord-protocol/src/codec.rs | 9 + mirrord-protocol/src/lib.rs | 23 ++- sample/node/app.mjs | 2 +- tests/app.yaml | 5 + ...v_vars_does_nothing_when_not_specified.mjs | 7 + .../test_remote_env_vars_exclude_works.mjs | 10 ++ .../test_remote_env_vars_include_works.mjs | 10 ++ ...panics_when_both_filters_are_specified.mjs | 7 + tests/src/sanity.rs | 164 ++++++++++++++++++ tests/test | 1 + 17 files changed, 574 insertions(+), 103 deletions(-) create mode 100644 mirrord-cli/src/config.rs create mode 100644 tests/node-e2e/remote_env/test_remote_env_vars_does_nothing_when_not_specified.mjs create mode 100644 tests/node-e2e/remote_env/test_remote_env_vars_exclude_works.mjs create mode 100644 tests/node-e2e/remote_env/test_remote_env_vars_include_works.mjs create mode 100644 tests/node-e2e/remote_env/test_remote_env_vars_panics_when_both_filters_are_specified.mjs create mode 100644 tests/test 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