diff --git a/foodx_devops_tools/utilities/command.py b/foodx_devops_tools/utilities/command.py index 26b4fc5..f86074b 100644 --- a/foodx_devops_tools/utilities/command.py +++ b/foodx_devops_tools/utilities/command.py @@ -16,6 +16,7 @@ import asyncio import dataclasses +import locale import logging import pathlib import shutil @@ -88,6 +89,10 @@ async def run_async_command( """ Run an external command asynchronously. + Command logging is disabled by default to help prevent accidental + credential leakage to logs when commands may necessarily contain + sensitive data. + Args: command: Command to be executed. enable_logging: Enable command and argument logging (default disabled) @@ -99,15 +104,33 @@ async def run_async_command( # WARNING: logging is _disabled_ by default to prevent "default" # leakage of secrets into logs via command arguments log.debug("command to run, {0}".format(str(command))) + + this_encoding = sys.getfilesystemencoding() + log.debug("sys.getdefaultencoding(), {0}".format(sys.getdefaultencoding())) + log.debug( + "locale.getpreferredencoding(), {0}".format( + locale.getpreferredencoding() + ) + ) + log.debug( + "sys.getfilesystemencoding(), {0}".format(sys.getfilesystemencoding()) + ) this_process = await asyncio.create_subprocess_exec( *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await this_process.communicate() + log.debug( + "run_async_command stdout, {0}".format(stdout.decode(this_encoding)) + ) + log.debug( + "run_async_command stderr, {0}".format(stderr.decode(this_encoding)) + ) result = CapturedStreams( - out=stdout.decode("utf-8"), error=stderr.decode("utf-8") + out=stdout.decode(this_encoding), error=stderr.decode(this_encoding) ) if this_process.returncode != 0: + log.debug(f"command std out, {result.out}") raise CommandError( "External command run did not exit cleanly, {0}".format( result.error diff --git a/tests/ci/unit_tests/utilities/test_command.py b/tests/ci/unit_tests/utilities/test_command.py index 888b698..e18852d 100644 --- a/tests/ci/unit_tests/utilities/test_command.py +++ b/tests/ci/unit_tests/utilities/test_command.py @@ -16,6 +16,7 @@ run_async_command, run_command, ) +from foodx_devops_tools.utilities.exceptions import CommandError class TestRunCommand: @@ -56,3 +57,25 @@ async def test_simple(self, mocker): stderr=asyncio.subprocess.PIPE ) assert result == expected_output + + @pytest.mark.asyncio + async def test_error(self, mocker): + expected_output = CapturedStreams(out="", error="some error") + async_mock = AsyncMock() + async_mock.return_value.returncode = 1 + async_mock.return_value.communicate.return_value = ( + expected_output.out.encode(), + expected_output.error.encode(), + ) + mocker.patch( + "foodx_devops_tools.utilities.command.asyncio" + ".create_subprocess_exec", + side_effect=async_mock, + ) + command = ["something", "--option"] + + with pytest.raises( + CommandError, + match=r"^External command run did " r"not exit cleanly", + ): + await run_async_command(command)