# Better case import logging import os import sys from enum import Enum from pprint import pformat, pprint from types import SimpleNamespace import coloredlogs import typer from typing_extensions import Annotated from iam.app import App from iam.framework import (empty, get_app_logger, iterate_any, open_yaml, prune, to_csv, to_json, to_yaml) from rich import box from rich.console import Console from rich.pretty import pprint from rich.prompt import Prompt from rich.table import Table from . import exceptions as error # from .cli_views import OutputFormat #, view_list, view_show from .cli_views import ViewItem, ViewList IamException = error.IamException # Global vars # ====================== # See: https://docs.python.org/3.8/library/logging.html#logging-levels DEFAULT_LOG_LEVEL = 30 # warning # Get default ident DEBUG = os.environ.get("IAM_DEBUG", "False") DEFAULT_IDENT = os.environ.get("IAM_IDENT", os.environ.get("SHELL_IDENT", "")) DEFAULT_CONFIG = os.environ.get("IAM_CONFIG", None) BOX_STYLE = box.MINIMAL TABLE_MIN_WIDTH = 80 table_box_params = { "box": box.ASCII2, "min_width": 60, } # Init app # ====================== logger = logging.getLogger(__name__) console = Console() item_view = ViewItem(output=console.print) list_view = ViewList(output=console.print) # General CLI # ====================== cli = typer.Typer(rich_markup_mode="rich") @cli.callback() def cli_callback( ctx: typer.Context, verbose: Annotated[int, typer.Option("--verbose", "-v", count=True, max=3)] = 0, config: Annotated[ str, typer.Option("--config", "-C", help="Configuration directory") ] = DEFAULT_CONFIG, fmt: Annotated[ list_view.formats_enum, typer.Option("--format", "-F", help="Output format") ] = list_view.default_format, ident: str = DEFAULT_IDENT, ): # Calculate log level log_level = DEFAULT_LOG_LEVEL - verbose * 10 log_level = log_level if log_level > 0 else 10 # Define loggers loggers = { "iam": {"level": log_level}, "iam.cli": {"level": "INFO"}, } # Instanciate logger get_app_logger(loggers=loggers, level=log_level, colors=True) cli_config = SimpleNamespace( log_level=log_level, fmt=fmt, ident=ident, ) render_config = { "fmt": fmt, "table_settings": table_box_params, } render_item = lambda *args, conf={}: item_view.render( *args, conf={**render_config, **conf} ) render_list = lambda *args, conf={}: list_view.render( *args, conf={**render_config, **conf} ) # Instanciate app logger.info(f"Current ident: {ident}") ctx.obj = SimpleNamespace( app=App(config_path=config, ident=ident), cli=cli_config, render_item=render_item, render_list=render_list, ) @cli.command("dump") def main_dump(ctx: typer.Context): "Dump ident configuration" ctx.obj.app.user_dump() @cli.command("help") def main_dump(ctx: typer.Context): "Dump ident configuration" # pprint(ctx.parent.__dict__) # pprint (cli.__dict__) # pprint (dir(cli)) # # help(cli.__class__) print("commands") for item in cli.registered_commands: pprint(item.__dict__) # get_help(ctx) # print ("OUTTT") # pprint ((cli.registered_commands[0])) # pprint ((cli.registered_commands[0].__dict__)) @cli.command("list") def main_list(ctx: typer.Context): "List all items" ctx.invoke(kind_list, ctx) ctx.invoke(res_list, ctx) ctx.invoke(svc_list, ctx) # Resource Kinds management # ====================== # cli_kind = typer.Typer() class KindCmd(str, Enum): "Kinds sub commands" LS = "list" SHOW = "show" @cli.command( "kind", hidden=True, context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def cli_kind(ctx: typer.Context, cmd: KindCmd): """Command sources""" prefix = "kind_" func = globals()[f"{prefix}{cmd.value}"] return ctx.invoke(func, ctx) @cli.command("kind list", rich_help_panel="Resources Kind") def kind_list( ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", ): "List resource kinds" # Fetch data filter = name resources_kind = ctx.obj.app.catalog.resources_kind data = [] for name, item in sorted(resources_kind.items(), key=lambda key: key): if filter and not name.startswith(filter): continue data.append((name, item.desc)) # Render data columns = ["name", "desc"] conf = { "table_title": "Resources kinds listing", } ctx.obj.render_list(data, columns, conf=conf) @cli.command("kind show", rich_help_panel="Resources Kind") def kind_show( ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", ): "Show resource kinds" # Fetch data columns = ["name", "desc", "input", "needs", "remap"] filter = name resources_kind = ctx.obj.app.catalog.resources_kind for name, item in sorted(resources_kind.items(), key=lambda key: key): if filter and not name.startswith(filter): continue data = [ item.name, item.desc, f"{to_yaml(item.input).strip()}", f"{to_yaml(item.needs)}".strip(), f"{to_yaml(item.remap)}".strip(), ] # Render data conf = { "table_title": f"Resources kind: {data[0]}", } ctx.obj.render_item(data, columns, fmt=fmt, conf=conf) # Resource management # ====================== # cli_res = typer.Typer() class ResCmds(str, Enum): "Resources sub commands" LS = "list" SHOW = "show" @cli.command( "res", hidden=True, context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def cli_res(ctx: typer.Context, cmd: ResCmds): """Command sources""" pprint(ctx.__dict__) prefix = "res_" func = globals()[f"{prefix}{cmd.value}"] ctx.invoke(res_list, ctx) # ctx.invoke(func, ctx) @cli.command("res list", rich_help_panel="Resources") def res_list( ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", ): "List resource kinds" # Fetch data filter = name resources = ctx.obj.app.catalog.resources data = [] for name, res in sorted(resources.items(), key=lambda key: key): if filter and not name.startswith(filter): continue data.append( ( res.get_kind(), res.get_name(), res.desc, ) ) # Render data columns = ["kind", "name", "desc"] conf = { "table_title": "Resources listing", } ctx.obj.render_list(data, columns, conf=conf) @cli.command("res show", rich_help_panel="Resources") def res_show( ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", all: bool = False, ): "Show resource" # Fetch data columns = ["name", "uses", "input"] columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"] filter = name resources = ctx.obj.app.catalog.resources.select("startswith", name) for name, item in sorted(resources.items(), key=lambda key: key): if filter and not name.startswith(filter): continue if not all and not item.is_active(): continue data = [ name, to_yaml(item.uses).strip(), to_yaml(item.input).strip(), ] if all: data.extend( [ item.is_active(), ",".join([x.name for x in item.resources_deps]), ",".join(item.resources_missing), item.loop_limit, to_yaml(item.loop).strip(), to_yaml(item.needs).strip(), ] ) # Render data ctx.obj.render_item( data, columns_all if all else columns, conf={ "table_title": f"Resources kind: {data[0]}", }, ) # Services management # ====================== class ServiceCmds(str, Enum): "Serviceources sub commands" LS = "list" SHOW = "show" @cli.command( "svc", hidden=True, context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def cli_svc(ctx: typer.Context, cmd: ServiceCmds): """Command sources""" prefix = "svc_" func = globals()[f"{prefix}{cmd.value}"] return ctx.invoke(func, ctx) @cli.command("svc list", rich_help_panel="Services") def svc_list( ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", all: bool = False ): # filter: str = ""): "List services" columns = ["name", "desc"] filter = name services = ctx.obj.app.catalog.services data = [] for name, res in sorted(services.items(), key=lambda key: key[1].name): if filter and not name.startswith(filter): continue # if not all and not res.is_active(): # continue data.append( ( # res.get_kind(), # res.get_name(), res.name, res.desc, ) ) # Render data ctx.obj.render_list( data, columns, conf={ "table_title": f"Services listing", }, ) @cli.command("svc show", rich_help_panel="Services") def svc_show( ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", all: bool = False, ): "Show service" columns = [ "name", "desc", "enabled", "input", "commands", "required_svc", "resource_lookup", "rest_matches" "dump", ] columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"] output = {} services = ctx.obj.app.catalog.services.select("startswith", name) for name, svc in services.items(): assert name == svc.name data = [ svc.name, svc.desc, svc.enabled, svc.input, to_yaml(svc.list_cmds()).strip(), svc.required_services, svc.resources_lookup, svc.resources_matches, svc.dump_item(), ] data = [str(item) for item in data] ctx.obj.render_item( data, columns_all if all else columns, conf={ "table_title": f"Service show: {data[0]}", }, ) @cli.command("svc commands", rich_help_panel="Services") def svc_commands(ctx: typer.Context): "List services commands" ret = { "shell": {}, "cmds": {}, } for svc_name, svc in ctx.obj.app.list_services().items(): cmds = svc.list_cmds() for source in ["shell", "cmds"]: items = cmds[source] target = ret[source] for cmd_name, conf in items.items(): target[cmd_name] = conf pprint(ret, expand_all=True) @cli.command("svc run", rich_help_panel="Services") def svc_run(ctx: typer.Context, name: str, command: str): "Run service command" ret = ctx.obj.app.services.get(name, resolve=True) tmp = ret.run_cmd(command) pprint(tmp) # Shell management # ====================== class ShellCmds(str, Enum): "Shellources sub commands" ENABLE = "enable" DISABLE = "disable" ORDER = "order" IDENTS = "idents" @cli.command( "shell", hidden=True, context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def cli_shell(ctx: typer.Context, cmd: ShellCmds): """Command sources""" prefix = "shell_" func = globals()[f"{prefix}{cmd.value}"] return ctx.forward(func) # return ctx.invoke(func, ctx) @cli.command("shell idents", rich_help_panel="Shell") def shell_idents(ctx: typer.Context, filter: str = ""): "List idents" ret = ctx.obj.app.get_idents() print(" ".join(ret)) # pprint(ret, expand_all=True) @cli.command("shell order", rich_help_panel="Shell") def shell_order(ctx: typer.Context, reverse: bool = False): "List services by loading order" table = Table(title="Identity services", **table_box_params) table.add_column("Order", style="magenta") table.add_column("Service", style="cyan") # , justify="right", , no_wrap=True) # services , _ = ctx.obj.app.catalog.resolve_service_order(reverse=reverse) services, _ = ctx.obj.app.catalog.services.get_loading_order(reverse=reverse) for index, name in enumerate(services): table.add_row(str(index), name) console.print(table) @cli.command("shell enable", rich_help_panel="Shell") def shell_enable(ctx: typer.Context, ident): "Enable shell" app = ctx.obj.app # Disable existing # logger.info(f"Disabling ident: {app.ident_name}") ret1 = app.shell_disable() print("-- %< --" * 8) print(ret1) print("-- %< --" * 8) # Enable new # logger.info(f"Enabling ident: {ident}") app.init_ident(ident) ret2 = app.shell_enable() # Output print("-- %< --" * 8) print(ret2) print("-- %< --" * 8) @cli.command("shell disable", rich_help_panel="Shell") def shell_disable(ctx: typer.Context): "Disable shell" ret = ctx.obj.app.shell_disable() print("-- %< --" * 8) print(ret) print("-- %< --" * 8) # Merge all subcommands # ====================== # cli.add_typer(cli_shell, name="shell", help="Manage shell") # cli.add_typer(cli_res, name="res", help="Manage resources") # cli.add_typer(cli_svc, name="svc", help="Manage services") # cli.add_typer(cli_kind, name="kind", help="Manage resource kind") import traceback def run(): "Run cli" if DEBUG.lower() in ["true", "y", "1", "t"]: cli() else: try: cli() except IamException as err: _msg = f"Iam exited with {type(err).__name__} error: {err}" logger.error(_msg) sys.exit(1) # traceback.print_stack() # logger.warning # Cli bootsrapping # ====================== if __name__ == "__main__": run()