def test_read_secrets_from_vault_with_kv_version_2(hvac): config = { "vault_addr": "http://someaddr.com", "vault_token": "not_awesome_token", "vault_kv_version": "2", } secrets_info = { "k8s": { "a-secret": { "type": "vault", "path": "foo/stuff" } } } # secret at secret/foo vault_secret_payload = { "data": { "data": { "my-important-secret": "bar", "my-less-important-secret": "baz" }, "metadata": { "auth": None, "lease_duration": 2764800, "lease_id": "", "renewable": False, }, } } fake_client = MagicMock() hvac.Client.return_value = fake_client fake_client.secrets.kv.v2.read_secret_version.return_value = vault_secret_payload secrets = load_secrets(secrets_info, config) assert secrets["k8s"]["a-secret"] == { "my-important-secret": "bar", "my-less-important-secret": "baz", } secrets_info = { "k8s": { "a-secret": { "type": "vault", "path": "foo/stuff", "key": "my-important-secret", } } } secrets = load_secrets(secrets_info, config) assert secrets["k8s"]["a-secret"] == "bar"
def test_vault_add_subkeys(hvac): config = { "vault_addr": "http://someaddr.com", "vault_token": "not_awesome_token", "vault_kv_version": "2", } vault_secret_payload = { "data": { "data": { "foo": "bar", "baz": "hello" }, "metadata": { "auth": None, "lease_duration": 2764800, "lease_id": "", "renewable": False, }, } } fake_client = MagicMock() hvac.Client.return_value = fake_client fake_client.secrets.kv.v2.read_secret_version.return_value = vault_secret_payload secrets = load_secrets( {"myapp": { "token": { "type": "vault", "path": "secrets/something" } }}, config) assert secrets["myapp"]["token"]["foo"] == "bar" assert secrets["myapp"]["token"]["baz"] == "hello"
def configure(self, experiment: Experiment, settings: Settings, experiment_vars: Dict[str, Any]) -> None: config_vars, secret_vars = experiment_vars or (None, None) self.settings = settings if settings is not None else \ get_loaded_settings() self.config = load_configuration(experiment.get("configuration", {}), config_vars) self.secrets = load_secrets(experiment.get("secrets", {}), self.config, secret_vars)
def test_should_override_load_inline_with_var(): secrets = load_secrets( {"kubernetes": { "api_server_url": "http://1.2.3.4" }}, config.EmptyConfig, {"kubernetes": { "api_server_url": "http://elsewhere" }}) assert secrets["kubernetes"]["api_server_url"] == "http://elsewhere"
def test_should_merge_properly(): secrets = load_secrets({ "kubernetes": { "username": "******", "address": {"host": "whatever", "port": 8090}, "api_server_url": {"type": "env", "key": "KUBE_API_URL"}, } }, config.EmptyConfig, )
def test_should_load_environment(): os.environ["KUBE_API_URL"] = "http://1.2.3.4" secrets = load_secrets( { "kubernetes": { "api_server_url": { "type": "env", "key": "KUBE_API_URL" } } }, config.EmptyConfig) assert secrets["kubernetes"]["api_server_url"] == "http://1.2.3.4"
def test_override_load_environmen_with_var(): os.environ["KUBE_API_URL"] = "http://1.2.3.4" secrets = load_secrets( { "kubernetes": { "api_server_url": { "type": "env", "key": "KUBE_API_URL" } } }, config.EmptyConfig, {"kubernetes": { "api_server_url": "http://elsewhere" }}) assert secrets["kubernetes"]["api_server_url"] == "http://elsewhere"
def test_should_merge_properly(): secrets = load_secrets( { "kubernetes": { "username": "******", "address": { "host": "whatever", "port": 8090 }, "api_server_url": { "type": "env", "key": "KUBE_API_URL" } } }, config.EmptyConfig) assert secrets["kubernetes"]["username"] == "jane" assert secrets["kubernetes"]["address"]["host"] == "whatever" assert secrets["kubernetes"]["address"]["port"] == 8090 assert secrets["kubernetes"]["api_server_url"] == "http://1.2.3.4"
def test_should_load_nested_environment(): secrets = load_secrets( { "kubernetes": { "env1": { "username": "******", "address": { "host": "whatever", "port": 8090 }, "api_server_url": { "type": "env", "key": "KUBE_API_URL" }, } } }, config.EmptyConfig, ) assert secrets["kubernetes"]["env1"]["username"] == "jane" assert secrets["kubernetes"]["env1"]["address"]["host"] == "whatever" assert secrets["kubernetes"]["env1"]["address"]["port"] == 8090 assert secrets["kubernetes"]["env1"]["api_server_url"] == "http://1.2.3.4"
def test_should_load_inline(): secrets = load_secrets( {"kubernetes": {"api_server_url": "http://1.2.3.4"}}, config.EmptyConfig )
def test_override_load_environmen_with_var(): secrets = load_secrets( {"kubernetes": {"api_server_url": {"type": "env", "key": "KUBE_API_URL"}}}, config.EmptyConfig, {"kubernetes": {"api_server_url": "http://elsewhere"}}, )
import pytest from fixtures import config from hvac.exceptions import InvalidRequest from chaoslib.exceptions import InvalidExperiment from chaoslib.secret import create_vault_client, load_secrets, load_secrets_from_vault @patch.dict(os.environ, {"KUBE_API_URL": "http://1.2.3.4"}) def test_should_load_environment(): <<<<<<< HEAD secrets = load_secrets({ "kubernetes": { "api_server_url": { "type": "env", "key": "KUBE_API_URL" } } }, config.EmptyConfig) ======= secrets = load_secrets( {"kubernetes": {"api_server_url": {"type": "env", "key": "KUBE_API_URL"}}}, config.EmptyConfig, ) >>>>>>> 6e72734 (Resolves #210) assert secrets["kubernetes"]["api_server_url"] == "http://1.2.3.4" def test_should_load_inline(): secrets = load_secrets( {"kubernetes": {"api_server_url": "http://1.2.3.4"}}, config.EmptyConfig
def ensure_experiment_is_valid(experiment: Experiment): """ A chaos experiment consists of a method made of activities to carry sequentially. There are two kinds of activities: * probe: detecting the state of a resource in your system or external to it There are two kinds of probes: `steady` and `close` * action: an operation to apply against your system Usually, an experiment is made of a set of `steady` probes that ensure the system is sound to carry further the experiment. Then, an action before another set of of ̀close` probes to sense the state of the system post-action. This function raises :exc:`InvalidExperiment`, :exc:`InvalidProbe` or :exc:`InvalidAction` depending on where it fails. """ logger.info("Validating the experiment's syntax") if not experiment: raise InvalidExperiment("an empty experiment is not an experiment") if not experiment.get("title"): raise InvalidExperiment("experiment requires a title") if not experiment.get("description"): raise InvalidExperiment("experiment requires a description") tags = experiment.get("tags") if tags: if list(filter(lambda t: t == '' or not isinstance(t, str), tags)): raise InvalidExperiment( "experiment tags must be a non-empty string") validate_extensions(experiment) config = load_configuration(experiment.get("configuration", {})) load_secrets(experiment.get("secrets", {}), config) ensure_hypothesis_is_valid(experiment) method = experiment.get("method") if not method: raise InvalidExperiment("an experiment requires a method with " "at least one activity") for activity in method: ensure_activity_is_valid(activity) # let's see if a ref is indeed found in the experiment ref = activity.get("ref") if ref and not lookup_activity(ref): raise InvalidActivity("referenced activity '{r}' could not be " "found in the experiment".format(r=ref)) rollbacks = experiment.get("rollbacks", []) for activity in rollbacks: ensure_activity_is_valid(activity) warn_about_deprecated_features(experiment) validate_controls(experiment) logger.info("Experiment looks valid")
def run_experiment(experiment: Experiment, settings: Settings = None) -> Journal: """ Run the given `experiment` method step by step, in the following sequence: steady probe, action, close probe. Activities can be executed in background when they have the `"background"` property set to `true`. In that case, the activity is run in a thread. By the end of runs, those threads block until they are all complete. If the experiment has the `"dry"` property set to `False`, the experiment runs without actually executing the activities. NOTE: Tricky to make a decision whether we should rollback when exiting abnormally (Ctrl-C, SIGTERM...). Afterall, there is a chance we actually cannot afford to rollback properly. Better bailing to a conservative approach. This means we swallow :exc:`KeyboardInterrupt` and :exc:`SystemExit` and do not bubble it back up to the caller. We when were interrupted, we set the `interrupted` flag of the result accordingly to notify the caller this was indeed not terminated properly. """ logger.info("Running experiment: {t}".format(t=experiment["title"])) dry = experiment.get("dry", False) if dry: logger.warning("Dry mode enabled") started_at = time.time() settings = settings if settings is not None else get_loaded_settings() config = load_configuration(experiment.get("configuration", {})) secrets = load_secrets(experiment.get("secrets", {}), config) initialize_global_controls(experiment, config, secrets, settings) initialize_controls(experiment, config, secrets) activity_pool, rollback_pool = get_background_pools(experiment) control = Control() journal = initialize_run_journal(experiment) try: try: control.begin("experiment", experiment, experiment, config, secrets) # this may fail the entire experiment right there if any of the # probes fail or fall out of their tolerance zone try: state = run_steady_state_hypothesis(experiment, config, secrets, dry=dry) journal["steady_states"]["before"] = state if state is not None and not state["steady_state_met"]: p = state["probes"][-1] raise ActivityFailed( "Steady state probe '{p}' is not in the given " "tolerance so failing this experiment".format( p=p["activity"]["name"])) except ActivityFailed as a: journal["steady_states"]["before"] = state journal["status"] = "failed" logger.fatal(str(a)) else: try: journal["run"] = apply_activities(experiment, config, secrets, activity_pool, dry) except Exception: journal["status"] = "aborted" logger.fatal( "Experiment ran into an un expected fatal error, " "aborting now.", exc_info=True) else: try: state = run_steady_state_hypothesis(experiment, config, secrets, dry=dry) journal["steady_states"]["after"] = state if state is not None and not state["steady_state_met"]: journal["deviated"] = True p = state["probes"][-1] raise ActivityFailed( "Steady state probe '{p}' is not in the given " "tolerance so failing this experiment".format( p=p["activity"]["name"])) except ActivityFailed as a: journal["status"] = "failed" logger.fatal(str(a)) except InterruptExecution as i: journal["status"] = "interrupted" logger.fatal(str(i)) except (KeyboardInterrupt, SystemExit): journal["status"] = "interrupted" logger.warn("Received an exit signal, " "leaving without applying rollbacks.") else: journal["status"] = journal["status"] or "completed" journal["rollbacks"] = apply_rollbacks(experiment, config, secrets, rollback_pool, dry) journal["end"] = datetime.utcnow().isoformat() journal["duration"] = time.time() - started_at has_deviated = journal["deviated"] status = "deviated" if has_deviated else journal["status"] logger.info("Experiment ended with status: {s}".format(s=status)) if has_deviated: logger.info( "The steady-state has deviated, a weakness may have been " "discovered") control.with_state(journal) try: control.end("experiment", experiment, experiment, config, secrets) except ChaosException: logger.debug("Failed to close controls", exc_info=True) finally: cleanup_controls(experiment) cleanup_global_controls() return journal
def configure(self, experiment: Experiment, settings: Settings) -> None: self.settings = settings if settings is not None else \ get_loaded_settings() self.config = load_configuration(experiment.get("configuration", {})) self.secrets = load_secrets(experiment.get("secrets", {}), self.config)