Skip to content

Commit

Permalink
Support app syntax (#338)
Browse files Browse the repository at this point in the history
* Add CLI options handling for 'app' syntax

---------

Co-authored-by: Brandon Squizzato <[email protected]>
  • Loading branch information
maknop and bsquizz authored Dec 18, 2023
1 parent 4e1693f commit 18a0dea
Show file tree
Hide file tree
Showing 4 changed files with 424 additions and 86 deletions.
75 changes: 58 additions & 17 deletions bonfire/bonfire.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import bonfire.config as conf
from bonfire.local import get_local_apps, get_appsfile_apps
from bonfire.utils import RepoFile, SYNTAX_ERR
from bonfire.utils import AppOrComponentSelector, RepoFile, SYNTAX_ERR
from bonfire.namespaces import (
Namespace,
extend_namespace,
Expand Down Expand Up @@ -350,29 +350,70 @@ def _validate_split_equals(ctx, param, value):
raise click.BadParameter(msg)


def _validate_opposing_opts(ctx, param, value):
def _translate_to_obj(value_list):
apps = []
components = []
select_all = False
for value in value_list:
if value == "all":
# all is a special keyword
select_all = True
elif value.startswith("app:"):
apps.append(value.split(":")[1])
else:
components.append(value)
return AppOrComponentSelector(select_all, apps, components)


def _app_or_component_selector(ctx, param, this_value):
if any([val.startswith("-") for val in this_value]):
raise click.BadParameter("requires a component name or keyword 'all'")

# check if 'app:' syntax has been used and translate input values to apps/components dictionary
this_value = _translate_to_obj(this_value)

opposite_option = {
"remove_resources": "no_remove_resources",
"no_remove_resources": "remove_resources",
"remove_dependencies": "no_remove_dependencies",
"no_remove_dependencies": "remove_dependencies",
}
opposite_option_value = ctx.params.get(opposite_option[param.name], "")

if any([val.startswith("-") for val in value]):
raise click.BadParameter("requires a component name or keyword 'all'")
if "all" in value and "all" in opposite_option_value:
this_param_name: str = param.name
other_param_name: str = opposite_option[this_param_name]

other_value = ctx.params.get(other_param_name, AppOrComponentSelector())

# validate that opposing options are not both set to 'all'
if this_value.select_all and other_value.select_all:
raise click.BadParameter(
f"'{param.opts[0]}' and its opposite option can't be both set to 'all'"
f"'all' cannot be specified on both this option"
f" and its opposite '{other_param_name}'"
)

# default values
if param.name == "remove_resources" and not value and not opposite_option_value:
value = ("all",)
if param.name == "no_remove_dependencies" and not value and not opposite_option_value:
value = ("all",)
# validate that the same app was not used in opposing options
for app in this_value.apps:
if app in other_value.apps:
raise click.BadParameter(
f"app '{app}' cannot be specified on both this option"
f" and its opposite '{other_param_name}'"
)

# validate that the same component was not used in opposing options
for component in this_value.components:
if component in other_value.components:
raise click.BadParameter(
f"component '{component}' cannot be specified on both this option"
f" and its opposite '{other_param_name}'"
)

# set default value for --remove-resources to 'all' if option was unspecified
# set default value for --no-remove-dependencies to 'all' if option was unspecified
options_w_defaults = ("remove_resources", "no_remove_dependencies")
if this_param_name in options_w_defaults and this_value.empty:
this_value.select_all = True

return value
return this_value


_app_source_options = [
Expand Down Expand Up @@ -495,7 +536,7 @@ def _validate_opposing_opts(ctx, param, value):
),
type=str,
multiple=True,
callback=_validate_opposing_opts,
callback=_app_or_component_selector,
),
click.option(
"--no-remove-resources",
Expand All @@ -505,7 +546,7 @@ def _validate_opposing_opts(ctx, param, value):
),
type=str,
multiple=True,
callback=_validate_opposing_opts,
callback=_app_or_component_selector,
),
click.option(
"--remove-dependencies",
Expand All @@ -515,7 +556,7 @@ def _validate_opposing_opts(ctx, param, value):
),
type=str,
multiple=True,
callback=_validate_opposing_opts,
callback=_app_or_component_selector,
),
click.option(
"--no-remove-dependencies",
Expand All @@ -525,7 +566,7 @@ def _validate_opposing_opts(ctx, param, value):
),
type=str,
multiple=True,
callback=_validate_opposing_opts,
callback=_app_or_component_selector,
),
click.option(
"--single-replicas/--no-single-replicas",
Expand Down
162 changes: 112 additions & 50 deletions bonfire/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from pathlib import Path

import yaml
from cached_property import cached_property
from ocviapy import process_template
from sh import ErrorReturnCode

import bonfire.config as conf
from bonfire.openshift import whoami
from bonfire.utils import FatalError, RepoFile
from bonfire.utils import AppOrComponentSelector, FatalError, RepoFile
from bonfire.utils import get_clowdapp_dependencies
from bonfire.utils import get_dependencies as utils_get_dependencies

Expand Down Expand Up @@ -233,6 +234,45 @@ def __init__(self, name, items, deps_handled=False, optional_deps_handled=False)
self.optional_deps_handled = optional_deps_handled


def _should_remove(
remove_option: AppOrComponentSelector,
no_remove_option: AppOrComponentSelector,
app_name: str,
component_name: str,
) -> bool:
# 'should_remove' evaluates to true when:
# "--remove-option all" is set
# "--remove-option all --no-remove-option x" is set and app/component does NOT match 'x'
# "--no-remove-option all --remove-option x" is set and app/component matches 'x'
remove_for_all_no_exceptions = remove_option.select_all and no_remove_option.empty
remove_for_none_no_exceptions = no_remove_option.select_all and remove_option.empty

log.debug(
"should_remove: app_name: %s, component_name: %s, remove_option: %s, no_remove_option: %s",
app_name,
component_name,
remove_option,
no_remove_option,
)

if remove_for_none_no_exceptions:
return False
if remove_for_all_no_exceptions:
return True
if remove_option.select_all:
if app_name in no_remove_option.apps or component_name in no_remove_option.components:
return False
return True
if no_remove_option.select_all:
if app_name in remove_option.apps or component_name in remove_option.components:
return True
return False

# in theory all use cases should be covered by the above logic, throw an exception
# so we can identify if we missed a use case
raise Exception("hit None condition evaluating should_remove")


class TemplateProcessor:
@staticmethod
def _parse_app_names(app_names):
Expand Down Expand Up @@ -305,22 +345,23 @@ def _validate_component_list(all_components, data, name):
f"component given for {name} not found in app config: {component_name}"
)

