diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fec5101..01832f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/chaoslib/__init__.py b/chaoslib/__init__.py index 457d4151..742667bc 100644 --- a/chaoslib/__init__.py +++ b/chaoslib/__init__.py @@ -41,7 +41,7 @@ "convert_vars", "PayloadEncoder", ] -__version__ = "1.27.3" +__version__ = "1.29.0" def substitute( diff --git a/chaoslib/activity.py b/chaoslib/activity.py index f09ea10b..c94b38bb 100644 --- a/chaoslib/activity.py +++ b/chaoslib/activity.py @@ -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") @@ -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( diff --git a/chaoslib/configuration.py b/chaoslib/configuration.py index d45f180e..a78f2590 100644 --- a/chaoslib/configuration.py +++ b/chaoslib/configuration.py @@ -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"] @@ -148,14 +153,13 @@ 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 @@ -163,14 +167,14 @@ def load_dynamic_configuration( 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 @@ -179,9 +183,9 @@ 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( @@ -189,4 +193,4 @@ def load_dynamic_configuration( "Please review the log file for understanding what happened." ) - return conf + return config diff --git a/chaoslib/exceptions.py b/chaoslib/exceptions.py index 97e6bf20..1c993d9c 100644 --- a/chaoslib/exceptions.py +++ b/chaoslib/exceptions.py @@ -10,12 +10,14 @@ "InvalidControl", ] +from logzero import logger class ChaosException(Exception): pass class InvalidActivity(ChaosException): + logger.info("error in InvalidActivity") pass diff --git a/chaoslib/experiment.py b/chaoslib/experiment.py index 905687c2..4913bd00 100644 --- a/chaoslib/experiment.py +++ b/chaoslib/experiment.py @@ -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: @@ -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") @@ -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") diff --git a/chaoslib/hypothesis.py b/chaoslib/hypothesis.py index adf2124b..01cc09d7 100644 --- a/chaoslib/hypothesis.py +++ b/chaoslib/hypothesis.py @@ -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"]) diff --git a/chaoslib/provider/python.py b/chaoslib/provider/python.py index 9836afd8..6568fef8 100644 --- a/chaoslib/provider/python.py +++ b/chaoslib/provider/python.py @@ -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 @@ -144,6 +163,7 @@ 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 " @@ -151,3 +171,4 @@ def validate_python_activity(activity: Activity): # noqa: C901 mod=mod_name, func=func, type=activity["type"], name=activity_name ) ) + logger.info("validate_python_activity 10") diff --git a/requirements.txt b/requirements.txt index ed4460f9..3359c2e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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" \ No newline at end of file +contextvars;python_version<"3.7" diff --git a/setup.cfg b/setup.cfg index 4c7fdcc2..7b583e31 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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' diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 28772342..4cbf21f4 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -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(): @@ -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()