Prevent running from a different Kayobe configuration repository
There are various ways in which it is possible to operate Kayobe incorrectly. One example is executing Kayobe from a different Kayobe configuration repository than the one referred to by the KAYOBE_CONFIG_PATH environment variable. While this shouldn't necessarily cause any errors, it may lead to unexpected results if the operator assumes they are operating against the configuration in the current directory. This change adds a validation step that checks for this case and fails the command early if found. Change-Id: I709884bbd7edebf1d409f39c11f293560e987506
This commit is contained in:
parent
c726492e49
commit
8930a3c106
@ -237,7 +237,15 @@ class HookDispatcher(CommandHook):
|
||||
self.logger.debug("Running hooks: %s" % hooks)
|
||||
self.command.run_kayobe_playbooks(parsed_args, hooks)
|
||||
|
||||
def _preflight_checks(self, parsed_args):
|
||||
# NOTE(mgoddard): Currently all commands use KayobeAnsibleMixin, so
|
||||
# should have a config_path attribute, but better to be defensive.
|
||||
config_path = getattr(parsed_args, "config_path", None)
|
||||
if config_path:
|
||||
utils.validate_config_path(config_path)
|
||||
|
||||
def before(self, parsed_args):
|
||||
self._preflight_checks(parsed_args)
|
||||
self.run_hooks(parsed_args, "pre")
|
||||
return parsed_args
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
@ -189,14 +190,12 @@ key3:
|
||||
mock_call.assert_called_once_with(["command", "to", "run"])
|
||||
self.assertIsNone(output)
|
||||
|
||||
@mock.patch("kayobe.utils.open")
|
||||
@mock.patch.object(subprocess, "check_call")
|
||||
def test_run_command_quiet(self, mock_call, mock_open):
|
||||
mock_devnull = mock_open.return_value.__enter__.return_value
|
||||
def test_run_command_quiet(self, mock_call):
|
||||
output = utils.run_command(["command", "to", "run"], quiet=True)
|
||||
mock_call.assert_called_once_with(["command", "to", "run"],
|
||||
stdout=mock_devnull,
|
||||
stderr=mock_devnull)
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
self.assertIsNone(output)
|
||||
|
||||
@mock.patch.object(subprocess, "check_output")
|
||||
@ -332,3 +331,122 @@ key3:
|
||||
finder = utils.EnvironmentFinder('/etc/kayobe', None)
|
||||
self.assertEqual([], finder.ordered())
|
||||
self.assertEqual([], finder.ordered_paths())
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
def test_validate_config_path_kayobe(self, mock_readable, mock_run):
|
||||
mock_run.return_value = b"/path/to/config"
|
||||
utils.validate_config_path("/path/to/config/etc/kayobe")
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
self.assertFalse(mock_readable.called)
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
def test_validate_config_path_not_a_repo(self, mock_readable, mock_run):
|
||||
mock_run.side_effect = subprocess.CalledProcessError(
|
||||
"not a repo", "command")
|
||||
utils.validate_config_path("/path/to/config/etc/kayobe")
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
self.assertFalse(mock_readable.called)
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
def test_validate_config_path_no_git(self, mock_readable, mock_run):
|
||||
mock_run.side_effect = FileNotFoundError("No such file")
|
||||
utils.validate_config_path("/path/to/config/etc/kayobe")
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
self.assertFalse(mock_readable.called)
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_validate_config_path_gitreview(self, mock_read, mock_readable,
|
||||
mock_run):
|
||||
mock_run.return_value = b"/path/to/repo"
|
||||
mock_readable.return_value = {"result": True}
|
||||
mock_read.return_value = """
|
||||
[gerrit]
|
||||
project=openstack/kayobe-config.git
|
||||
"""
|
||||
with self.assertLogs(level=logging.ERROR) as ctx:
|
||||
self.assertRaises(SystemExit,
|
||||
utils.validate_config_path,
|
||||
"/path/to/config/etc/kayobe")
|
||||
exp = ("Executing from within a different Kayobe configuration "
|
||||
"repository is not allowed")
|
||||
assert any(exp in t for t in ctx.output)
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
mock_readable.assert_called_once_with("/path/to/repo/.gitreview")
|
||||
mock_read.assert_called_once_with("/path/to/repo/.gitreview")
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
def test_validate_config_path_no_gitreview(self, mock_readable, mock_run):
|
||||
mock_run.return_value = b"/path/to/repo"
|
||||
mock_readable.return_value = {"result": False}
|
||||
utils.validate_config_path("/path/to/config/etc/kayobe")
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
mock_readable.assert_called_once_with("/path/to/repo/.gitreview")
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_validate_config_path_gitreview_no_gerrit(self, mock_read,
|
||||
mock_readable, mock_run):
|
||||
mock_run.return_value = b"/path/to/repo"
|
||||
mock_readable.return_value = {"result": False}
|
||||
mock_read.return_value = """
|
||||
[foo]
|
||||
bar=baz
|
||||
"""
|
||||
utils.validate_config_path("/path/to/config/etc/kayobe")
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
mock_readable.assert_called_once_with("/path/to/repo/.gitreview")
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_validate_config_path_gitreview_no_project(self, mock_read,
|
||||
mock_readable,
|
||||
mock_run):
|
||||
mock_run.return_value = b"/path/to/repo"
|
||||
mock_readable.return_value = {"result": False}
|
||||
mock_read.return_value = """
|
||||
[gerrit]
|
||||
bar=baz
|
||||
"""
|
||||
utils.validate_config_path("/path/to/config/etc/kayobe")
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
mock_readable.assert_called_once_with("/path/to/repo/.gitreview")
|
||||
|
||||
@mock.patch.object(utils, "run_command")
|
||||
@mock.patch.object(utils, "is_readable_file")
|
||||
@mock.patch.object(utils, "read_file")
|
||||
def test_validate_config_path_gitreview_other_project(self, mock_read,
|
||||
mock_readable,
|
||||
mock_run):
|
||||
mock_run.return_value = b"/path/to/repo"
|
||||
mock_readable.return_value = {"result": False}
|
||||
mock_read.return_value = """
|
||||
[gerrit]
|
||||
project=baz
|
||||
"""
|
||||
utils.validate_config_path("/path/to/config/etc/kayobe")
|
||||
mock_run.assert_called_once_with(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
check_output=True, quiet=True)
|
||||
mock_readable.assert_called_once_with("/path/to/repo/.gitreview")
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
import configparser
|
||||
import glob
|
||||
import graphlib
|
||||
from importlib.metadata import Distribution
|
||||
@ -223,11 +224,10 @@ def run_command(cmd, quiet=False, check_output=False, **kwargs):
|
||||
cmd_string = " ".join(cmd)
|
||||
LOG.debug("Running command: %s", cmd_string)
|
||||
if quiet:
|
||||
with open("/dev/null", "w") as devnull:
|
||||
kwargs["stdout"] = devnull
|
||||
kwargs["stderr"] = devnull
|
||||
subprocess.check_call(cmd, **kwargs)
|
||||
elif check_output:
|
||||
kwargs["stderr"] = subprocess.DEVNULL
|
||||
if not check_output:
|
||||
kwargs["stdout"] = subprocess.DEVNULL
|
||||
if check_output:
|
||||
return subprocess.check_output(cmd, **kwargs)
|
||||
else:
|
||||
subprocess.check_call(cmd, **kwargs)
|
||||
@ -409,3 +409,55 @@ class EnvironmentFinder(object):
|
||||
)
|
||||
result.append(full_path)
|
||||
return result
|
||||
|
||||
|
||||
def _gitreview_is_kayobe_config(gitreview_path):
|
||||
"""Return whether a .gitreview file is for kayobe-config."""
|
||||
config = configparser.ConfigParser()
|
||||
config_string = read_file(gitreview_path)
|
||||
config.read_string(config_string)
|
||||
gerrit_project = config.get('gerrit', 'project')
|
||||
if not gerrit_project:
|
||||
return False
|
||||
gerrit_project = os.path.basename(gerrit_project)
|
||||
gerrit_project = os.path.splitext(gerrit_project)[0]
|
||||
if gerrit_project == 'kayobe-config':
|
||||
return True
|
||||
|
||||
|
||||
def validate_config_path(config_path):
|
||||
"""Validate the Kayobe configuration path.
|
||||
|
||||
Check whether we are executing from inside a Kayobe configuration
|
||||
repository, and if so, assert that matches the Kayobe configuration path
|
||||
defined in CLI args or environment variables.
|
||||
|
||||
Exit 1 if validation fails.
|
||||
|
||||
:param config_path: Kayobe configuration path or None.
|
||||
"""
|
||||
assert config_path
|
||||
|
||||
try:
|
||||
cmd = ["git", "rev-parse", "--show-toplevel"]
|
||||
repo_root = run_command(cmd, quiet=True, check_output=True)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
# FileNotFoundError: git probably not installed.
|
||||
# CalledProcessError: probably not in a git repository.
|
||||
return
|
||||
|
||||
repo_root = repo_root.decode().strip()
|
||||
if config_path:
|
||||
repo_config_path = os.path.join(repo_root, "etc", "kayobe")
|
||||
if repo_config_path == os.path.realpath(config_path):
|
||||
return
|
||||
|
||||
# Paths did not match. Check that repo_root does not look like a Kayobe
|
||||
# configuration repo.
|
||||
gitreview_path = os.path.join(repo_root, ".gitreview")
|
||||
result = is_readable_file(gitreview_path)
|
||||
if result["result"]:
|
||||
if _gitreview_is_kayobe_config(gitreview_path):
|
||||
LOG.error("Executing from within a different Kayobe configuration "
|
||||
"repository is not allowed")
|
||||
sys.exit(1)
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds validation to protect against executing Kayobe from within a different
|
||||
Kayobe configuration repository than the one referred to by environment
|
||||
variables (e.g. ``KAYOBE_CONFIG_PATH``) or CLI arguments (e.g.
|
||||
``--config-path``).
|
Loading…
x
Reference in New Issue
Block a user