def _validate_component_options(self, all_components, data, name):
if isinstance(data, dict):
@staticmethod
def _validate_app_list(all_apps, app_list, name):
for app_name in app_list:
if app_name not in all_apps:
raise FatalError(f"app given for {name} not found in app config: {app_name}")

def _validate_selector_options(self, all_apps, all_components, data, name):
if isinstance(data, AppOrComponentSelector):
self._validate_app_list(all_apps, data.apps, name)
self._validate_component_list(all_components, data.components, name)
elif isinstance(data, dict):
self._validate_component_dict(all_components, data, name)
else:
self._validate_component_list(all_components, data, name)

def _validate(self):
"""
Validate app configurations and options passed to the TemplateProcessor
1. Check that each app has required keys
2. Check that each app name is unique
3. Check that each component in an app has required keys
4. Check that each component is a unique name across the whole config
5. Check that CLI params requiring a component use a valid component name
"""
@cached_property
def _components_for_app(self):
components_for_app = {}

for app_name, app_cfg in self.apps_config.items():
Expand All @@ -347,37 +388,53 @@ def _validate(self):
comp_name = component["name"]
components_for_app[app_name].append(comp_name)

return components_for_app

def _validate(self):
"""
Validate app configurations and options passed to the TemplateProcessor
1. Check that each app has required keys
2. Check that each app name is unique
3. Check that each component in an app has required keys
4. Check that each component is a unique name across the whole config
5. Check that CLI params requiring a component use a valid component name
"""
# Check that each component name is unique across the whole config
self._find_dupe_components(components_for_app)
self._find_dupe_components(self._components_for_app)

# Check that CLI params requiring a component use a valid component name
# Check that CLI params requiring a component use a valid component name or app name
all_components = []
for _, app_components in components_for_app.items():
for _, app_components in self._components_for_app.items():
all_components.extend(app_components)

