2023-10-06 19:16:30 -04:00

586 lines
14 KiB
Python

# 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()