From 901a35714b6eae1a295201e49763d1896b90606d Mon Sep 17 00:00:00 2001 From: Jonathan Lee <107072447+jj22ee@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:51:00 -0700 Subject: [PATCH] Add `cp -r` support to cp-utility tool (#108) *Issue #, if available:* ADOT SDK instrumentation Docker Images only support `cp -a` copy command so that the [CWAgentOperator can extract](https://github.com/aws/amazon-cloudwatch-agent-operator/blob/main/pkg/instrumentation/nodejs.go#L65) the instrumentation SDK via `cp -a`, but upstream OTel Operator has [changed a few months ago](https://github.com/open-telemetry/opentelemetry-operator/commit/4cd6dcb3350da220eb54df738b4ae82d2168a0c2) to use `cp -r` instead of `-a` to copy files from the OTel SDK Instrumentation Images. Today, OTel Operator is not compatible with some of the ADOT SDKs, and in the future, CWAgent Operator may also change to use `cp -r` as well *Description of changes:* - Copy changes from Java's PR to add `cp -r` support - https://github.com/aws-observability/aws-otel-java-instrumentation/pull/843 - Modify the above `cp -r` support with bug fix implemented in Python's `cp -a` command - https://github.com/aws-observability/aws-otel-python-instrumentation/pull/214#discussion_r1653671950 By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --- tools/cp-utility/src/main.rs | 120 +++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 5 deletions(-) diff --git a/tools/cp-utility/src/main.rs b/tools/cp-utility/src/main.rs index 6e14061..83b69d2 100644 --- a/tools/cp-utility/src/main.rs +++ b/tools/cp-utility/src/main.rs @@ -28,6 +28,8 @@ enum CopyType { SingleFile, /// equivalent to cp -a Archive, + /// equivalent to cp -r + Recursive, } /// Encapsulate a copy operation @@ -42,10 +44,10 @@ struct CopyOperation { /// Parse command line arguments and transform into `CopyOperation` fn parse_args(args: Vec<&str>) -> io::Result { - if !(args.len() == 3 || args.len() == 4 && args[1].eq("-a")) { + if !(args.len() == 3 || (args.len() == 4 && (args[1] == "-a" || args[1] == "-r"))) { return Err(io::Error::new( io::ErrorKind::InvalidInput, - "Invalid parameters. Expected cp [-a] ", + "Invalid parameters. Expected cp [-a | -r] ", )); } @@ -53,7 +55,11 @@ fn parse_args(args: Vec<&str>) -> io::Result { return Ok(CopyOperation { source: PathBuf::from(args[2]), destination: PathBuf::from(args[3]), - copy_type: CopyType::Archive, + copy_type: match args[1] { + "-a" => CopyType::Archive, + "-r" => CopyType::Recursive, + _ => panic!("Invalid option. Expected -a or -r"), + }, }); } @@ -69,10 +75,40 @@ fn do_copy(operation: CopyOperation) -> io::Result<()> { match operation.copy_type { CopyType::Archive => copy_archive(&operation.source, &operation.destination)?, CopyType::SingleFile => fs::copy(&operation.source, &operation.destination).map(|_| ())?, + CopyType::Recursive => copy_recursive(&operation.source, &operation.destination)?, }; Ok(()) } +fn copy_recursive(source: &Path, dest: &Path) -> io::Result<()> { + let mut stack = VecDeque::new(); + stack.push_back((source.to_path_buf(), dest.to_path_buf())); + while let Some((current_source, current_dest)) = stack.pop_back() { + if current_source.is_dir() { + if !current_dest.exists() { + fs::create_dir(¤t_dest)?; + } + for entry in fs::read_dir(current_source)? { + let next_source = entry?.path(); + let next_dest = + current_dest + .clone() + .join(next_source.file_name().ok_or(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid source file", + ))?); + stack.push_back((next_source, next_dest)); + } + } else if current_source.is_symlink() { + // Follow symbolic links as regular files + fs::copy(current_source, current_dest)?; + } else if current_source.is_file() { + fs::copy(current_source, current_dest)?; + } + } + Ok(()) +} + // Execute the recursive type of copy operation fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> { let mut stack = VecDeque::new(); @@ -100,7 +136,6 @@ fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> { fs::copy(current_source, current_dest)?; } } - Ok(()) } @@ -163,7 +198,7 @@ mod tests { fn parser_failure() { // prepare let inputs = vec![ - vec!["cp", "-r", "foo.txt", "bar.txt"], + vec!["cp", "-r", "foo.txt", "bar.txt", "foo1.txt"], vec!["cp", "-a", "param1", "param2", "param3"], vec!["cp", "param1", "param2", "param3"], ]; @@ -177,6 +212,24 @@ mod tests { } } + #[test] + fn parser_correct() { + // prepare + let inputs = vec![ + vec!["cp", "-r", "foo.txt", "bar.txt"], + vec!["cp", "-a", "param1", "param2"], + vec!["cp", "param1", "param2"], + ]; + + for input in inputs.into_iter() { + // act + let result = parse_args(input.clone()); + + // assert + assert!(result.is_ok(), "input should fail {:?}", input); + } + } + #[test] fn test_copy_single() { // prepare @@ -220,6 +273,51 @@ mod tests { assert!(result.is_err()); } + #[test] + fn test_copy_recursive() { + // prepare + let tempdir = tempfile::tempdir().unwrap(); + let test_base = tempdir.path().to_path_buf(); + ["foo", "foo/foo0", "foo/foo1", "foo/bar"] + .iter() + .for_each(|x| create_dir(&test_base, x)); + let files = [ + "foo/file1.txt", + "foo/file2.txt", + "foo/foo1/file3.txt", + "foo/bar/file4.txt", + ]; + files.iter().for_each(|x| create_file(&test_base, x)); + [("foo/symlink1.txt", "./file1.txt")] + .iter() + .for_each(|(x, y)| create_symlink(&test_base, x, y)); + + // act + let recursive_copy = CopyOperation { + copy_type: CopyType::Recursive, + source: test_base.join("foo"), + destination: test_base.join("bar"), + }; + do_copy(recursive_copy).unwrap(); + + // assert + files.iter().for_each(|x| { + assert_same_file( + &test_base.join(x), + &test_base.join(x.replace("foo/", "bar/")), + ) + }); + assert_same_file( + &test_base.join("foo/symlink1.txt"), + &test_base.join("bar/symlink1.txt"), + ); + // recursive copy will treat symlink as a file + assert_recursive_same_link( + &test_base.join("foo/symlink1.txt"), + &test_base.join("bar/symlink1.txt"), + ) + } + #[test] fn test_copy_archive() { // prepare @@ -342,4 +440,16 @@ mod tests { assert_eq!(fs::read_link(source).unwrap(), fs::read_link(dest).unwrap()); } + + fn assert_recursive_same_link(source: &Path, dest: &Path) { + assert!(source.exists()); + assert!(dest.exists()); + assert!(source.is_symlink()); + assert!(dest.is_file()); + + assert_eq!( + fs::read_to_string(source).unwrap(), + fs::read_to_string(dest).unwrap() + ); + } }