log.debug("components found: %s", all_components)
all_apps = list(self._components_for_app.keys())

self._validate_component_options(
all_components, self.template_ref_overrides, "--set-template-ref"
self._validate_selector_options(
all_apps, all_components, self.template_ref_overrides, "--set-template-ref"
)
self._validate_selector_options(
all_apps, all_components, self.param_overrides, "--set-parameter"
)
self._validate_component_options(all_components, self.param_overrides, "--set-parameter")

# 'all' is a valid component keyword for these options below
all_components.append("all")

self._validate_component_options(
all_components, self.remove_resources, "--remove-resources"
self._validate_selector_options(
all_apps, all_components, self.remove_resources, "--remove-resources"
)
self._validate_component_options(
all_components, self.no_remove_resources, "--no-remove-resources"
self._validate_selector_options(
all_apps, all_components, self.no_remove_resources, "--no-remove-resources"
)
self._validate_component_options(
all_components, self.remove_dependencies, "--remove-dependencies"
self._validate_selector_options(
all_apps, all_components, self.remove_dependencies, "--remove-dependencies"
)
self._validate_component_options(
all_components, self.no_remove_dependencies, "--no-remove-dependencies"
self._validate_selector_options(
all_apps, all_components, self.no_remove_dependencies, "--no-remove-dependencies"
)

# 'all' is a valid component keyword for this option below
all_components.append("all")
self._validate_selector_options(
all_apps, all_components, self.component_filter, "--component"
)
self._validate_component_options(all_components, self.component_filter, "--component")

def __init__(
self,
Expand All @@ -389,10 +446,10 @@ def __init__(
template_ref_overrides,
param_overrides,
clowd_env,
remove_resources,
no_remove_resources,
remove_dependencies,
no_remove_dependencies,
remove_resources: AppOrComponentSelector,
no_remove_resources: AppOrComponentSelector,
remove_dependencies: AppOrComponentSelector,
no_remove_dependencies: AppOrComponentSelector,
single_replicas,
component_filter,
local,
Expand Down Expand Up @@ -480,6 +537,11 @@ def _sub_params(self, current_component_name, params):
)
params[param_name] = value

def _get_app_for_component(self, component_name):
for app_name, components in self._components_for_app.items():
if component_name in components:
return app_name

def _get_component_items(self, component_name):
component = self._get_component_config(component_name)
try:
Expand Down Expand Up @@ -513,21 +575,21 @@ def _get_component_items(self, component_name):
# override the tags for all occurences of an image if requested
new_items = self._sub_image_tags(new_items)

remove_all_resources = "all" in self.remove_resources or not self.remove_resources
remove_all_dependencies = "all" in self.remove_dependencies

if (
"all" not in self.no_remove_resources
and (remove_all_resources or component_name in self.remove_resources)
and component_name not in self.no_remove_resources
):
# evaluate --remove-resources/--no-remove-resources
app_name = self._get_app_for_component(component_name)
should_remove_resources = _should_remove(
self.remove_resources, self.no_remove_resources, app_name, component_name
)
log.debug("should_remove_resources evaluates to %s", should_remove_resources)
if should_remove_resources:
_remove_resource_config(new_items)

if (
"all" not in self.no_remove_dependencies
and (remove_all_dependencies or component_name in self.remove_dependencies)
and component_name not in self.no_remove_dependencies
):
# evaluate --remove-dependencies/--no-remove-dependencies
should_remove_deps = _should_remove(
self.remove_dependencies, self.no_remove_dependencies, app_name, component_name
)
log.debug("should_remove_dependencies evaluates to %s", should_remove_deps)
if should_remove_deps:
_remove_dependency_config(new_items)

if self.single_replicas:
Expand Down Expand Up @@ -622,7 +684,7 @@ def _process_component(self, component_name, app_name, in_recursion):
log.debug("template already processed for component '%s'", component_name)
processed_component = self.processed_components[component_name]
else:
log.info("processing component %s", component_name)
log.info("--> processing component %s", component_name)
items = self._get_component_items(component_name)

# ignore frontends if we're not supposed to deploy them
Expand Down
Loading

0 comments on commit 18a0dea

Please sign in to comment.