Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(actors): Allow boolean-like values for SKIP_DRY #538

Merged
merged 7 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath(".."))
import kingpin
from kingpin import version as kingpin_version

# We need sphinx 1.2+ for some of our used features
Expand Down Expand Up @@ -69,7 +68,7 @@

# General information about the project.
project = u'Kingpin'
copyright = u'2015, Nextdoor'
copyright = u'2025, Nextdoor'
author = u'Nextdoor Engineering'

# The version info for the project you're documenting, acts as replacement for
Expand Down Expand Up @@ -112,7 +111,7 @@

# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
html_theme = "sphinx_rtd_theme" # default: 'alabaster'

# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
Expand Down Expand Up @@ -271,7 +270,7 @@
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Kingpin', u'Kingpin Documentation',
author, 'Kingpin', 'One line description of project.',
author, 'Kingpin', 'Deployment Automation Engine',
'Miscellaneous'),
]

Expand All @@ -294,8 +293,3 @@
'boto': ('http://boto.cloudhackers.com/en/latest/', None),
'boto3': ('http://boto3.readthedocs.io/en/latest/', None)
}

# Force the RTD theme for all builds
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
48 changes: 14 additions & 34 deletions kingpin/actors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ def __init__(
Args:
desc: (Str) description of the action being executed.
options: (Dict) Key/Value pairs that have the options
for this action. Values should be primitives.
for this action. Values should be primitives.
dry: (Bool) or not this Actor will actually make changes.
warn_on_failure: (Bool) Whether this actor ignores its return
value and always succeeds (but warns).
value and always succeeds (but warns).
condition: (Bool) Whether to run this actor.
init_context: (Dict) Key/Value pairs used at instantiation
time to replace {KEY} strings in the actor definition.
Expand Down Expand Up @@ -250,9 +250,9 @@ def _validate_options(self):
# failure get caught below.
if expected_type is bool:
try:
value = self.str2bool(value, strict=True)
value = utils.str2bool(value, strict=True)
self._options[opt] = value
except exceptions.InvalidOptions as e:
except RuntimeError as e:
self.log.warning(e)

if not (value is None or isinstance(value, expected_type)):
Expand Down Expand Up @@ -350,29 +350,6 @@ def timeout(self, f, *args, **kwargs):

raise gen.Return(ret)

def str2bool(self, v, strict=False):
"""Returns a Boolean from a variety of inputs.

Args:
value: String/Bool
strict: Whether or not to _only_ convert the known words into booleans, or whether to allow "any" word to be considered True other than the known False words.

Returns:
A boolean
"""
false = ("no", "false", "f", "0")
true = ("yes", "true", "t", "1")

string = str(v).lower()

if strict:
if string not in true and string not in false:
raise exceptions.InvalidOptions(
"Expected [%s, %s] but got: %s" % (true, false, string)
)

return string not in false
Comment on lines -353 to -374
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to kingpin.utils for use in non-actor classes.


def _check_condition(self):
"""Check if specified condition allows this actor to run.

Expand All @@ -381,8 +358,11 @@ def _check_condition(self):
the value of self._condition is a string "False" or string "0".
"""

check = self.str2bool(self._condition)
self.log.debug("Condition %s evaluates to %s" % (self._condition, check))
try:
check = utils.str2bool(self._condition)
self.log.debug("Condition %s evaluates to %s" % (self._condition, check))
except ValueError as e:
raise exceptions.InvalidOptions(e.args) from e
Comment on lines +361 to +365
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added try-except to convert the raised error to maintain correct raise types outside function.

return check

def _fill_in_contexts(self, context={}, strict=True, remove_escape_sequence=True):
Expand All @@ -395,7 +375,7 @@ def _fill_in_contexts(self, context={}, strict=True, remove_escape_sequence=True

Args:
strict: bool whether or not to allow missing context keys to be
skipped over.
skipped over.

Raises:
exceptions.InvalidOptions
Expand Down Expand Up @@ -460,10 +440,10 @@ def get_orgchart(self, parent=""):
actors that are called.

orgchart object:
id: unique string identifying this actor's instance.
class: kingpin class name
desc: actor description
parent_id: organizational relationship. Same as `id` above.
id: unique string identifying this actor's instance.
class: kingpin class name
desc: actor description
parent_id: organizational relationship. Same as `id` above.
"""

return [
Expand Down
18 changes: 0 additions & 18 deletions kingpin/actors/test/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
from kingpin.constants import REQUIRED, STATE


__author__ = "Matt Wise <[email protected]>"


class FakeHTTPClientClass(object):
"""Fake HTTPClient object for testing"""

Expand Down Expand Up @@ -280,21 +277,6 @@ def test_execute(self):
res = yield self.actor.execute()
self.assertEqual(res, True)

def test_str2bool(self):
self.assertEqual(True, self.actor.str2bool("true"))
self.assertEqual(True, self.actor.str2bool("junk text"))
self.assertEqual(True, self.actor.str2bool("1"))
self.assertEqual(True, self.actor.str2bool(True))
self.assertEqual(False, self.actor.str2bool("false"))
self.assertEqual(False, self.actor.str2bool("0"))
self.assertEqual(False, self.actor.str2bool(False))

def test_str2bool_strict(self):
self.assertEqual(True, self.actor.str2bool("true"))
self.assertEqual(False, self.actor.str2bool(False))
with self.assertRaises(exceptions.InvalidOptions):
self.actor.str2bool("Junk", strict=True)

@testing.gen_test
def test_check_condition(self):
conditions = {
Expand Down
9 changes: 8 additions & 1 deletion kingpin/bin/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,15 @@ def main():

sys.exit(0)

skip_dry = False
try:
skip_dry = utils.str2bool(os.environ.get("SKIP_DRY", "False"), strict=True)
except ValueError:
log.warning("SKIP_DRY is not a valid boolean-like. Defaulting to False.")
pass

# Begin doing real stuff!
if os.environ.get("SKIP_DRY", False):
if skip_dry:
log.warning("")
log.warning("*** You have disabled the dry run.")
log.warning("*** Execution will begin with no expectation of success.")
Expand Down
9 changes: 9 additions & 0 deletions kingpin/bin/test/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ def test_main_with_skip_dry(self):
self.kingpin_bin_deploy.main()
mock_get_main_actor.assert_called()

@mock.patch("sys.argv", ["kingpin"])
@mock.patch.dict(os.environ, {"SKIP_DRY": "not-a-boolean-like-string"})
def test_main_with_skip_dry_invalid(self):
self._import_kingpin_bin_deploy()
with mock.patch("kingpin.bin.deploy.get_main_actor") as mock_get_main_actor:
mock_get_main_actor.return_value = Sleep(options={"sleep": 0.1}, dry=True)
self.kingpin_bin_deploy.main()
mock_get_main_actor.assert_called()

@mock.patch("sys.argv", ["kingpin"])
def test_main_with_bad_runner(self):
self._import_kingpin_bin_deploy()
Expand Down
15 changes: 15 additions & 0 deletions kingpin/test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,21 @@ def raises_exc():
self.assertEqual(1, logger().debug.call_count)
logger().debug.assert_called_with(mock.ANY, exc_info=1)

def test_str2bool(self):
self.assertEqual(True, utils.str2bool("true"))
self.assertEqual(True, utils.str2bool("junk text"))
self.assertEqual(True, utils.str2bool("1"))
self.assertEqual(True, utils.str2bool(True))
self.assertEqual(False, utils.str2bool("false"))
self.assertEqual(False, utils.str2bool("0"))
self.assertEqual(False, utils.str2bool(False))

def test_str2bool_strict(self):
self.assertEqual(True, utils.str2bool("true"))
self.assertEqual(False, utils.str2bool(False))
with self.assertRaises(ValueError):
utils.str2bool("Junk", strict=True)


class TestSetupRootLoggerUtils(unittest.TestCase):
def setUp(self):
Expand Down
31 changes: 25 additions & 6 deletions kingpin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
from kingpin import exceptions


__author__ = "Matt Wise ([email protected])"

log = logging.getLogger(__name__)

# Constants for some of the utilities below
Expand Down Expand Up @@ -277,10 +275,9 @@ def populate_with_tokens(
left_wrapper: the character to use as the START of a token
right_wrapper: the character to use as the END of a token
strict: (bool) whether or not to make sure all tokens were replaced
escape_sequence: character string to use as the escape sequence for
left and right wrappers
remove_escape_sequence: (bool) whether or not to remove the escape
sequence if it found. For example \\%FOO\\% would turn into %FOO%.
escape_sequence: character string to use as the escape sequence for left and right wrappers
remove_escape_sequence: (bool) whether or not to remove the escape sequence if it found. For example \\%FOO\\% would turn into %FOO%.

Example:
export ME=biz

Expand Down Expand Up @@ -499,3 +496,25 @@ def diff_dicts(dict1, dict2):
dict2 = [line.replace("u'", "'") for line in dict2]

return "\n".join(difflib.unified_diff(dict1, dict2, n=2))


def str2bool(v, strict=False) -> bool:
"""Returns a Boolean from a variety of inputs.

Args:
value: String/Bool
strict: Whether or not to _only_ convert the known words into booleans, or whether to allow "any" word to be considered True other than the known False words.

Returns:
A boolean
"""
false = ("no", "false", "f", "0")
true = ("yes", "true", "t", "1")

string = str(v).lower()

if strict:
if string not in true and string not in false:
raise ValueError(f"Expected [{true}, {false}] but got: {string}")

return string not in false
Comment on lines +501 to +520
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From deleted function above.