Repository: metalbear-co/mirrord (language: rust)

Issue:
mirrord + vscode + templating seems to get ide to hang
when running a launch configuration as follows (next dev project, node 18, macOS)
```json
 {  "version": "0.2.0",
  "configurations": [   {
      "name": "NEW",
      "request": "launch",
      "runtimeArgs": [
        "next",
        "dev"
      ],
      "runtimeExecutable": "npx",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "env": {
        "NODE_OPTIONS": "--inspect"
      },
      "type": "node",
      "outFiles": [
        "${workspaceFolder}/x/**/*.(m|c|)js",
        "!**/node_modules/**"
      ],
      "cwd": "${workspaceFolder}/apps/x",
    }
  ]
}
```

mirrord.json:
```
{
  "target": {
    "path": "x",
    "namespace": "y"
  },

  "feature": {
    "env": {
      "override": {
        "NODE_EXTRA_CA_CERTS": "{{ get_env(name="HOMEBREW_PREFIX", default="/opt/homebrew") }}/etc/ca-certificates/cert.pem",
        "NODE_ENV": "development"
      }
    },
    "fs": false,
    "network": {
      "incoming": {
        "mode": "steal",
        "http_filter": {
          "header_filter": "x-intercept: {{ get_env(name="USER") }}"
        },
        "port_mapping": [[4003, 8080]]
      },
      "outgoing": false
    }
  },
  "operator": true
}
```


Discussion / hints:
<p><a href="https://linear.app/metalbear/issue/MBE-535/mirrord-vscode-templating-seems-to-get-ide-to-hang">MBE-535 mirrord + vscode + templating seems to get ide to hang</a></p>
after trying out a few different launch configs, i think the issue may be down to the `env` field - out of the two following launch configs, the first works with and without mirrord enabled, but the second only works without mirrord (and hangs when mirrord is on):

```jsonc
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Next.js: Working",
            "type": "node",
            "request": "launch",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "runtimeExecutable": "npx",
            "runtimeArgs": [
                "next",
                "dev"
            ],
        },
        {
            "name": "Next.js: Non-working",
            "type": "node",
            "request": "launch",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "runtimeExecutable": "npx",
            "runtimeArgs": [
                "next",
                "dev"
            ],
            "env": {
                "NODE_OPTIONS": "--inspect"
            },
        }
    ]
}
```

now, we just need to find out what the problem is with using `env`
~~for now, i think the following configuration should work and do the same thing :)~~ the `--inspect` flag can only be set in `NODE_OPTIONS`, and there looks to be interference with mirrord concerning the port that the process listens to - to prevent the hanging until this is fixed, remove `--inspect` altogether

