Skip to content

Commit

Permalink
Initial commit to get bonfire working with ephemeral namespace operat…
Browse files Browse the repository at this point in the history
…or (#116)

Get bonfire working with ephemeral namespace operator

Co-authored-by: Brandon Squizzato <[email protected]>
  • Loading branch information
Jason-RH and bsquizz authored Oct 4, 2021
1 parent de562e4 commit 4802588
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 3 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ When a tester is logged in using the proper account, namespace commands can be u

Use `bonfire namespace -h` to see a list of all available namespace commands.


### NamespaceReservation CRD

`bonfire` can also be used to perform create, extend, list, and delete operations on a new NamespaceReservation CRD. This is available in the ephemeral cluster and is currently
in an 'alpha' state while we test out the new ephemeral namespace operator.

Use `bonfire reservation -h` to test it out.

### Namespace reconciler

A separate cron job runs the `bonfire namespace reconcile` command every 2 minutes. This command does the following:
Expand Down
280 changes: 278 additions & 2 deletions bonfire/bonfire.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,20 @@
find_clowd_env_for_ns,
wait_for_clowd_env_target_ns,
wait_on_cji,
wait_on_reservation,
get_reservation,
check_for_existing_reservation,
oc,
whoami,
)
from bonfire.utils import FatalError, split_equals, find_what_depends_on
from bonfire.utils import FatalError, split_equals, find_what_depends_on, validate_time_string
from bonfire.local import get_local_apps
from bonfire.processor import TemplateProcessor, process_clowd_env, process_iqe_cji
from bonfire.processor import (
TemplateProcessor,
process_clowd_env,
process_iqe_cji,
process_reservation,
)
from bonfire.namespaces import (
Namespace,
get_namespaces,
Expand Down Expand Up @@ -85,6 +95,12 @@ def apps():
pass


@main.group()
def reservation():
"""ALPHA: Perform operations related to the NamespaceReservation CRD"""
pass


def _warn_if_unsafe(namespace):
ns = Namespace(name=namespace)
if not ns.owned_by_me and not ns.available:
Expand All @@ -95,6 +111,23 @@ def _warn_if_unsafe(namespace):
sys.exit(0)


def _warn_before_delete():
if not click.confirm(
"Deleting your reservation will also delete the associated namespace. Proceed?"
):
click.echo("Aborting")
sys.exit(0)


def _warn_of_existing(requester):
if not click.confirm(
f"Existing reservation(s) detected for requester '{requester}'. "
"Do you need to reserve an additional namespace?"
):
click.echo("Aborting")
sys.exit(0)


def _reserve_namespace(duration, retries, namespace):
log.info(
"reserving ephemeral namespace%s...",
Expand Down Expand Up @@ -252,6 +285,13 @@ def _validate_resource_arguments(ctx, param, value):
return value


def _validate_reservation_duration(ctx, param, value):
try:
return validate_time_string(value)
except ValueError:
raise click.BadParameter("expecting h/m/s string. Ex: '1h30m'")


_app_source_options = [
click.option(
"--source",
Expand Down Expand Up @@ -463,6 +503,53 @@ def _validate_resource_arguments(ctx, param, value):
),
]

_reservation_process_options = [
click.option(
"--name",
type=str,
default=None,
help="Identifier for the reservation",
),
click.option(
"--requester",
"-r",
type=str,
default=None,
help="Name of the user requesting a reservation",
),
click.option(
"--duration",
"-d",
type=str,
default="1h",
help="Duration of the reservation",
callback=_validate_reservation_duration,
),
]

_reservation_lookup_options = [
click.option(
"--name",
type=str,
default=None,
help="Identifier for the reservation",
),
click.option(
"--requester",
"-r",
type=str,
default=None,
help="Name of the user requesting a reservation"
),
click.option(
"--namespace",
"-n",
type=str,
default=None,
help="Namespace for the reservation",
),
]


def options(options_list):
"""Click decorator used to set a list of click options on a command."""
Expand Down Expand Up @@ -1016,6 +1103,195 @@ def _cmd_apps_what_depends_on(
print("\n".join(found) or f"no apps depending on {component} found")


@reservation.command("create")
@click.option(
'--bot',
'-b',
is_flag=True,
help="Use this flag to skip the duplicate reservation check (for automation)",
)
@options(_reservation_process_options)
@options(_timeout_option)
def _create_new_reservation(bot, name, requester, duration, timeout):
def _err_handler(err):
msg = f"reservation failed: {str(err)}"
_error(msg)

try:
res = get_reservation(name)
# Name should be unique on reservation creation.
if res:
raise FatalError(
f"Reservation with name {name} already exists"
)

res_config = process_reservation(name, requester, duration)

log.debug("processed reservation:\n%s", res_config)

if not bot:
if check_for_existing_reservation(res_config["items"][0]["spec"]["requester"]):
_warn_of_existing(res_config["items"][0]["spec"]["requester"])

try:
res_name = res_config["items"][0]["metadata"]["name"]
except (KeyError, IndexError):
raise Exception(
"error parsing name of Reservation from processed template, "
"check Reservation template"
)

apply_config(None, list_resource=res_config)

ns_name = wait_on_reservation(res_name, timeout)
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(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)
else:
log.info(
"namespace '%s' is reserved by '%s' for '%s'",
ns_name,
res_config["items"][0]["spec"]["requester"],
duration,
)
click.echo(ns_name)


@reservation.command("extend")
@click.option(
'--duration',
'-d',
type=str,
default='1h',
help='Amount of time to extend the reservation',
callback=_validate_reservation_duration,
)
@options(_reservation_lookup_options)
def _extend_reservation(name, namespace, requester, duration):
def _err_handler(err):
msg = f"reservation extension failed: {str(err)}"
_error(msg)

try:
res = get_reservation(name, namespace, requester)
if res:
res_config = process_reservation(
res["metadata"]["name"],
res["spec"]["requester"],
duration,
)

log.debug("processed reservation:\n%s", res_config)

apply_config(None, list_resource=res_config)
else:
raise FatalError("Reservation lookup 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)
_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)
else:
log.info(
"reservation '%s' extended by '%s'", res["metadata"]["name"], duration
)


@reservation.command("delete")
@options(_reservation_lookup_options)
def _delete_reservation(name, namespace, requester):
def _err_handler(err):
msg = f"reservation deletion failed: {str(err)}"
_error(msg)

try:
res = get_reservation(name, namespace, requester)
if res:
_warn_before_delete()
res_name = res["metadata"]["name"]
log.info("deleting reservation '%s'", res_name)
oc("delete", "reservation", res_name)
log.info("reservation '%s' deleted", res_name)
else:
raise FatalError("Reservation lookup 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)
_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)


@reservation.command("list")
@click.option(
'--mine',
'-m',
is_flag=True,
help='Return reservations belonging to the result of oc whoami',
)
@click.option(
'--requester',
'-r',
type=str,
default=None,
help='Return reservations belonging to the provided requester',
)
def _list_reservations(mine, requester):
def _err_handler(err):
msg = f"reservation listing failed: {str(err)}"
_error(msg)

try:
if mine:
try:
requester = whoami()
except Exception:
log.info(
"whoami returned an error - getting reservations for 'bonfire'"
) # minikube
requester = "bonfire"
oc("get", "reservation", "--selector", f"requester={requester}")
else:
if requester:
oc("get", "reservation", "--selector", f"requester={requester}")
else:
oc("get", "reservation")
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(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)


def main_with_handler():
try:
main()
Expand Down
1 change: 1 addition & 0 deletions bonfire/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def _get_config_path():
)
DEFAULT_IQE_CJI_TEMPLATE = resource_filename("bonfire", "resources/default-iqe-cji.yaml")
DEFAULT_CONFIG_DATA = resource_filename("bonfire", "resources/default_config.yaml")
DEFAULT_RESERVATION_TEMPLATE = resource_filename("bonfire", "resources/reservation-template.yaml")

LOCAL_GRAPHQL_URL = "http://localhost:4000/graphql"

Expand Down
Loading

0 comments on commit 4802588

Please sign in to comment.