diff --git a/doozerlib/source_modifications.py b/doozerlib/source_modifications.py index 534624bbc..1d50711b5 100644 --- a/doozerlib/source_modifications.py +++ b/doozerlib/source_modifications.py @@ -4,6 +4,7 @@ import requests import yaml +from pathlib import Path from doozerlib.exceptions import DoozerFatalError from doozerlib.exectools import cmd_assert @@ -192,3 +193,40 @@ def act(self, *args, **kwargs): SourceModifierFactory.MODIFICATIONS["command"] = CommandModifier + + +class RemoveModifier(object): + """ A source modifier that supports removing files from the distgit repository. + """ + + def __init__(self, *args, **kwargs): + """ Initialize CommandModifier + :param command: a `str` or `list` of the command with arguments + """ + self.glob = kwargs["glob"] + + def act(self, *args, **kwargs): + """ Run the command + :param context: A context dict. `context.set_env` is a `dict` of env vars to set for command (overriding existing). + """ + context = kwargs["context"] + component = context["component_name"] + distgit_path = Path(context['distgit_path']) + ceiling_dir = Path(kwargs["ceiling_dir"]) + + LOGGER.info("Distgit repo %s: Running 'remove' modification action...", component) + removed = [] + for path in distgit_path.rglob(self.glob): + if not is_in_directory(path, ceiling_dir): + raise PermissionError("Removing a file from a location outside of directory {} is not allowed.".format(ceiling_dir)) + relative_path = str(path.relative_to(distgit_path)) + LOGGER.info("Distgit repo %s: Removing %s", component, relative_path) + path.unlink() + removed.append(relative_path) + if len(removed): + LOGGER.info("Distgit repo %s: %s files have been removed:\n%s", component, len(removed), "\n".join(removed)) + else: + LOGGER.warning("Distgit repo %s: No files matching glob `%s`", component, self.glob) + + +SourceModifierFactory.MODIFICATIONS["remove"] = RemoveModifier diff --git a/tests/test_source_modifications.py b/tests/test_source_modifications.py index 2e1a9fab8..3f1a17809 100644 --- a/tests/test_source_modifications.py +++ b/tests/test_source_modifications.py @@ -8,7 +8,8 @@ import mock import yaml.scanner -from doozerlib.source_modifications import AddModifier, SourceModifierFactory +from doozerlib.source_modifications import (AddModifier, RemoveModifier, + SourceModifierFactory) class SourceModifierFactoryTestCase(unittest.TestCase): @@ -90,5 +91,43 @@ def test_act_failed_with_invalid_yaml(self): modifier.act(ceiling_dir=self.temp_dir, session=session, context=context) +class TestRemoveModifier(unittest.TestCase): + def test_remove_file(self): + distgit_path = pathlib.Path("/path/to/distgit") + modifier = RemoveModifier(glob="**/*.txt", distgit_path=distgit_path) + context = { + "component_name": "foo", + "kind": "Dockerfile", + "content": "whatever", + "distgit_path": distgit_path, + } + with mock.patch.object(pathlib.Path, "rglob") as rglob, mock.patch.object(pathlib.Path, "unlink") as unlink: + rglob.return_value = map(lambda path: distgit_path.joinpath(path), [ + "1.txt", + "a/2.txt", + "b/c/d/e/3.txt", + ]) + modifier.act(context=context, ceiling_dir=str(distgit_path)) + unlink.assert_called() + + def test_remove_file_outside_of_distgit_dir(self): + distgit_path = pathlib.Path("/path/to/distgit") + modifier = RemoveModifier(glob="**/*.txt", distgit_path=distgit_path) + context = { + "component_name": "foo", + "kind": "Dockerfile", + "content": "whatever", + "distgit_path": distgit_path, + } + with mock.patch.object(pathlib.Path, "rglob") as rglob: + rglob.return_value = map(lambda path: pathlib.Path("/some/other/path").joinpath(path), [ + "1.txt", + "a/2.txt", + "b/c/d/e/3.txt", + ]) + with self.assertRaises(PermissionError): + modifier.act(context=context, ceiling_dir=str(distgit_path)) + + if __name__ == "__main__": unittest.main()