From ecc5f6570d5c74cdd1bc68b835ea235d24ccdbc7 Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Tue, 27 Sep 2022 09:00:50 +0530 Subject: [PATCH] Added `support`, `cost`, `upgrade` and `keycloak` commands. (#1468) * Add , , and commands. * Add pre-commit * Add pre-commit * Add pre-commit * Add rich print * Change do_keycloak function * Create two keycloak subcommands Co-authored-by: iameskild Co-authored-by: eskild <42120229+iameskild@users.noreply.github.com> --- qhub/cli/_keycloak.py | 47 ++++++++++++ qhub/cli/main.py | 168 +++++++++++++++++++++++++++++++++++++++++- qhub/keycloak.py | 7 +- qhub/upgrade.py | 5 +- 4 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 qhub/cli/_keycloak.py diff --git a/qhub/cli/_keycloak.py b/qhub/cli/_keycloak.py new file mode 100644 index 000000000..0ae76c212 --- /dev/null +++ b/qhub/cli/_keycloak.py @@ -0,0 +1,47 @@ +from pathlib import Path +from typing import Tuple + +import typer + +from qhub.keycloak import do_keycloak + +app_keycloak = typer.Typer() + + +@app_keycloak.command() +def add_user( + add_users: Tuple[str, str] = typer.Option( + ..., "--user", help="Provide both: " + ), + config_filename: str = typer.Option( + ..., + "-c", + "--config", + help="qhub configuration file path", + ), +): + """Add a user to Keycloak. User will be automatically added to the [italic]analyst[/italic] group.""" + if isinstance(config_filename, str): + config_filename = Path(config_filename) + + args = ["adduser", add_users[0], add_users[1]] + + do_keycloak(config_filename, *args) + + +@app_keycloak.command() +def list_users( + config_filename: str = typer.Option( + ..., + "-c", + "--config", + help="qhub configuration file path", + ) +): + """List the users in Keycloak.""" + if isinstance(config_filename, str): + config_filename = Path(config_filename) + + args = ["listusers"] + + do_keycloak(config_filename, *args) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index c8cc766d7..e58d5ac09 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -1,8 +1,12 @@ from pathlib import Path +from zipfile import ZipFile import typer from click import Context +from kubernetes import client +from kubernetes import config as kube_config from rich import print +from ruamel import yaml from typer.core import TyperGroup from qhub.cli._init import ( @@ -13,6 +17,8 @@ guided_init_wizard, handle_init, ) +from qhub.cli._keycloak import app_keycloak +from qhub.cost import infracost_report from qhub.deploy import deploy_configuration from qhub.destroy import destroy_configuration from qhub.render import render_template @@ -24,6 +30,7 @@ TerraformStateEnum, verify, ) +from qhub.upgrade import do_upgrade from qhub.utils import load_yaml SECOND_COMMAND_GROUP_NAME = "Additional Commands" @@ -43,6 +50,7 @@ def list_commands(self, ctx: Context): rich_markup_mode="rich", context_settings={"help_option_names": ["-h", "--help"]}, ) +app.add_typer(app_keycloak, name="keycloak", help="keycloak") guided_init_help_msg = ( @@ -145,7 +153,7 @@ def init( @app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) def validate( config: str = typer.Option( - None, + ..., "--config", "-c", help="qhub configuration yaml file path, please pass in as -c/--config flag", @@ -183,7 +191,7 @@ def render( help="output directory", ), config: str = typer.Option( - None, + ..., "-c", "--config", help="nebari configuration yaml file path", @@ -275,7 +283,9 @@ def deploy( @app.command() def destroy( - config: str = typer.Option(..., "-c", "--config", help="qhub configuration"), + config: str = typer.Option( + ..., "-c", "--config", help="qhub configuration file path" + ), output: str = typer.Option( "./" "-o", "--output", @@ -310,5 +320,157 @@ def destroy( destroy_configuration(config_yaml) +@app.command() +def cost( + path: str = typer.Option( + None, + "-p", + "--path", + help="Pass the path of your stages directory generated after rendering QHub configurations before deployment", + ), + dashboard: bool = typer.Option( + True, + "-d", + "--dashboard", + help="Enable the cost dashboard", + ), + file: str = typer.Option( + None, + "-f", + "--file", + help="Specify the path of the file to store the cost report", + ), + currency: str = typer.Option( + "USD", + "-c", + "--currency", + help="Specify the currency code to use in the cost report", + ), + compare: bool = typer.Option( + False, + "-cc", + "--compare", + help="Compare the cost report to a previously generated report", + ), +): + """ + Cost-Estimate + """ + infracost_report( + path=path, + dashboard=True, + file=file, + currency_code=currency, + compare=False, + ) + + +@app.command() +def upgrade( + config: str = typer.Option( + ..., + "-c", + "--config", + help="qhub configuration file path", + ), + attempt_fixes: bool = typer.Option( + False, + "--attempt-fixes", + help="Attempt to fix the config for any incompatibilities between your old and new QHub versions.", + ), +): + """ + Upgrade + """ + config_filename = Path(config) + if not config_filename.is_file(): + raise ValueError( + f"passed in configuration filename={config_filename} must exist" + ) + + do_upgrade(config_filename, attempt_fixes=attempt_fixes) + + +@app.command() +def support( + config_filename: str = typer.Option( + ..., + "-c", + "--config", + help="qhub configuration file path", + ), + output: str = typer.Option( + "./qhub-support-logs.zip", + "-o", + "--output", + help="output filename", + ), +): + """ + Support + """ + + kube_config.load_kube_config() + + v1 = client.CoreV1Api() + + namespace = get_config_namespace(config=config_filename) + + pods = v1.list_namespaced_pod(namespace=namespace) + + for pod in pods.items: + Path(f"./log/{namespace}").mkdir(parents=True, exist_ok=True) + path = Path(f"./log/{namespace}/{pod.metadata.name}.txt") + with path.open(mode="wt") as file: + try: + file.write( + "%s\t%s\t%s\n" + % ( + pod.status.pod_ip, + namespace, + pod.metadata.name, + ) + ) + + # some pods are running multiple containers + containers = [ + _.name if len(pod.spec.containers) > 1 else None + for _ in pod.spec.containers + ] + + for container in containers: + if container is not None: + file.write(f"Container: {container}\n") + file.write( + v1.read_namespaced_pod_log( + name=pod.metadata.name, + namespace=namespace, + container=container, + ) + ) + + except client.exceptions.ApiException as e: + file.write("%s not available" % pod.metadata.name) + raise e + + with ZipFile(output, "w") as zip: + for file in list(Path(f"./log/{namespace}").glob("*.txt")): + print(file) + zip.write(file) + + +def get_config_namespace(config): + config_filename = Path(config) + if not config_filename.is_file(): + raise ValueError( + f"passed in configuration filename={config_filename} must exist" + ) + + with config_filename.open() as f: + config = yaml.safe_load(f.read()) + + return config["namespace"] + + if __name__ == "__main__": app() diff --git a/qhub/keycloak.py b/qhub/keycloak.py index f836578da..7934f566b 100644 --- a/qhub/keycloak.py +++ b/qhub/keycloak.py @@ -2,6 +2,7 @@ import os import keycloak +import rich from .schema import verify from .utils import load_yaml @@ -55,9 +56,11 @@ def create_user( {"type": "password", "value": password, "temporary": False} ] else: - print(f"Creating user={username} without password (none supplied)") + rich.print( + f"Creating user=[green]{username}[/green] without password (none supplied)" + ) keycloak_admin.create_user(payload) - print(f"Created user={username}") + rich.print(f"Created user=[green]{username}[/green]") def list_users(keycloak_admin: keycloak.KeycloakAdmin): diff --git a/qhub/upgrade.py b/qhub/upgrade.py index 4cb5a6ac1..c2c959a29 100644 --- a/qhub/upgrade.py +++ b/qhub/upgrade.py @@ -6,6 +6,7 @@ import string from abc import ABC +import rich from pydantic.error_wrappers import ValidationError from .schema import is_version_accepted, verify @@ -21,8 +22,8 @@ def do_upgrade(config_filename, attempt_fixes=False): try: verify(config) - print( - f"Your config file {config_filename} appears to be already up-to-date for qhub version {__version__}" + rich.print( + f"Your config file [purple]{config_filename}[/purple] appears to be already up-to-date for qhub version [green]{__version__}[/green]" ) return except (ValidationError, ValueError) as e: