Skip to content

Fix configurations #253

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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

[1.27.3]: https://github.com/chaostoolkit/chaostoolkit-lib/compare/1.27.2...1.27.3

### Fix

- Fix bug in load_configurations

### Changed

- Ensure experiment level controls are only played once
Expand Down
2 changes: 1 addition & 1 deletion chaoslib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"convert_vars",
"PayloadEncoder",
]
__version__ = "1.27.3"
__version__ = "1.29.0"


def substitute(
Expand Down
15 changes: 15 additions & 0 deletions chaoslib/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,45 +35,57 @@ def ensure_activity_is_valid(activity: Activity): # noqa: C901

In all failing cases, raises :exc:`InvalidActivity`.
"""
logger.info("ensuring activity is valid 1")
if not activity:
raise InvalidActivity("empty activity is no activity")

logger.info("ensuring activity is valid 2")
# when the activity is just a ref, there is little to validate
ref = activity.get("ref")
logger.info("ensuring activity is valid 3")
if ref is not None:
if not isinstance(ref, str) or ref == "":
raise InvalidActivity("reference to activity must be non-empty strings")
return

logger.info("ensuring activity is valid 4")
activity_type = activity.get("type")
if not activity_type:
raise InvalidActivity("an activity must have a type")

logger.info("ensuring activity is valid 5")
if activity_type not in ("probe", "action"):
raise InvalidActivity(f"'{activity_type}' is not a supported activity type")

logger.info("ensuring activity is valid 6")
if not activity.get("name"):
raise InvalidActivity("an activity must have a name")

logger.info("ensuring activity is valid 7")
provider = activity.get("provider")
if not provider:
raise InvalidActivity("an activity requires a provider")

logger.info("ensuring activity is valid 8")
provider_type = provider.get("type")
if not provider_type:
raise InvalidActivity("a provider must have a type")

logger.info("ensuring activity is valid 9")
if provider_type not in ("python", "process", "http"):
raise InvalidActivity(f"unknown provider type '{provider_type}'")

logger.info("ensuring activity is valid 10")
if not activity.get("name"):
raise InvalidActivity("activity must have a name (cannot be empty)")

logger.info("ensuring activity is valid 11")
timeout = activity.get("timeout")
if timeout is not None:
if not isinstance(timeout, numbers.Number):
raise InvalidActivity("activity timeout must be a number")

logger.info("ensuring activity is valid 12")
pauses = activity.get("pauses")
if pauses is not None:
before = pauses.get("before")
Expand All @@ -83,16 +95,19 @@ def ensure_activity_is_valid(activity: Activity): # noqa: C901
if after is not None and not isinstance(after, numbers.Number):
raise InvalidActivity("activity after pause must be a number")

logger.info("ensuring activity is valid 13")
if "background" in activity:
if not isinstance(activity["background"], bool):
raise InvalidActivity("activity background must be a boolean")

logger.info("ensuring activity is valid 14")
if provider_type == "python":
validate_python_activity(activity)
elif provider_type == "process":
validate_process_activity(activity)
elif provider_type == "http":
validate_http_activity(activity)
logger.info("ensuring activity is valid 15")


def run_activities(
Expand Down
24 changes: 14 additions & 10 deletions chaoslib/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def load_configuration(
conf = {}

for (key, value) in config_info.items():
# ----------------FIX----------------
if extra_vars.get(key):
value = extra_vars[key]
del extra_vars[key]
# ------------------------------------
if isinstance(value, dict) and "type" in value:
if value["type"] == "env":
env_key = value["key"]
Expand Down Expand Up @@ -148,29 +153,28 @@ def load_dynamic_configuration(
# from elsewhere
from chaoslib.activity import run_activity

conf = {}
secrets = secrets or {}

# output = None
had_errors = False
logger.debug("Loading dynamic configuration...")
for (key, value) in config.items():
if not (isinstance(value, dict) and value.get("type") == "probe"):
conf[key] = config.get(key, value)
config[key] = config.get(key, value)
continue

# we have a dynamic config
name = value.get("name")
provider_type = value["provider"]["type"]
value["provider"]["secrets"] = deepcopy(secrets)
try:
output = run_activity(value, conf, secrets)
except Exception:
output = run_activity(value, config, secrets)
except Exception as err:
had_errors = True
logger.debug(f"Failed to load configuration '{name}'", exc_info=True)
continue
raise err

if provider_type == "python":
conf[key] = output
config[key] = output
elif provider_type == "process":
if output["status"] != 0:
had_errors = True
Expand All @@ -179,14 +183,14 @@ def load_dynamic_configuration(
f"from probe '{name}': {output['stderr']}"
)
else:
conf[key] = output.get("stdout", "").strip()
config[key] = output.get("stdout", "").strip()
elif provider_type == "http":
conf[key] = output.get("body")
config[key] = output.get("body")

if had_errors:
logger.warning(
"Some of the dynamic configuration failed to be loaded."
"Please review the log file for understanding what happened."
)

return conf
return config
2 changes: 2 additions & 0 deletions chaoslib/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
"InvalidControl",
]

from logzero import logger

class ChaosException(Exception):
pass


class InvalidActivity(ChaosException):
logger.info("error in InvalidActivity")
pass


Expand Down
12 changes: 12 additions & 0 deletions chaoslib/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,17 @@ def ensure_experiment_is_valid(experiment: Experiment):
if list(filter(lambda t: t == "" or not isinstance(t, str), tags)):
raise InvalidExperiment("experiment tags must be a non-empty string")

logger.info("Pass 1")
validate_extensions(experiment)
logger.info("Pass 2")

config = load_configuration(experiment.get("configuration", {}))
logger.info("Pass 3")
load_secrets(experiment.get("secrets", {}), config)
logger.info("Pass 4")

ensure_hypothesis_is_valid(experiment)
logger.info("Pass 5")

method = experiment.get("method")
if method is None:
Expand All @@ -87,8 +92,10 @@ def ensure_experiment_is_valid(experiment: Experiment):
"which can be empty for only checking steady state hypothesis "
)

logger.info("Pass 6")
for activity in method:
ensure_activity_is_valid(activity)
logger.info("Pass 7")

# let's see if a ref is indeed found in the experiment
ref = activity.get("ref")
Expand All @@ -98,13 +105,18 @@ def ensure_experiment_is_valid(experiment: Experiment):
"found in the experiment".format(r=ref)
)

logger.info("Pass 8")
rollbacks = experiment.get("rollbacks", [])
logger.info("Pass 9")
for activity in rollbacks:
ensure_activity_is_valid(activity)
logger.info("Pass 10")

warn_about_deprecated_features(experiment)
logger.info("Pass 11")

validate_controls(experiment)
logger.info("Pass 12")

logger.info("Experiment looks valid")

Expand Down
7 changes: 7 additions & 0 deletions chaoslib/hypothesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,27 @@ def ensure_hypothesis_is_valid(experiment: Experiment):
or raises :exc:`InvalidExperiment` or :exc:`InvalidActivity`.
"""
hypo = experiment.get("steady-state-hypothesis")
logger.info("ensure_hypothesis_is_valid 1")
if hypo is None:
return
logger.info("ensure_hypothesis_is_valid 2")

if not hypo.get("title"):
raise InvalidExperiment("hypothesis requires a title")
logger.info("ensure_hypothesis_is_valid 3")

probes = hypo.get("probes")
logger.info("ensure_hypothesis_is_valid 4")
if probes:
logger.info("ensure_hypothesis_is_valid 5")
for probe in probes:
logger.info(f"ensure_hypothesis_is_valid prob: {probe}")
ensure_activity_is_valid(probe)

if "tolerance" not in probe:
raise InvalidActivity("hypothesis probe must have a tolerance entry")

logger.info("ensure_hypothesis_is_valid 6")
ensure_hypothesis_tolerance_is_valid(probe["tolerance"])


Expand Down
27 changes: 24 additions & 3 deletions chaoslib/provider/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,48 @@ def validate_python_activity(activity: Activity): # noqa: C901

This should be considered as a private function.
"""
logger.info("validate_python_activity 1")
activity_name = activity["name"]
provider = activity["provider"]
mod_name = provider.get("module")
logger.info("validate_python_activity 2")
if not mod_name:
raise InvalidActivity("a Python activity must have a module path")

logger.info("validate_python_activity 3")
func = provider.get("func")
if not func:
raise InvalidActivity("a Python activity must have a function name")
logger.info("validate_python_activity 4")

try:
logger.info(f"validate_python_activity 5 mod_name: {mod_name}")
mod = importlib.import_module(mod_name)
except ImportError:
logger.info("validate_python_activity 6")
except ImportError as err:
logger.info(
"could not find Python module '{mod}'"
"in activity '{name}'"
"\nerror traceback:\n{error}".format(
mod=mod_name, name=activity_name, error=err
)
)
raise InvalidActivity(
"could not find Python module '{mod}' "
"in activity '{name}'".format(mod=mod_name, name=activity_name)
"could not find Python module '{mod}'"
"in activity '{name}'"
"\nerror traceback:\n{error}".format(
mod=mod_name, name=activity_name, error=err
)
)

logger.info("validate_python_activity 7")
found_func = False
arguments = provider.get("arguments", {})
logger.info("validate_python_activity 7")
candidates = set(inspect.getmembers(mod, inspect.isfunction)).union(
inspect.getmembers(mod, inspect.isbuiltin)
)
logger.info("validate_python_activity 8")
for (name, cb) in candidates:
if name == func:
found_func = True
Expand Down Expand Up @@ -144,10 +163,12 @@ def validate_python_activity(activity: Activity): # noqa: C901
raise
break

logger.info("validate_python_activity 9")
if not found_func:
raise InvalidActivity(
"The python module '{mod}' does not expose a function called "
"'{func}' in {type} '{name}'".format(
mod=mod_name, func=func, type=activity["type"], name=activity_name
)
)
logger.info("validate_python_activity 10")
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
logzero>=1.5.0
requests>=2.21
pyyaml~=5.4
pyyaml~=6.0
importlib-metadata~=1.2; python_version < '3.8'
importlib-metadata~=4.4; python_version > '3.9'
contextvars;python_version<"3.7"
contextvars;python_version<"3.7"
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ setup_requires =
install_requires =
logzero~=1.5
requests~=2.21
pyyaml~=5.4
pyyaml~=6.0
contextvars;python_version<"3.7"
importlib-metadata~=1.2; python_version < '3.8'
importlib-metadata~=4.4; python_version > '3.9'
Expand Down
31 changes: 18 additions & 13 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,22 +306,23 @@ def test_that_environment_variables_are_typed_correctly():


def test_dynamic_configuration_exception_means_output_is_missing():
config = load_dynamic_configuration(
{
"somekey": "hello world",
"token": {
"type": "probe",
"provider": {
"type": "python",
"module": "fixtures.configprobe",
"func": "raise_exception",
config = {"somekey": "hello world"}
with pytest.raises(Exception):
config = load_dynamic_configuration(
{
"somekey": "hello world",
"token": {
"type": "probe",
"provider": {
"type": "python",
"module": "fixtures.configprobe",
"func": "raise_exception",
},
},
},
}
)
}
)

assert config["somekey"] == "hello world"
assert "token" not in config


def test_dynamic_configuration_can_be_used_next_key():
Expand Down Expand Up @@ -350,3 +351,7 @@ def test_dynamic_configuration_can_be_used_next_key():

assert config["capped"] == "Hello World From Earth"
assert config["shorten"] == "Hello [...]"


if __name__ == "__main__":
test_dynamic_configuration_exception_means_output_is_missing()