Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support, cost, upgrade and keycloak commands. #1468

Merged
merged 8 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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