diff --git a/bonfire/bonfire.py b/bonfire/bonfire.py index 0bdc4643..dc64bbc6 100755 --- a/bonfire/bonfire.py +++ b/bonfire/bonfire.py @@ -20,7 +20,7 @@ wait_for_clowd_env_target_ns, wait_on_cji, ) -from bonfire.utils import split_equals, find_what_depends_on +from bonfire.utils import FatalError, split_equals, find_what_depends_on from bonfire.local import get_local_apps from bonfire.processor import TemplateProcessor, process_clowd_env, process_iqe_cji from bonfire.namespaces import ( @@ -759,7 +759,7 @@ def _cmd_config_deploy( clowd_env = match["metadata"]["name"] log.debug("inferred clowd_env: '%s'", clowd_env) - def _err_handler(): + def _err_handler(err): try: if not no_release_on_fail and not requested_ns and used_ns_reservation_system: # if we auto-reserved this ns, auto-release it on failure unless @@ -767,7 +767,8 @@ def _err_handler(): log.info("releasing namespace '%s'", ns) release_namespace(ns) finally: - _error("deploy failed") + msg = f"deploy failed: {str(err)}" + _error(msg) try: log.info("processing app templates...") @@ -795,15 +796,18 @@ def _err_handler(): apply_config(ns, apps_config) log.info("waiting on resources for max of %dsec...", timeout) _wait_on_namespace_resources(ns, timeout) - except KeyboardInterrupt: - log.error("Aborted by keyboard interrupt!") - _err_handler() + except KeyboardInterrupt as err: + log.error("aborted by keyboard interrupt!") + _err_handler(err) except TimedOutError as err: - log.error("Hit timeout error: %s", err) - _err_handler() - except Exception: + log.error("hit timeout error: %s", err) + _err_handler(err) + except FatalError as err: + log.error("hit fatal error: %s", err) + _err_handler(err) + except Exception as err: log.exception("hit unexpected error!") - _err_handler() + _err_handler(err) else: log.info("successfully deployed to namespace '%s'", ns) click.echo(ns) @@ -811,13 +815,7 @@ def _err_handler(): def _process_clowdenv(target_namespace, quay_user, env_name, template_file): env_name = _get_env_name(target_namespace, env_name) - - try: - clowd_env_config = process_clowd_env(target_namespace, quay_user, env_name, template_file) - except ValueError as err: - _error(str(err)) - - return clowd_env_config + return process_clowd_env(target_namespace, quay_user, env_name, template_file) @main.command("process-env") @@ -849,6 +847,10 @@ def _cmd_deploy_clowdenv( """Process ClowdEnv template and deploy to a cluster""" _warn_if_unsafe(namespace) + def _err_handler(err): + msg = f"deploy failed: {str(err)}" + _error(msg) + try: if import_secrets: import_secrets_from_dir(secrets_dir) @@ -867,15 +869,18 @@ def _cmd_deploy_clowdenv( _wait_on_namespace_resources(namespace, timeout) clowd_env_name = find_clowd_env_for_ns(namespace)["metadata"]["name"] - except KeyboardInterrupt: - log.error("Aborted by keyboard interrupt!") - _error("deploy failed") + except KeyboardInterrupt as err: + log.error("aborted by keyboard interrupt!") + _err_handler(err) except TimedOutError as err: - log.error("Hit timeout error: %s", err) - _error("deploy failed") - except Exception: + log.error("hit timeout error: %s", err) + _err_handler(err) + except FatalError as err: + log.error("hit fatal error: %s", err) + _err_handler(err) + except Exception as err: log.exception("hit unexpected error!") - _error("deploy failed") + _err_handler(err) else: log.info("ClowdEnvironment '%s' using ns '%s' is ready", clowd_env_name, namespace) click.echo(namespace) @@ -912,6 +917,10 @@ def _cmd_deploy_iqe_cji( """Process IQE CJI template, apply it, and wait for it to start running.""" _warn_if_unsafe(namespace) + def _err_handler(err): + msg = f"deploy failed: {str(err)}" + _error(msg) + try: cji_config = process_iqe_cji( clowd_app_name, debug, marker, filter, env, image_tag, cji_name, template_file @@ -928,15 +937,18 @@ def _cmd_deploy_iqe_cji( log.info("waiting on CJI '%s' for max of %dsec...", cji_name, timeout) pod_name = wait_on_cji(namespace, cji_name, timeout) - except KeyboardInterrupt: - log.error("Aborted by keyboard interrupt!") - _error("deploy failed") + except KeyboardInterrupt as err: + log.error("aborted by keyboard interrupt!") + _err_handler(err) except TimedOutError as err: - log.error("Hit timeout error: %s", err) - _error("deploy failed") - except Exception: + log.error("hit timeout error: %s", err) + _err_handler(err) + except FatalError as err: + log.error("hit fatal error: %s", err) + _err_handler(err) + except Exception as err: log.exception("hit unexpected error!") - _error("deploy failed") + _err_handler(err) else: log.info( "pod '%s' related to CJI '%s' in ns '%s' is running", pod_name, cji_name, namespace @@ -1004,5 +1016,12 @@ def _cmd_apps_what_depends_on( print("\n".join(found) or f"no apps depending on {component} found") +def main_with_handler(): + try: + main() + except FatalError as err: + _error(str(err)) + + if __name__ == "__main__": - main() + main_with_handler() diff --git a/bonfire/config.py b/bonfire/config.py index bceb4438..e2b7d9c5 100644 --- a/bonfire/config.py +++ b/bonfire/config.py @@ -8,7 +8,7 @@ from dotenv import load_dotenv -from bonfire.utils import load_file +from bonfire.utils import load_file, FatalError log = logging.getLogger(__name__) @@ -83,7 +83,7 @@ def load_config(config_path=None): log.debug("user provided explicit config path: %s", config_path) config_path = Path(config_path) if not config_path.exists(): - raise ValueError(f"provided config file path '{str(config_path)}' does not exist") + raise FatalError(f"provided config file path '{str(config_path)}' does not exist") else: # no user-provided path, check default locations config_path = Path("config.yaml") diff --git a/bonfire/local.py b/bonfire/local.py index 91c73dc7..b0b79deb 100644 --- a/bonfire/local.py +++ b/bonfire/local.py @@ -1,7 +1,7 @@ import logging import yaml -from bonfire.utils import RepoFile, get_dupes +from bonfire.utils import RepoFile, get_dupes, FatalError log = logging.getLogger(__name__) @@ -20,12 +20,12 @@ def _fetch_apps_file(config): fetched_apps = yaml.safe_load(content) if "apps" not in fetched_apps: - raise ValueError("fetched apps file has no 'apps' key") + raise FatalError("fetched apps file has no 'apps' key") app_names = [a["name"] for a in fetched_apps["apps"]] dupes = get_dupes(app_names) if dupes: - raise ValueError("duplicate app names found in fetched apps file: {dupes}") + raise FatalError("duplicate app names found in fetched apps file: {dupes}") return {a["name"]: a for a in fetched_apps["apps"]} @@ -34,7 +34,7 @@ def _parse_apps_in_cfg(config): app_names = [a["name"] for a in config["apps"]] dupes = get_dupes(app_names) if dupes: - raise ValueError("duplicate app names found in config: {dupes}") + raise FatalError("duplicate app names found in config: {dupes}") return {a["name"]: a for a in config["apps"]} diff --git a/bonfire/processor.py b/bonfire/processor.py index 3061c4ec..ec6d1e26 100644 --- a/bonfire/processor.py +++ b/bonfire/processor.py @@ -9,7 +9,7 @@ import bonfire.config as conf from bonfire.openshift import process_template -from bonfire.utils import RepoFile +from bonfire.utils import RepoFile, FatalError from bonfire.utils import get_dependencies as utils_get_dependencies log = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def process_clowd_env(target_ns, quay_user, env_name, template_path): env_template_path = Path(template_path if template_path else conf.DEFAULT_CLOWDENV_TEMPLATE) if not env_template_path.exists(): - raise ValueError("ClowdEnvironment template file does not exist: %s", env_template_path) + raise FatalError("ClowdEnvironment template file does not exist: %s", env_template_path) with env_template_path.open() as fp: template_data = yaml.safe_load(fp) @@ -75,7 +75,7 @@ def process_clowd_env(target_ns, quay_user, env_name, template_path): processed_template = process_template(template_data, params=params) if not processed_template.get("items"): - raise ValueError("Processed ClowdEnvironment template has no items") + raise FatalError("Processed ClowdEnvironment template has no items") return processed_template @@ -95,7 +95,7 @@ def process_iqe_cji( template_path = Path(template_path if template_path else conf.DEFAULT_IQE_CJI_TEMPLATE) if not template_path.exists(): - raise ValueError("CJI template file does not exist: %s", template_path) + raise FatalError("CJI template file does not exist: %s", template_path) with template_path.open() as fp: template_data = yaml.safe_load(fp) @@ -112,7 +112,7 @@ def process_iqe_cji( processed_template = process_template(template_data, params=params) if not processed_template.get("items"): - raise ValueError("Processed CJI template has no items") + raise FatalError("Processed CJI template has no items") return processed_template @@ -140,7 +140,7 @@ def _find_dupe_components(components_for_app): if component in other_components: found_in.append(other_app_name) if len(found_in) > 1: - raise ValueError( + raise FatalError( f"component '{component}' is not unique, found in apps: {found_in}" ) @@ -151,18 +151,18 @@ def _validate_app_config(self, apps_config): required_keys = ["name", "components"] missing_keys = [k for k in required_keys if k not in app_cfg] if missing_keys: - raise ValueError(f"app '{app_name}' is missing required keys: {missing_keys}") + raise FatalError(f"app '{app_name}' is missing required keys: {missing_keys}") app_name = app_cfg["name"] if app_name in components_for_app: - raise ValueError(f"app with name '{app_name}' is not unique") + raise FatalError(f"app with name '{app_name}' is not unique") components_for_app[app_name] = [] for component in app_cfg.get("components", []): required_keys = ["name", "host", "repo", "path"] missing_keys = [k for k in required_keys if k not in component] if missing_keys: - raise ValueError( + raise FatalError( f"component on app {app_name} is missing required keys: {missing_keys}" ) comp_name = component["name"] @@ -209,7 +209,7 @@ def __init__( def _get_app_config(self, app_name): if app_name not in self.apps_config: - raise ValueError(f"app {app_name} not found in apps config") + raise FatalError(f"app {app_name} not found in apps config") return self.apps_config[app_name] def _get_component_config(self, component_name): @@ -218,7 +218,7 @@ def _get_component_config(self, component_name): if component["name"] == component_name: return component else: - raise ValueError(f"component with name '{component_name}' not found") + raise FatalError(f"component with name '{component_name}' not found") def _sub_image_tags(self, items): content = json.dumps(items) @@ -238,7 +238,7 @@ def _sub_ref(self, current_component_name, repo_file): elif len(split) == 1: component_name = split[0] else: - raise ValueError( + raise FatalError( f"invalid format for template ref override: {app_component}={value}" ) @@ -259,7 +259,7 @@ def _sub_params(self, current_component_name, params): elif len(split) == 2: component_name, param_name = split else: - raise ValueError(f"invalid format for parameter override: {param_path}={value}") + raise FatalError(f"invalid format for parameter override: {param_path}={value}") if current_component_name == component_name: log.info( diff --git a/bonfire/secrets.py b/bonfire/secrets.py index cc5388bd..bdeaea40 100644 --- a/bonfire/secrets.py +++ b/bonfire/secrets.py @@ -7,7 +7,7 @@ import os from bonfire.openshift import oc, get_json -from bonfire.utils import load_file +from bonfire.utils import load_file, FatalError log = logging.getLogger(__name__) @@ -30,7 +30,7 @@ def _parse_secret_file(path): try: secrets[item["metadata"]["name"]] = item except KeyError: - raise ValueError("Secret at path '{}' has no metadata/name".format(path)) + raise FatalError("Secret at path '{}' has no metadata/name".format(path)) return secrets @@ -61,10 +61,10 @@ def _import_secret(secret_name, secret_data): def import_secrets_from_dir(path): if not os.path.exists(path): - raise ValueError(f"secrets directory not found: {path}") + raise FatalError(f"secrets directory not found: {path}") if not os.path.isdir(path): - raise ValueError(f"invalid secrets directory: {path}") + raise FatalError(f"invalid secrets directory: {path}") files = _get_files_in_dir(path) secrets = {} @@ -74,7 +74,7 @@ def import_secrets_from_dir(path): log.info("loaded %d secret(s) from file '%s'", len(secrets_in_file), secret_file) for secret_name in secrets_in_file: if secret_name in secrets: - raise ValueError(f"secret with name '{secret_name}' defined twice in secrets dir") + raise FatalError(f"secret with name '{secret_name}' defined twice in secrets dir") secrets.update(secrets_in_file) for secret_name, secret_data in secrets.items(): diff --git a/bonfire/utils.py b/bonfire/utils.py index aa515f94..f363984a 100644 --- a/bonfire/utils.py +++ b/bonfire/utils.py @@ -11,6 +11,13 @@ from cached_property import cached_property + +class FatalError(Exception): + """An exception that will cause the CLI to exit""" + + pass + + GH_RAW_URL = "https://raw.githubusercontent.com/{org}/{repo}/{ref}{path}" GL_RAW_URL = "https://gitlab.cee.redhat.com/{group}/{project}/-/raw/{ref}{path}" GH_API_URL = os.getenv("GITHUB_API_URL", "https://api.github.com") @@ -102,7 +109,7 @@ def split_equals(list_of_str, allow_null=False): class RepoFile: def __init__(self, host, org, repo, path, ref="master"): if host not in ["local", "github", "gitlab"]: - raise ValueError(f"invalid repo host type: {host}") + raise FatalError(f"invalid repo host type: {host}") if not path.startswith("/"): path = f"/{path}" @@ -121,12 +128,12 @@ def from_config(cls, d): required_keys = ["host", "repo", "path"] missing_keys = [k for k in required_keys if k not in d.keys()] if missing_keys: - raise ValueError(f"repo config missing keys: {', '.join(missing_keys)}") + raise FatalError(f"repo config missing keys: {', '.join(missing_keys)}") repo = d["repo"] if d["host"] in ["github", "gitlab"]: if "/" not in repo: - raise ValueError( + raise FatalError( f"invalid value for repo '{repo}', required format: /" ) org, repo = repo.split("/") @@ -229,7 +236,7 @@ def _get_gl_commit_hash(self): project_id = p["id"] if not project_id: - raise ValueError("gitlab project ID not found for {self.org}/{self.repo}") + raise FatalError("gitlab project ID not found for {self.org}/{self.repo}") def get_ref_func(ref): return requests.get( @@ -359,7 +366,7 @@ def find_what_depends_on(apps_config, clowdapp_name): def load_file(path): """Load a .json/.yml/.yaml file.""" if not os.path.isfile(path): - raise ValueError("Path '{}' is not a file or does not exist".format(path)) + raise FatalError("Path '{}' is not a file or does not exist".format(path)) _, file_ext = os.path.splitext(path) @@ -369,9 +376,9 @@ def load_file(path): elif file_ext == ".json": content = json.load(f) else: - raise ValueError("File '{}' must be a YAML or JSON file".format(path)) + raise FatalError("File '{}' must be a YAML or JSON file".format(path)) if not content: - raise ValueError("File '{}' is empty!".format(path)) + raise FatalError("File '{}' is empty!".format(path)) return content diff --git a/entry_points.txt b/entry_points.txt index 18060543..3175b778 100644 --- a/entry_points.txt +++ b/entry_points.txt @@ -1,2 +1,2 @@ [console_scripts] -bonfire = bonfire.bonfire:main +bonfire = bonfire.bonfire:main_with_handler