```json
 { "version": "0.2.0",
  "configurations": [ {
      "name": "NEW",
      "request": "launch",
      "runtimeArgs": [
        "next",
        "dev"
      ],
      "runtimeExecutable": "npx",
      "skipFiles": [
        "/**"
      ],
      "type": "node",
      "outFiles": [
        "${workspaceFolder}/x/**/*.(m|c|)js",
        "!**/node_modules/**"
      ],
      "cwd": "${workspaceFolder}/apps/x",
    }
  ]
}
```
mirrord is being loaded into vscode's watchdog extension (which looks to be an activity monitor or some sort) and then trying to render the config with Tera - the call to get_env() is then hanging. This can be worked around by adding a default value, for example `"header_filter": "x-intercept: {{ get_env(name="USER",default="default") }}"` but should be fixed long-term by stopping mirrord from loading into this process altogether

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 89528a03ca1..650ad376774 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4262,6 +4262,7 @@ dependencies = [
 name = "mirrord-config"
 version = "3.128.0"
 dependencies = [
+ "base64 0.22.1",
  "bimap",
  "bitflags 2.6.0",
  "fancy-regex",
diff --git a/changelog.d/2936.fixed.md b/changelog.d/2936.fixed.md
new file mode 100644
index 00000000000..e4dfd4792a6
--- /dev/null
+++ b/changelog.d/2936.fixed.md
@@ -0,0 +1,1 @@
+Stopped mirrord entering a crash loop when trying to load into some processes like VSCode's `watchdog.js` when the user config contained a call to `get_env()`, which occurred due to missing env - the config is now only rendered once and set into an env var.
\ No newline at end of file
diff --git a/mirrord/cli/src/container.rs b/mirrord/cli/src/container.rs
index aa396a0162f..a2659190e0d 100644
--- a/mirrord/cli/src/container.rs
+++ b/mirrord/cli/src/container.rs
@@ -333,6 +333,7 @@ pub(crate) async fn container_command(
         (CONTAINER_EXECUTION_KIND as u32).to_string(),
     );
 
+    // LayerConfig must be created after setting relevant env vars
     let (mut config, mut analytics) = create_config_and_analytics(&mut progress, watch)?;
 
     let (_internal_proxy_tls_guards, _external_proxy_tls_guards) =
@@ -427,6 +428,7 @@ pub(crate) async fn container_ext_command(
         env.insert("MIRRORD_IMPERSONATED_TARGET".into(), target.to_string());
     }
 
+    // LayerConfig must be created after setting relevant env vars
     let (mut config, mut analytics) = create_config_and_analytics(&mut progress, watch)?;
 
     let (_internal_proxy_tls_guards, _external_proxy_tls_guards) =
diff --git a/mirrord/cli/src/execution.rs b/mirrord/cli/src/execution.rs
index 7629b466b20..5378f83d5b6 100644
--- a/mirrord/cli/src/execution.rs
+++ b/mirrord/cli/src/execution.rs
@@ -177,7 +177,7 @@ impl MirrordExecution {
     /// [`tokio::time::sleep`] or [`tokio::task::yield_now`] after calling this function.
     #[tracing::instrument(level = Level::TRACE, skip_all)]
     pub(crate) async fn start<P>(
-        config: &LayerConfig,
+        config: &mut LayerConfig,
         // We only need the executable on macos, for SIP handling.
         #[cfg(target_os = "macos")] executable: Option<&str>,
         progress: &mut P,
@@ -289,10 +289,8 @@ impl MirrordExecution {
             })?;
 
         // Provide details for layer to connect to agent via internal proxy
-        env_vars.insert(
-            MIRRORD_CONNECT_TCP_ENV.to_string(),
-            format!("127.0.0.1:{}", address.port()),
-        );
+        config.connect_tcp = Some(format!("127.0.0.1:{}", address.port()));
+        config.update_env_var()?;
 
         // Fixes <https://github.com/metalbear-co/mirrord/issues/1745>
         // by disabling the fork safety check in the Objective-C runtime.
diff --git a/mirrord/cli/src/extension.rs b/mirrord/cli/src/extension.rs
index f8d4a0da482..005491a9aed 100644
--- a/mirrord/cli/src/extension.rs
+++ b/mirrord/cli/src/extension.rs
@@ -10,7 +10,7 @@ use crate::{config::ExtensionExecArgs, error::CliError, execution::MirrordExecut
 async fn mirrord_exec<P>(
     #[cfg(target_os = "macos")] executable: Option<&str>,
     env: HashMap<String, String>,
-    config: LayerConfig,
+    mut config: LayerConfig,
     mut progress: P,
     analytics: &mut AnalyticsReporter,
 ) -> CliResult<()>
@@ -21,9 +21,9 @@ where
     // or run tasks before actually launching.
     #[cfg(target_os = "macos")]
     let mut execution_info =
-        MirrordExecution::start(&config, executable, &mut progress, analytics).await?;
+        MirrordExecution::start(&mut config, executable, &mut progress, analytics).await?;
     #[cfg(not(target_os = "macos"))]
-    let mut execution_info = MirrordExecution::start(&config, &mut progress, analytics).await?;
+    let mut execution_info = MirrordExecution::start(&mut config, &mut progress, analytics).await?;
 
     // We don't execute so set envs aren't passed, so we need to add config file and target to
     // env.
diff --git a/mirrord/cli/src/external_proxy.rs b/mirrord/cli/src/external_proxy.rs
index a6ab4bb8674..34bf46b82b8 100644
--- a/mirrord/cli/src/external_proxy.rs
+++ b/mirrord/cli/src/external_proxy.rs
@@ -60,7 +60,7 @@ fn print_addr(listener: &TcpListener) -> io::Result<()> {
 }
 
 pub async fn proxy(listen_port: u16, watch: drain::Watch) -> CliResult<()> {
-    let config = LayerConfig::from_env()?;
+    let config = LayerConfig::recalculate_from_env()?;
 
     init_extproxy_tracing_registry(&config)?;
     tracing::info!(?config, "external_proxy starting");
diff --git a/mirrord/cli/src/internal_proxy.rs b/mirrord/cli/src/internal_proxy.rs
index b1b9a20b72a..e8210962e84 100644
--- a/mirrord/cli/src/internal_proxy.rs
+++ b/mirrord/cli/src/internal_proxy.rs
@@ -50,7 +50,7 @@ pub(crate) async fn proxy(
     listen_port: u16,
     watch: drain::Watch,
 ) -> CliResult<(), InternalProxyError> {
-    let config = LayerConfig::from_env()?;
+    let config = LayerConfig::recalculate_from_env()?;
 
     init_intproxy_tracing_registry(&config)?;
     tracing::info!(?config, "internal_proxy starting");
diff --git a/mirrord/cli/src/main.rs b/mirrord/cli/src/main.rs
index 4631af499dc..2ca498c83fc 100644
--- a/mirrord/cli/src/main.rs
+++ b/mirrord/cli/src/main.rs
@@ -64,7 +64,7 @@ pub(crate) use error::{CliError, CliResult};
 use verify_config::verify_config;
 
 async fn exec_process<P>(
-    config: LayerConfig,
+    mut config: LayerConfig,
     args: &ExecArgs,
     progress: &P,
     analytics: &mut AnalyticsReporter,
@@ -75,10 +75,15 @@ where
     let mut sub_progress = progress.subtask("preparing to launch process");
 
     #[cfg(target_os = "macos")]
-    let execution_info =
-        MirrordExecution::start(&config, Some(&args.binary), &mut sub_progress, analytics).await?;
+    let execution_info = MirrordExecution::start(
+        &mut config,
+        Some(&args.binary),
+        &mut sub_progress,
+        analytics,
+    )
+    .await?;
     #[cfg(not(target_os = "macos"))]
-    let execution_info = MirrordExecution::start(&config, &mut sub_progress, analytics).await?;
+    let execution_info = MirrordExecution::start(&mut config, &mut sub_progress, analytics).await?;
 
     // This is not being yielded, as this is not proper async, something along those lines.
     // We need an `await` somewhere in this function to drive our socket IO that happens
@@ -352,6 +357,7 @@ async fn exec(args: &ExecArgs, watch: drain::Watch) -> CliResult<()> {
         std::env::set_var(name, value);
     }
 
+    // LayerConfig must be created after setting relevant env vars
     let (config, mut context) = LayerConfig::from_env_with_warnings()?;
 
     let mut analytics = AnalyticsReporter::only_error(config.telemetry, Default::default(), watch);
@@ -472,6 +478,7 @@ async fn port_forward(args: &PortForwardArgs, watch: drain::Watch) -> CliResult<
         std::env::set_var("MIRRORD_CONFIG_FILE", config_file);
     }
 
+    // LayerConfig must be created after setting relevant env vars
     let (config, mut context) = LayerConfig::from_env_with_warnings()?;
 
     let mut analytics = AnalyticsReporter::new(config.telemetry, ExecutionKind::PortForward, watch);
diff --git a/mirrord/config/Cargo.toml b/mirrord/config/Cargo.toml
index e17985aa0cd..a2fd265c274 100644
--- a/mirrord/config/Cargo.toml
+++ b/mirrord/config/Cargo.toml
@@ -17,8 +17,8 @@ edition.workspace = true
 workspace = true
 
 [dependencies]
-mirrord-config-derive = { path = "./derive"}
-mirrord-analytics = { path = "../analytics"}
+mirrord-config-derive = { path = "./derive" }
+mirrord-analytics = { path = "../analytics" }
 
 serde.workspace = true
 serde_json.workspace = true
@@ -27,13 +27,14 @@ tracing.workspace = true
 serde_yaml.workspace = true
 toml = "0.8"
 schemars.workspace = true
-bimap = "0.6"
+bimap = { version = "0.6" }
 nom = "7.1"
 ipnet.workspace = true
 bitflags = "2"
 k8s-openapi = { workspace = true, features = ["schemars", "earliest"] }
 tera = "1"
 fancy-regex.workspace = true
+base64.workspace = true
 
 [dev-dependencies]
 rstest.workspace = true
diff --git a/mirrord/config/src/agent.rs b/mirrord/config/src/agent.rs
index b82c45a7177..3dff5adfefc 100644
--- a/mirrord/config/src/agent.rs
+++ b/mirrord/config/src/agent.rs
@@ -67,7 +67,7 @@ impl fmt::Display for LinuxCapability {
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "AgentFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq"))]
 pub struct AgentConfig {
@@ -354,7 +354,7 @@ pub struct AgentConfig {
     /// Create an agent that returns an error after accepting the first client. For testing
     /// purposes. Only supported with job agents (not with ephemeral agents).
     #[cfg(all(debug_assertions, not(test)))] // not(test) so that it's not included in the schema json.
-    #[serde(skip_serializing)]
+    #[serde(skip)]
     #[config(env = "MIRRORD_AGENT_TEST_ERROR", default = false, unstable)]
     pub test_error: bool,
 }
@@ -477,7 +477,7 @@ impl AgentFileConfig {
     }
 }
 
-#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
 #[config(derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct AgentDnsConfig {
diff --git a/mirrord/config/src/config.rs b/mirrord/config/src/config.rs
index c0176c6f66b..9fa4bdd168a 100644
--- a/mirrord/config/src/config.rs
+++ b/mirrord/config/src/config.rs
@@ -86,6 +86,12 @@ pub enum ConfigError {
         value: String,
         fail: Box<fancy_regex::Error>,
     },
+
+    #[error("mirrord-config: decoding resolved config from env var failed with `{0}`")]
+    EnvVarDecodeError(String),
+
+    #[error("mirrord-config: encoding resolved config failed with `{0}`")]
+    EnvVarEncodeError(String),
 }
 
 impl From<tera::Error> for ConfigError {
diff --git a/mirrord/config/src/container.rs b/mirrord/config/src/container.rs
index 5f9e16f7447..c48fec4a34d 100644
--- a/mirrord/config/src/container.rs
+++ b/mirrord/config/src/container.rs
@@ -2,7 +2,7 @@ use std::path::PathBuf;
 
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use crate::config::source::MirrordConfigSource;
 
@@ -12,7 +12,7 @@ static DEFAULT_CLI_IMAGE: &str = concat!(
 );
 
 /// Unstable: `mirrord container` command specific config.
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "ContainerFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq"))]
 pub struct ContainerConfig {
diff --git a/mirrord/config/src/experimental.rs b/mirrord/config/src/experimental.rs
index 302cb14fc27..29af8e2f145 100644
--- a/mirrord/config/src/experimental.rs
+++ b/mirrord/config/src/experimental.rs
@@ -1,13 +1,13 @@
 use mirrord_analytics::CollectAnalytics;
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use crate::config::source::MirrordConfigSource;
 
 /// mirrord Experimental features.
 /// This shouldn't be used unless someone from MetalBear/mirrord tells you to.
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "ExperimentalFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct ExperimentalConfig {
diff --git a/mirrord/config/src/external_proxy.rs b/mirrord/config/src/external_proxy.rs
index 2ce284b6b7b..0f1c80f5e0a 100644
--- a/mirrord/config/src/external_proxy.rs
+++ b/mirrord/config/src/external_proxy.rs
@@ -2,7 +2,7 @@ use std::path::PathBuf;
 
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use crate::config::source::MirrordConfigSource;
 
@@ -23,7 +23,7 @@ pub static MIRRORD_EXTERNAL_TLS_KEY_ENV: &str = "MIRRORD_EXTERNAL_TLS_KEY";
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "ExternalProxyFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq"))]
 pub struct ExternalProxyConfig {
diff --git a/mirrord/config/src/feature.rs b/mirrord/config/src/feature.rs
index 317d590e0c8..41fecca7002 100644
--- a/mirrord/config/src/feature.rs
+++ b/mirrord/config/src/feature.rs
@@ -1,7 +1,7 @@
 use mirrord_analytics::CollectAnalytics;
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use self::{copy_target::CopyTargetConfig, env::EnvConfig, fs::FsConfig, network::NetworkConfig};
 use crate::{config::source::MirrordConfigSource, feature::split_queues::SplitQueuesConfig};
@@ -64,7 +64,7 @@ pub mod split_queues;
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "FeatureFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct FeatureConfig {
diff --git a/mirrord/config/src/feature/copy_target.rs b/mirrord/config/src/feature/copy_target.rs
index c5ed004ebb4..8a72326d52a 100644
--- a/mirrord/config/src/feature/copy_target.rs
+++ b/mirrord/config/src/feature/copy_target.rs
@@ -75,7 +75,7 @@ impl FromMirrordConfig for CopyTargetConfig {
 ///   }
 /// }
 /// ```
-#[derive(Clone, Debug, Serialize)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
 pub struct CopyTargetConfig {
     pub enabled: bool,
 
diff --git a/mirrord/config/src/feature/env.rs b/mirrord/config/src/feature/env.rs
index 339b0c16e78..700b31f0667 100644
--- a/mirrord/config/src/feature/env.rs
+++ b/mirrord/config/src/feature/env.rs
@@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf};
 use mirrord_analytics::CollectAnalytics;
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use crate::{
     config::{from_env::FromEnv, source::MirrordConfigSource, ConfigContext, Result},
@@ -47,7 +47,7 @@ pub const MIRRORD_OVERRIDE_ENV_FILE_ENV: &str = "MIRRORD_OVERRIDE_ENV_VARS_FILE"
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "EnvFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct EnvConfig {
diff --git a/mirrord/config/src/feature/fs/advanced.rs b/mirrord/config/src/feature/fs/advanced.rs
index da1df66c00f..40c0e21c926 100644
--- a/mirrord/config/src/feature/fs/advanced.rs
+++ b/mirrord/config/src/feature/fs/advanced.rs
@@ -3,7 +3,7 @@ use std::collections::HashMap;
 use mirrord_analytics::{AnalyticValue, CollectAnalytics};
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use super::{FsModeConfig, FsUserConfig};
 use crate::{
@@ -80,7 +80,7 @@ use crate::{
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Default, Clone, PartialEq, Eq, Debug, Serialize)]
+#[derive(MirrordConfig, Default, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
 #[config(
     map_to = "AdvancedFsUserConfig",
     derive = "PartialEq,Eq,JsonSchema",
diff --git a/mirrord/config/src/feature/network.rs b/mirrord/config/src/feature/network.rs
index 227bd82a915..1d86071e32e 100644
--- a/mirrord/config/src/feature/network.rs
+++ b/mirrord/config/src/feature/network.rs
@@ -2,7 +2,7 @@ use dns::{DnsConfig, DnsFileConfig};
 use mirrord_analytics::CollectAnalytics;
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use self::{incoming::*, outgoing::*};
 use crate::{
@@ -54,7 +54,7 @@ pub mod outgoing;
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
 #[config(map_to = "NetworkFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct NetworkConfig {
diff --git a/mirrord/config/src/feature/network/dns.rs b/mirrord/config/src/feature/network/dns.rs
index fd0df6411b6..9371a2ee1e8 100644
--- a/mirrord/config/src/feature/network/dns.rs
+++ b/mirrord/config/src/feature/network/dns.rs
@@ -87,7 +87,7 @@ pub enum DnsFilterConfig {
 ///   `read_only: ["/etc/resolv.conf"]`.
 /// - DNS filter currently works only with frameworks that use `getaddrinfo`/`gethostbyname`
 ///   functions.
-#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
 #[config(map_to = "DnsFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct DnsConfig {
diff --git a/mirrord/config/src/feature/network/incoming.rs b/mirrord/config/src/feature/network/incoming.rs
index fddd46260d5..857fb0179d3 100644
--- a/mirrord/config/src/feature/network/incoming.rs
+++ b/mirrord/config/src/feature/network/incoming.rs
@@ -310,6 +310,7 @@ fn serialize_bi_map<S>(map: &BiMap<u16, u16>, serializer: S) -> Result<S::Ok, S:
 where
     S: ser::Serializer,
 {
+    // NB: this serialises the BiMap to a vec
     let mut seq = serializer.serialize_seq(Some(map.len()))?;
 
     for (key, value) in map {
@@ -319,6 +320,20 @@ where
     seq.end()
 }
 
+fn deserialize_bi_map<'de, D>(deserializer: D) -> Result<BiMap<u16, u16>, D::Error>
+where
+    D: de::Deserializer<'de>,
+{
+    // NB: this deserialises the BiMap from a vec
+    let vec: Vec<(u16, u16)> = Vec::deserialize(deserializer)?;
+
+    let mut elements = BiMap::new();
+    vec.iter().for_each(|(key, value)| {
+        elements.insert(*key, *value);
+    });
+    Ok(elements)
+}
+
 /// Controls the incoming TCP traffic feature.
 ///
 /// See the incoming [reference](https://mirrord.dev/docs/reference/traffic/#incoming) for more
@@ -387,7 +402,7 @@ where
 ///   }
 /// }
 /// ```
-#[derive(Default, PartialEq, Eq, Clone, Debug, Serialize)]
+#[derive(Default, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
 pub struct IncomingConfig {
     /// #### feature.network.incoming.port_mapping {#feature-network-incoming-port_mapping}
     ///
@@ -396,7 +411,10 @@ pub struct IncomingConfig {
     /// This is useful when you want to mirror/steal a port to a different port on the remote
     /// machine. For example, your local process listens on port `9333` and the container listens
     /// on port `80`. You'd use `[[9333, 80]]`
-    #[serde(serialize_with = "serialize_bi_map")]
+    #[serde(
+        serialize_with = "serialize_bi_map",
+        deserialize_with = "deserialize_bi_map"
+    )]
     pub port_mapping: BiMap<u16, u16>,
 
     /// #### feature.network.incoming.ignore_localhost {#feature-network-incoming-ignore_localhost}
@@ -434,7 +452,10 @@ pub struct IncomingConfig {
     /// you probably can't listen on `80` without sudo, so you can use `[[80, 4480]]`
     /// then access it on `4480` while getting traffic from remote `80`.
     /// The value of `port_mapping` doesn't affect this.
-    #[serde(serialize_with = "serialize_bi_map")]
+    #[serde(
+        serialize_with = "serialize_bi_map",
+        deserialize_with = "deserialize_bi_map"
+    )]
     pub listen_ports: BiMap<u16, u16>,
 
     /// #### feature.network.incoming.on_concurrent_steal {#feature-network-incoming-on_concurrent_steal}
diff --git a/mirrord/config/src/feature/network/incoming/http_filter.rs b/mirrord/config/src/feature/network/incoming/http_filter.rs
index 343e850b75d..c68e288fa58 100644
--- a/mirrord/config/src/feature/network/incoming/http_filter.rs
+++ b/mirrord/config/src/feature/network/incoming/http_filter.rs
@@ -79,7 +79,7 @@ use crate::{
 ///    { "header": "^x-debug-session: 121212$" }
 ///  ]
 ///}
-#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
 #[config(map_to = "HttpFilterFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct HttpFilterConfig {
diff --git a/mirrord/config/src/feature/network/outgoing.rs b/mirrord/config/src/feature/network/outgoing.rs
index 8ff2a84ce21..c0878a412a9 100644
--- a/mirrord/config/src/feature/network/outgoing.rs
+++ b/mirrord/config/src/feature/network/outgoing.rs
@@ -89,7 +89,7 @@ pub enum OutgoingFilterConfig {
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Default, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
 #[config(map_to = "OutgoingFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq, Eq"))]
 pub struct OutgoingConfig {
diff --git a/mirrord/config/src/internal_proxy.rs b/mirrord/config/src/internal_proxy.rs
index dd28f2f4d9e..ba5b9483479 100644
--- a/mirrord/config/src/internal_proxy.rs
+++ b/mirrord/config/src/internal_proxy.rs
@@ -2,7 +2,7 @@ use std::{net::SocketAddr, path::PathBuf};
 
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use crate::config::source::MirrordConfigSource;
 
@@ -26,7 +26,7 @@ pub static MIRRORD_INTPROXY_CLIENT_TLS_KEY_ENV: &str = "MIRRORD_INTPROXY_CLIENT_
 ///   }
 /// }
 /// ```
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "InternalProxyFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq"))]
 pub struct InternalProxyConfig {
diff --git a/mirrord/config/src/lib.rs b/mirrord/config/src/lib.rs
index 055adac98d2..8414bd742df 100644
--- a/mirrord/config/src/lib.rs
+++ b/mirrord/config/src/lib.rs
@@ -25,13 +25,14 @@ use std::{
     path::Path,
 };
 
+use base64::prelude::*;
 use config::{ConfigContext, ConfigError, MirrordConfig};
 use experimental::ExperimentalConfig;
 use feature::{env::mapper::EnvVarsRemapper, network::outgoing::OutgoingFilterConfig};
 use mirrord_analytics::CollectAnalytics;
 use mirrord_config_derive::MirrordConfig;
 use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 use target::Target;
 use tera::Tera;
 use tracing::warn;
@@ -45,6 +46,9 @@ use crate::{
 /// Env variable to load config from file (json, yaml and toml supported).
 pub static MIRRORD_CONFIG_FILE_ENV: &str = "MIRRORD_CONFIG_FILE";
 
+/// Env variable to load config from an already resolved base64 encoding.
+pub static MIRRORD_RESOLVED_CONFIG_ENV: &str = "MIRRORD_RESOLVED_CONFIG";
+
 /// mirrord allows for a high degree of customization when it comes to which features you want to
 /// enable, and how they should function.
 ///
@@ -174,7 +178,7 @@ pub static MIRRORD_CONFIG_FILE_ENV: &str = "MIRRORD_CONFIG_FILE";
 /// ```
 ///
 /// # Options {#root-options}
-#[derive(MirrordConfig, Clone, Debug, Serialize)]
+#[derive(MirrordConfig, Clone, Debug, Serialize, Deserialize, PartialEq)]
 #[config(map_to = "LayerFileConfig", derive = "JsonSchema")]
 #[cfg_attr(test, config(derive = "PartialEq"))]
 pub struct LayerConfig {
@@ -327,15 +331,55 @@ pub struct LayerConfig {
 }
 
 impl LayerConfig {
+    /// Given an encoded complete config from the [`MIRRORD_RESOLVED_CONFIG_ENV`]
+    /// env var, attempt to decode it into [`LayerConfig`].
+    /// Intended to avoid re-resolving the config in every process mirrord is loaded into.
+    fn from_env_var(encoded_value: String) -> Result<Self, ConfigError> {
+        let decoded = BASE64_STANDARD
+            .decode(encoded_value)
+            .map_err(|error| ConfigError::EnvVarDecodeError(error.to_string()))?;
+        let serialized = std::str::from_utf8(&decoded)
+            .map_err(|error| ConfigError::EnvVarDecodeError(error.to_string()))?;
+        Ok(serde_json::from_str::<Self>(serialized)?)
+    }
+
+    /// Given a [`LayerConfig`], serialise it and convert to base 64 so it can be
+    /// set into [`MIRRORD_RESOLVED_CONFIG_ENV`].
+    fn to_env_var(&self) -> Result<String, ConfigError> {
+        let serialized = serde_json::to_string(self)
+            .map_err(|error| ConfigError::EnvVarEncodeError(error.to_string()))?;
+        Ok(BASE64_STANDARD.encode(serialized))
+    }
+
+    /// Given the encoded config as a string, set it into [`MIRRORD_RESOLVED_CONFIG_ENV`].
+    /// Must be used when updating [`LayerConfig`] after creation in order for the config
+    /// in env to reflect the change.
+    pub fn update_env_var(&self) -> Result<(), ConfigError> {
+        std::env::set_var(MIRRORD_RESOLVED_CONFIG_ENV, self.to_env_var()?);
+        Ok(())
+    }
+
     /// Generate a config from the environment variables and/or a config file.
     /// On success, returns the config and a vec of warnings.
     /// To be used from CLI to verify config and print warnings
     pub fn from_env_with_warnings() -> Result<(Self, ConfigContext), ConfigError> {
         let mut cfg_context = ConfigContext::default();
-        if let Ok(path) = std::env::var(MIRRORD_CONFIG_FILE_ENV) {
-            LayerFileConfig::from_path(path)?.generate_config(&mut cfg_context)
-        } else {
-            LayerFileConfig::default().generate_config(&mut cfg_context)
+
+        match std::env::var(MIRRORD_RESOLVED_CONFIG_ENV) {
+            Ok(value) if !value.is_empty() => LayerConfig::from_env_var(value),
+            _ => {
+                // the resolved config is not present in env, so resolve it and then set into env
+                // var
+                let config = if let Ok(path) = std::env::var(MIRRORD_CONFIG_FILE_ENV) {
+                    LayerFileConfig::from_path(path)?.generate_config(&mut cfg_context)
+                } else {
+                    LayerFileConfig::default().generate_config(&mut cfg_context)
+                }?;
+
+                // serialise the config and encode as base64
+                config.update_env_var()?;
+                Ok(config)
+            }
         }
         .map(|config| (config, cfg_context))
     }
@@ -347,6 +391,17 @@ impl LayerConfig {
         Self::from_env_with_warnings().map(|(config, _)| config)
     }
 
+    /// forcefully recalculate the config using [`Self::from_env_with_warnings()`]
+    pub fn recalculate_from_env_with_warnings() -> Result<(Self, ConfigContext), ConfigError> {
+        std::env::remove_var(MIRRORD_RESOLVED_CONFIG_ENV);
+        Self::from_env_with_warnings()
+    }
+
+    /// forcefully recalculate the config using [`Self::from_env_with_warnings()`] without warnings
+    pub fn recalculate_from_env() -> Result<Self, ConfigError> {
+        Self::recalculate_from_env_with_warnings().map(|(config, _)| config)
+    }
+
     /// Verify that there are no conflicting settings.
     ///
     /// We don't call it from `from_env` since we want to verify it only once (from cli)
@@ -986,4 +1041,73 @@ mod tests {
 
         assert_eq!(existing_content.replace("\r\n", "\n"), compare_content);
     }
+
+    /// related to issue #2936: https://github.com/metalbear-co/mirrord/issues/2936
+    /// checks that resolved config written to [`MIRRORD_RESOLVED_CONFIG_ENV`] can be
+    /// transformed back into a [`LayerConfig`]
+    #[test]
+    fn encode_and_decode_default_config() {
+        let mut cfg_context = ConfigContext::default();
+        let resolved_config = LayerFileConfig::default()
+            .generate_config(&mut cfg_context)
+            .expect("Default config should be generated from default 'LayerFileConfig'");
+
+        let encoded = resolved_config.to_env_var().unwrap();
+        let decoded = LayerConfig::from_env_var(encoded).unwrap();
+
+        assert_eq!(decoded, resolved_config);
+    }
+
+    #[test]
+    fn encode_and_decode_advanced_config() {
+        let mut cfg_context = ConfigContext::default();
+
+        // this config includes template variables, so it needs to be rendered first
+        let mut template_engine = Tera::default();
+        template_engine
+            .add_raw_template("main", get_advanced_config().as_str())
+            .unwrap();
+        let rendered = template_engine
+            .render("main", &tera::Context::new())
+            .expect("Tera should render JSON config file contents");
+        let resolved_config = ConfigType::Json
+            .parse(rendered.as_str())
+            .generate_config(&mut cfg_context)
+            .expect("Layer config should be generated from JSON config file contents");
+
+        let encoded = resolved_config.to_env_var().unwrap();
+        let decoded = LayerConfig::from_env_var(encoded).unwrap();
+
+        assert_eq!(decoded, resolved_config);
+    }
+
+    fn get_advanced_config() -> String {
+        r#"
+            {
+                "accept_invalid_certificates": false,
+                "target": {
+                    "path": "pod/test-service-abcdefg-abcd",
+                    "namespace": "default"
+                },
+                "feature": {
+                    "env": true,
+                    "fs": "write",
+                    "network": {
+                        "dns": false,
+                        "incoming": {
+                            "mode": "steal",
+                            "http_filter": {
+                                "header_filter": "x-intercept: {{ get_env(name="USER") }}"
+                            }
+                        },
+                        "outgoing": {
+                            "tcp": true,
+                            "udp": false
+                        }
+                    }
+                }
+            }
+        "#
+        .to_string()
+    }
 }


Test patch: