diff --git a/fact/src/host_scanner.rs b/fact/src/host_scanner.rs index b5252601..6594a74b 100644 --- a/fact/src/host_scanner.rs +++ b/fact/src/host_scanner.rs @@ -105,6 +105,7 @@ pub struct HostScanner { metrics: HostScannerMetrics, paths_globset: GlobSet, + paths_patterns: Vec, } impl HostScanner { @@ -119,9 +120,8 @@ impl HostScanner { let kernel_inode_map = RefCell::new(bpf.take_inode_map()?); let inode_map = RefCell::new(InodeMap::new()); let (tx, output) = mpsc::channel(100); - let paths_globset = HostScanner::build_globset(paths.borrow().as_slice())?; - let host_scanner = HostScanner { + let mut host_scanner = HostScanner { kernel_inode_map, inode_map, paths, @@ -130,18 +130,26 @@ impl HostScanner { tx, introspection, metrics, - paths_globset, + paths_globset: GlobSet::empty(), + paths_patterns: Vec::new(), }; + host_scanner.reload_paths_config()?; + // Run an initial scan to fill in the inode map host_scanner.scan()?; Ok((host_scanner, output)) } - fn build_globset(paths: &[PathBuf]) -> anyhow::Result { + fn reload_paths_config(&mut self) -> anyhow::Result<()> { + let paths = self.paths.borrow(); let mut builder = GlobSetBuilder::new(); + let mut patterns = Vec::with_capacity(paths.len()); + for p in paths.iter() { + patterns.push(host_info::prepend_host_mount(p)); + let Some(glob_str) = p.to_str() else { bail!("failed to convert path {} to string", p.display()); }; @@ -152,23 +160,24 @@ impl HostScanner { .unwrap(), ); } - Ok(builder.build()?) + + self.paths_globset = builder.build()?; + self.paths_patterns = patterns; + + Ok(()) } fn scan(&self) -> anyhow::Result<()> { info!("Host scan started"); let start = Instant::now(); self.metrics.scan_inc(ScanLabels::Scans); - let config = self.paths.borrow(); // Cleanup any items that are either: // * Not configured to be monitored anymore. // * Are configured to be monitored but no longer are found in // the file system. self.inode_map.borrow_mut().retain(|inode, path| { - if config.iter().any(|prefix| path.starts_with(prefix)) - && host_info::prepend_host_mount(path).exists() - { + if self.paths_globset.is_match(&path) && host_info::prepend_host_mount(path).exists() { true } else { let _ = self.kernel_inode_map.borrow_mut().remove(inode); @@ -177,9 +186,8 @@ impl HostScanner { } }); - for pattern in self.paths.borrow().iter() { - let path = host_info::prepend_host_mount(pattern); - self.scan_inner(&path)?; + for path in &self.paths_patterns { + self.scan_inner(path)?; } let duration = start.elapsed(); self.metrics.scan_duration.observe(duration.as_secs_f64()); @@ -231,6 +239,18 @@ impl HostScanner { Ok(()) } + fn scan_partial(&self, path: &Path) -> anyhow::Result<()> { + for pattern in self + .paths_globset + .matches(path) + .iter() + .map(|index| &self.paths_patterns[*index]) + { + self.scan_inner(pattern)?; + } + Ok(()) + } + fn update_entry(&self, path: &Path, metadata: &Metadata) -> anyhow::Result<()> { let inode = inode_key_t { inode: metadata.st_ino(), @@ -448,12 +468,8 @@ You can increase this limit with: } /// Handle a mount being modified in a monitored directory. - /// - /// This should really do a partial scan of the directory where the - /// mount is being changed, but we don't have an easy way to do that - /// at the moment, so we trigger a full scan instead. - fn handle_mount_event(&self) { - if let Err(e) = self.scan() { + fn handle_mount_event(&self, event: &Event) { + if let Err(e) = self.scan_partial(event.get_host_path()) { warn!("Host scan failed: {e:?}"); } } @@ -511,12 +527,6 @@ You can increase this limit with: warn!("Failed to handle creation event: {e}"); } - // Handle mount events and move on. - if event.is_mount_related() { - self.handle_mount_event(); - continue; - } - if let Some(host_path) = self.get_host_path(Some(event.get_inode())) { self.metrics.scan_inc(ScanLabels::InodeHit); event.set_host_path(host_path); @@ -527,6 +537,12 @@ You can increase this limit with: event.set_old_host_path(host_path); } + // Handle mount events and move on. + if event.is_mount_related() { + self.handle_mount_event(&event); + continue; + } + // Remove inode from the map if event.is_deletion() { self.handle_unlink_event(&event); @@ -568,7 +584,7 @@ You can increase this limit with: } _ = scan_trigger.notified() => self.scan()?, _ = self.paths.changed() => { - self.paths_globset = HostScanner::build_globset(self.paths.borrow().as_slice())?; + self.reload_paths_config()?; self.scan()?; } } diff --git a/tests/test_wildcard.py b/tests/test_wildcard.py index c2a2f01c..6559ec57 100644 --- a/tests/test_wildcard.py +++ b/tests/test_wildcard.py @@ -11,6 +11,15 @@ from server import EventServer +def reload_config( + fact: docker.models.containers.Container, config: dict, config_file: str +): + with open(config_file, 'w') as f: + yaml.dump(config, f) + fact.kill('SIGHUP') + sleep(0.1) + + @pytest.fixture def wildcard_config( fact: docker.models.containers.Container, @@ -23,12 +32,22 @@ def wildcard_config( f'{monitored_dir}/*.conf', f'{monitored_dir}/**/test-*.log', ] - with open(config_file, 'w') as f: - yaml.dump(config, f) - # reload the config - fact.kill('SIGHUP') - sleep(0.1) + reload_config(fact, config, config_file) + return config, config_file + + +@pytest.fixture +def partial_path_match_wildcard( + fact: docker.models.containers.Container, + fact_config: tuple[dict, str], + ignored_dir: str, +): + config, config_file = fact_config + partial_dir = ignored_dir.rsplit('-', 1)[0] + config['paths'] = [f'{partial_dir}*/**/*', f'{partial_dir}*'] + + reload_config(fact, config, config_file) return config, config_file @@ -197,3 +216,25 @@ def test_multiple_patterns( ] server.wait_events(events) + + +def test_partial_dir_pattern( + partial_path_match_wildcard: tuple[dict, str], + ignored_dir: str, + server: EventServer, +): + process = Process.from_proc() + file = os.path.join(ignored_dir, 'file.txt') + with open(file, 'w') as f: + f.write('This is a test') + + server.wait_events( + [ + Event( + process=process, + event_type=EventType.CREATION, + file=file, + host_path=file, + ) + ] + )