Skip to content

Commit

Permalink
Added support, cost, upgrade and keycloak commands. (#1468)
Browse files Browse the repository at this point in the history
* 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 <eskild@doublee.io>
Co-authored-by: eskild <42120229+iameskild@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 27, 2022
1 parent 0e15458 commit ecc5f65
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 7 deletions.
47 changes: 47 additions & 0 deletions qhub/cli/_keycloak.py
Original file line number Diff line number Diff line change
@@ -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: <username> <password>"
),
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)
168 changes: 165 additions & 3 deletions qhub/cli/main.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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
Expand All @@ -24,6 +30,7 @@
TerraformStateEnum,
verify,
)
from qhub.upgrade import do_upgrade
from qhub.utils import load_yaml

SECOND_COMMAND_GROUP_NAME = "Additional Commands"
Expand All @@ -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 = (
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -183,7 +191,7 @@ def render(
help="output directory",
),
config: str = typer.Option(
None,
...,
"-c",
"--config",
help="nebari configuration yaml file path",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
7 changes: 5 additions & 2 deletions qhub/keycloak.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os

import keycloak
import rich

from .schema import verify
from .utils import load_yaml
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 3 additions & 2 deletions qhub/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down

0 comments on commit ecc5f65

Please sign in to comment.