Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fs path re-mapping #2556

Merged
merged 18 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog.d/2068.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Add fs mapping, under `feature.fs.mapping` now it's possible to specify regex match and replace for paths while running mirrord exec.

Example:

```toml
[feature.fs.mapping]
"/var/app/temp" = "/tmp" # Will replace all calls to read/write/scan for "/var/app/temp/sample.txt" to "/tmp/sample.txt"
"/var/app/.cache" = "/workspace/mirrord$0" # Will replace "/var/app/.cache/sample.txt" to "/workspace/mirrord/var/app/.cache/sample.txt" see [Regex::replace](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace)
DmitryDodzin marked this conversation as resolved.
Show resolved Hide resolved
```
13 changes: 12 additions & 1 deletion mirrord-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"additionalProperties": false,
"definitions": {
"AdvancedFsUserConfig": {
"description": "Allows the user to specify the default behavior for file operations:\n\n1. `\"read\"` - Read from the remote file system (default) 2. `\"write\"` - Read/Write from the remote file system. 3. `\"local\"` - Read from the local file system. 4. `\"localwithoverrides\"` - perform fs operation locally, unless the path matches a pre-defined or user-specified exception.\n\n> Note: by default, some paths are read locally or remotely, regardless of the selected FS mode. > This is described in further detail below.\n\nBesides the default behavior, the user can specify behavior for specific regex patterns. Case insensitive.\n\n1. `\"read_write\"` - List of patterns that should be read/write remotely. 2. `\"read_only\"` - List of patterns that should be read only remotely. 3. `\"local\"` - List of patterns that should be read locally. 4. `\"not_found\"` - List of patters that should never be read nor written. These files should be treated as non-existent.\n\nThe logic for choosing the behavior is as follows:\n\n1. Check if one of the patterns match the file path, do the corresponding action. There's no specified order if two lists match the same path, we will use the first one (and we do not guarantee what is first).\n\n**Warning**: Specifying the same path in two lists is unsupported and can lead to undefined behaviour.\n\n2. There are pre-defined exceptions to the set FS mode. 1. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_local_by_default.rs) are read locally by default. 2. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_remote_by_default.rs) are read remotely by default when the mode is `localwithoverrides`. 3. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/not_found_by_default.rs) under the running user's home directory will not be found by the application when the mode is not `local`.\n\nIn order to override that default setting for a path, or a pattern, include it the appropriate pattern set from above. E.g. in order to read files under `/etc/` remotely even though it is covered by [the set of patterns that are read locally by default](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_local_by_default.rs), add `\"^/etc/.\"` to the `read_only` set.\n\n3. If none of the above match, use the default behavior (mode).\n\nFor more information, check the file operations [technical reference](https://mirrord.dev/docs/reference/fileops/).\n\n```json { \"feature\": { \"fs\": { \"mode\": \"write\", \"read_write\": \".+\\\\.json\" , \"read_only\": [ \".+\\\\.yaml\", \".+important-file\\\\.txt\" ], \"local\": [ \".+\\\\.js\", \".+\\\\.mjs\" ], \"not_found\": [ \"\\\\.config/gcloud\" ] } } } ```",
"description": "Allows the user to specify the default behavior for file operations:\n\n1. `\"read\"` - Read from the remote file system (default) 2. `\"write\"` - Read/Write from the remote file system. 3. `\"local\"` - Read from the local file system. 4. `\"localwithoverrides\"` - perform fs operation locally, unless the path matches a pre-defined or user-specified exception.\n\n> Note: by default, some paths are read locally or remotely, regardless of the selected FS mode. > This is described in further detail below.\n\nBesides the default behavior, the user can specify behavior for specific regex patterns. Case insensitive.\n\n1. `\"read_write\"` - List of patterns that should be read/write remotely. 2. `\"read_only\"` - List of patterns that should be read only remotely. 3. `\"local\"` - List of patterns that should be read locally. 4. `\"not_found\"` - List of patters that should never be read nor written. These files should be treated as non-existent. 4. `\"mapping\"` - Map of patterns and their corresponding replacers. The replacement happens before any specific behavior as defined above or mode (uses [`Regex::replace`](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace))\n\nThe logic for choosing the behavior is as follows:\n\n1. Check agains \"mapping\" if path needs to be replaced, if matched then continue to next step with new path after replacements otherwise continue as usual. 2. Check if one of the patterns match the file path, do the corresponding action. There's no specified order if two lists match the same path, we will use the first one (and we do not guarantee what is first).\n\n**Warning**: Specifying the same path in two lists is unsupported and can lead to undefined behaviour.\n\n3. There are pre-defined exceptions to the set FS mode. 1. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_local_by_default.rs) are read locally by default. 2. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_remote_by_default.rs) are read remotely by default when the mode is `localwithoverrides`. 3. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/not_found_by_default.rs) under the running user's home directory will not be found by the application when the mode is not `local`.\n\nIn order to override that default setting for a path, or a pattern, include it the appropriate pattern set from above. E.g. in order to read files under `/etc/` remotely even though it is covered by [the set of patterns that are read locally by default](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_local_by_default.rs), add `\"^/etc/.\"` to the `read_only` set.\n\n4. If none of the above match, use the default behavior (mode).\n\nFor more information, check the file operations [technical reference](https://mirrord.dev/docs/reference/fileops/).\n\n```json { \"feature\": { \"fs\": { \"mode\": \"write\", \"read_write\": \".+\\\\.json\" , \"read_only\": [ \".+\\\\.yaml\", \".+important-file\\\\.txt\" ], \"local\": [ \".+\\\\.js\", \".+\\\\.mjs\" ], \"not_found\": [ \"\\\\.config/gcloud\" ] } } } ```",
"type": "object",
"properties": {
"local": {
Expand All @@ -166,6 +166,17 @@
}
]
},
"mapping": {
"title": "feature.fs.mapping {#feature-fs-mapping}",
"description": "Specify map of patterns that if matched will replace the path according to specification.\n\n*Capture groups are allowed.*\n\nExample: ```json { \"^/home/(?<user>\\S+)/dev/tomcat\": \"/etc/tomcat\" \"^/home/(?<user>\\S+)/dev/config/(?<app>\\S+)\": \"/mnt/configs/${user}-$app\" } ``` Will do the next replacements for any io operaton\n\n`/home/johndoe/dev/tomcat/context.xml` => `/etc/tomcat/context.xml` `/home/johndoe/dev/config/api/app.conf` => `/mnt/configs/johndoe-api/app.conf`",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "string"
}
},
"mode": {
"title": "feature.fs.mode {#feature-fs-mode}",
"anyOf": [
Expand Down
2 changes: 2 additions & 0 deletions mirrord/config/src/feature/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl MirrordConfig for FsUserConfig {
.source_value(context)
.transpose()?,
not_found: None,
mapping: None,
},
FsUserConfig::Advanced(advanced) => advanced.generate_config(context)?,
};
Expand Down Expand Up @@ -117,6 +118,7 @@ impl MirrordToggleableConfig for FsUserConfig {
read_only,
local,
not_found: None,
mapping: None,
})
}
}
Expand Down
32 changes: 29 additions & 3 deletions mirrord/config/src/feature/fs/advanced.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use mirrord_analytics::{AnalyticValue, CollectAnalytics};
use mirrord_config_derive::MirrordConfig;
use schemars::JsonSchema;
Expand Down Expand Up @@ -31,17 +33,21 @@ use crate::{
/// 3. `"local"` - List of patterns that should be read locally.
/// 4. `"not_found"` - List of patters that should never be read nor written. These files should be
/// treated as non-existent.
/// 4. `"mapping"` - Map of patterns and their corresponding replacers. The replacement happens before any specific behavior as defined above or mode (uses [`Regex::replace`](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace))
///
/// The logic for choosing the behavior is as follows:
///
/// 1. Check if one of the patterns match the file path, do the corresponding action. There's
///
/// 1. Check agains "mapping" if path needs to be replaced, if matched then continue to next step
/// with new path after replacements otherwise continue as usual.
/// 2. Check if one of the patterns match the file path, do the corresponding action. There's
/// no specified order if two lists match the same path, we will use the first one (and we
/// do not guarantee what is first).
///
/// **Warning**: Specifying the same path in two lists is unsupported and can lead to undefined
/// behaviour.
///
/// 2. There are pre-defined exceptions to the set FS mode.
/// 3. There are pre-defined exceptions to the set FS mode.
/// 1. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_local_by_default.rs)
/// are read locally by default.
/// 2. Paths that match [the patterns defined here](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_remote_by_default.rs)
Expand All @@ -55,7 +61,7 @@ use crate::{
/// though it is covered by [the set of patterns that are read locally by default](https://github.com/metalbear-co/mirrord/tree/latest/mirrord/layer/src/file/filter/read_local_by_default.rs),
/// add `"^/etc/."` to the `read_only` set.
///
/// 3. If none of the above match, use the default behavior (mode).
/// 4. If none of the above match, use the default behavior (mode).
///
/// For more information, check the file operations
/// [technical reference](https://mirrord.dev/docs/reference/fileops/).
Expand Down Expand Up @@ -106,6 +112,25 @@ pub struct FsConfig {
///
/// Specify file path patterns that if matched will be treated as non-existent.
pub not_found: Option<VecOrSingle<String>>,

/// ### feature.fs.mapping {#feature-fs-mapping}
///
/// Specify map of patterns that if matched will replace the path according to specification.
///
/// *Capture groups are allowed.*
///
/// Example:
/// ```json
/// {
/// "^/home/(?<user>\S+)/dev/tomcat": "/etc/tomcat"
/// "^/home/(?<user>\S+)/dev/config/(?<app>\S+)": "/mnt/configs/${user}-$app"
/// }
/// ```
/// Will do the next replacements for any io operaton
///
/// `/home/johndoe/dev/tomcat/context.xml` => `/etc/tomcat/context.xml`
/// `/home/johndoe/dev/config/api/app.conf` => `/mnt/configs/johndoe-api/app.conf`
pub mapping: Option<HashMap<String, String>>,
}

impl MirrordToggleableConfig for AdvancedFsUserConfig {
Expand All @@ -127,6 +152,7 @@ impl MirrordToggleableConfig for AdvancedFsUserConfig {
read_only,
local,
not_found: None,
mapping: None,
})
}
}
Expand Down
18 changes: 15 additions & 3 deletions mirrord/layer/src/detour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use core::{
convert,
ops::{FromResidual, Residual, Try},
};
use std::{cell::RefCell, ops::Deref, os::unix::prelude::*, path::PathBuf, sync::OnceLock};
use std::{
cell::RefCell, ffi::CString, ops::Deref, os::unix::prelude::*, path::PathBuf, sync::OnceLock,
};

#[cfg(target_os = "macos")]
use libc::c_char;
Expand Down Expand Up @@ -150,10 +152,10 @@ pub(crate) enum Bypass {
FileOperationInMirrordBinTempDir(*const c_char),

/// File [`PathBuf`] should be ignored (used for tests).
IgnoredFile(PathBuf),
IgnoredFile(CString),

/// Some operations only handle absolute [`PathBuf`]s.
RelativePath(PathBuf),
RelativePath(CString),

/// Started mirrord with [`FsModeConfig`](mirrord_config::feature::fs::mode::FsModeConfig) set
/// to [`FsModeConfig::Read`](mirrord_config::feature::fs::FsModeConfig::Read), but
Expand Down Expand Up @@ -209,6 +211,16 @@ pub(crate) enum Bypass {
LocalDns,
}

impl Bypass {
pub fn relative_path(path: impl Into<Vec<u8>>) -> Self {
Bypass::RelativePath(CString::new(path).expect("Should be CStringable"))
}

pub fn ignored_file(path: impl Into<Vec<u8>>) -> Self {
Bypass::IgnoredFile(CString::new(path).expect("Should be CStringable"))
}
}

/// [`ControlFlow`](std::ops::ControlFlow)-like enum to be used by hooks.
///
/// Conversion from `Result`:
Expand Down
1 change: 1 addition & 0 deletions mirrord/layer/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use mirrord_protocol::file::{GetDEnts64Request, GetDEnts64Response};

pub(crate) mod filter;
pub(crate) mod hooks;
pub(crate) mod mapper;
pub(crate) mod open_dirs;
pub(crate) mod ops;

Expand Down
10 changes: 5 additions & 5 deletions mirrord/layer/src/file/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ impl FileFilter {
local,
mode,
not_found,
..
} = fs_config;

let read_write =
Expand Down Expand Up @@ -399,12 +400,12 @@ mod tests {
local,
not_found,
mode,
mapping: None,
};

let file_filter = FileFilter::new(fs_config);

let res =
file_filter.continue_or_bypass_with(path, write, || Bypass::IgnoredFile("".into()));
let res = file_filter.continue_or_bypass_with(path, write, || Bypass::ignored_file(""));
println!("filter result: {res:?}");
assert_eq!(res.kind(), expected);
}
Expand Down Expand Up @@ -439,8 +440,7 @@ mod tests {

let file_filter = FileFilter::new(fs_config);

let res =
file_filter.continue_or_bypass_with(path, write, || Bypass::IgnoredFile("".into()));
let res = file_filter.continue_or_bypass_with(path, write, || Bypass::ignored_file(""));
println!("filter result: {res:?}");

assert_eq!(res.kind(), expected);
Expand All @@ -464,7 +464,7 @@ mod tests {
#[case("/root/.nuget/packages/microsoft.azure.amqp", DetourKind::Success)]
fn not_found_set(#[case] path: &str, #[case] expected: DetourKind) {
let filter = FileFilter::new(Default::default());
let res = filter.continue_or_bypass_with(path, false, || Bypass::IgnoredFile("".into()));
let res = filter.continue_or_bypass_with(path, false, || Bypass::ignored_file(""));
println!("filter result: {res:?}");

assert_eq!(res.kind(), expected);
Expand Down
Loading
Loading