From a122688e39ea8949878e46c2abee73dc325bbac6 Mon Sep 17 00:00:00 2001 From: iameskild Date: Sun, 28 Aug 2022 21:10:15 -0700 Subject: [PATCH 01/43] Initial typer CLI implmentation --- qhub/cli/_init.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ qhub/cli/main.py | 93 ++++++++++++++++++++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 205 insertions(+) create mode 100644 qhub/cli/_init.py create mode 100644 qhub/cli/main.py diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py new file mode 100644 index 000000000..f29ec08af --- /dev/null +++ b/qhub/cli/_init.py @@ -0,0 +1,111 @@ +import os + +import typer + +from qhub.schema import ProviderEnum + + +def check_cloud_provider_creds(cloud_provider: str): + + cloud_provider = cloud_provider.lower() + + if cloud_provider == ProviderEnum.aws.value and ( + not os.environ.get("AWS_ACCESS_KEY_ID") + or not os.environ.get("AWS_SECRET_ACCESS_KEY") + ): + print( + "\nUnable to location AWS credentials, please generate your AWS keys at this link: aws.com/..." + ) + os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( + "Please enter your AWS_ACCESS_KEY_ID", + hide_input=True, + ) + os.environ["AWS_SECRET_ACCESS_KEY"] = typer.prompt( + "Please enter your AWS_SECRET_ACCESS_KEY", + hide_input=True, + ) + + elif cloud_provider == ProviderEnum.gcp.value and ( + not os.environ.get("GOOGLE_CREDENTIALS") or not os.environ.get("PROJECT_ID") + ): + print( + "\nUnable to location AWS credentials,please generate your GCP credentials at this link: [link=https://cloud.google.com/iam/docs/creating-managing-service-accounts][/link]" + ) + os.environ["GOOGLE_CREDENTIALS"] = typer.prompt( + "Please enter your GOOGLE_CREDENTIALS", + hide_input=True, + ) + os.environ["PROJECT_ID"] = typer.prompt( + "Please enter your PROJECT_ID", + hide_input=True, + ) + + elif cloud_provider == ProviderEnum.do.value and ( + not os.environ.get("DIGITALOCEAN_TOKEN") + or not os.environ.get("SPACES_ACCESS_KEY_ID") + or not os.environ.get("SPACES_SECRET_ACCESS_KEY") + or not os.environ.get("AWS_ACCESS_KEY_ID") + or not os.environ.get("AWS_SECRET_ACCESS_KEY") + ): + print( + "\nUnable to location AWS credentials, please generate your Digital Ocean token at this link: [link=https://docs.digitalocean.com/reference/api/create-personal-access-token/][/link]" + ) + os.environ["DIGITALOCEAN_TOKEN"] = typer.prompt( + "Please enter your DIGITALOCEAN_TOKEN", + hide_input=True, + ) + os.environ["SPACES_ACCESS_KEY_ID"] = typer.prompt( + "Please enter your SPACES_ACCESS_KEY_ID", + ) + os.environ["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( + "Please enter your SPACES_SECRET_ACCESS_KEY", + hide_input=True, + ) + os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( + "Please set this variable with the same value as `AWS_ACCESS_KEY_ID`", + hide_input=True, + ) + os.environ["AWS_SECRET_ACCESS_KEY"] = typer.prompt( + "Please set this variable with the same value as `AWS_SECRET_ACCESS_KEY`", + hide_input=True, + ) + + elif ( + cloud_provider == "AZURE" + and not os.environ.get("ARM_CLIENT_ID") + and not os.environ.get("ARM_CLIENT_SECRET") + and not os.environ.get("ARM_SUBSCRIPTION_ID") + and not os.environ.get("ARM_TENANT_ID") + ): + print( + "Please generate your AWS keys at this link: [link=https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret#creating-a-service-principal-in-the-azure-portal][/link]" + ) + os.environ["ARM_CLIENT_ID"] = typer.prompt( + "Please enter your ARM_CLIENT_ID", + hide_input=True, + ) + os.environ["ARM_CLIENT_SECRET"] = typer.prompt( + "Please enter your ARM_CLIENT_SECRET", + hide_input=True, + ) + os.environ["ARM_SUBSCRIPTION_ID"] = typer.prompt( + "Please enter your ARM_SUBSCRIPTION_ID", + hide_input=True, + ) + os.environ["ARM_TENANT_ID"] = typer.prompt( + "Please enter your ARM_TENANT_ID", + hide_input=True, + ) + + +# testing questionary package +# def auth_provider_options(auth_provider): +# import questionary + +# if auth_provider == None: +# auth_provider = questionary.select( +# "auth provider ...", +# choices=["password", "github", "auth0"], +# ).ask() + +# return auth_provider diff --git a/qhub/cli/main.py b/qhub/cli/main.py new file mode 100644 index 000000000..daa077d90 --- /dev/null +++ b/qhub/cli/main.py @@ -0,0 +1,93 @@ +import typer + +from qhub.cli._init import check_cloud_provider_creds +from qhub.schema import ProviderEnum + + +def enum_to_list(enum_cls): + return [e.value for e in enum_cls] + + +app = typer.Typer( + help="Nebari CLI 🪴", + add_completion=False, + context_settings={"help_option_names": ["-h", "--help"]}, +) + + +@app.command() +def init( + cloud_provider: str = typer.Argument( + ..., + help=f"options: {enum_to_list(ProviderEnum)}", + callback=check_cloud_provider_creds, + ), + project_name: str = typer.Option( + None, + "--project-name", + "--project", + "-p", + prompt=True, + ), + domain_name: str = typer.Option( + None, + "--domain-name", + "--domain", + "-d", + prompt=True, + ), + auth_provider: str = typer.Option( + "password", + prompt=True, + # callback=auth_provider_options + ), +): + """ + Initialize the nebari-config.yaml file. + + """ + print(f"Cloud provider: {cloud_provider}") + print(f"Project name: {project_name}") + print(f"Domain name: {domain_name}") + print(f"Auth provider: {auth_provider}") + + +@app.command() +def validate(): + """ + Validate the config.yaml file. + + """ + print("Validate the config.yaml file") + + +@app.command() +def render(): + """ + Render the config.yaml file. + """ + print("Render the congig.yaml file") + + +@app.command() +def deploy(): + """ + Deploy the nebari + """ + print("Deploy the Nebari") + + +@app.command() +def destroy(): + """ + Destroy the nebari + """ + print("Destroy the Nebari") + + +def main(): + app() + + +if __name__ == "__main__": + app() diff --git a/setup.cfg b/setup.cfg index 64c312c53..314d5a97c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,6 +57,7 @@ dev = [options.entry_points] console_scripts = qhub = qhub.__main__:main + nebari = qhub.cli.main:app [tool:pytest] norecursedirs = _build .nox .ipynb_checkpoints From eb8523fead50698021aabe2907cd8fd148f2b7ab Mon Sep 17 00:00:00 2001 From: iameskild Date: Sun, 28 Aug 2022 21:54:19 -0700 Subject: [PATCH 02/43] edit logic --- qhub/cli/_init.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index f29ec08af..dc0e07565 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -70,15 +70,14 @@ def check_cloud_provider_creds(cloud_provider: str): hide_input=True, ) - elif ( - cloud_provider == "AZURE" - and not os.environ.get("ARM_CLIENT_ID") - and not os.environ.get("ARM_CLIENT_SECRET") - and not os.environ.get("ARM_SUBSCRIPTION_ID") - and not os.environ.get("ARM_TENANT_ID") + elif cloud_provider == "AZURE" and ( + not os.environ.get("ARM_CLIENT_ID") + or not os.environ.get("ARM_CLIENT_SECRET") + or not os.environ.get("ARM_SUBSCRIPTION_ID") + or not os.environ.get("ARM_TENANT_ID") ): print( - "Please generate your AWS keys at this link: [link=https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret#creating-a-service-principal-in-the-azure-portal][/link]" + "\nUnable to location AWS credentials, please generate your AWS keys at this link: [link=https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret#creating-a-service-principal-in-the-azure-portal][/link]" ) os.environ["ARM_CLIENT_ID"] = typer.prompt( "Please enter your ARM_CLIENT_ID", From 731bc149c779cb4a18330bd3e704674e77bc3da6 Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Sat, 3 Sep 2022 04:51:06 +0530 Subject: [PATCH 03/43] Nebari cli: Add files _init.py and main.py (#1423) * Nebari cli: Add files _init.py and main.py * run pre-commit Co-authored-by: iameskild --- qhub/cli/_init.py | 8 +++++++- qhub/cli/main.py | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index dc0e07565..23fa6eaed 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -1,12 +1,18 @@ import os import typer +from rich import print from qhub.schema import ProviderEnum def check_cloud_provider_creds(cloud_provider: str): + print("[green]Initializing the Nebari 🚀 [/green]") + print( + "\n[green]Note: Values that the user assign for each arguments will be reflected in the [red]config.yaml[/red] file. Later you can update by using[blue] nebari update[/blue] command [/green]" + ) + cloud_provider = cloud_provider.lower() if cloud_provider == ProviderEnum.aws.value and ( @@ -14,7 +20,7 @@ def check_cloud_provider_creds(cloud_provider: str): or not os.environ.get("AWS_SECRET_ACCESS_KEY") ): print( - "\nUnable to location AWS credentials, please generate your AWS keys at this link: aws.com/..." + "Unable to location AWS credentials, please generate your AWS keys at this link:[blue] https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create.html[/blue]" ) os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( "Please enter your AWS_ACCESS_KEY_ID", diff --git a/qhub/cli/main.py b/qhub/cli/main.py index daa077d90..f56cb0198 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -26,14 +26,12 @@ def init( None, "--project-name", "--project", - "-p", prompt=True, ), domain_name: str = typer.Option( None, "--domain-name", "--domain", - "-d", prompt=True, ), auth_provider: str = typer.Option( @@ -41,15 +39,41 @@ def init( prompt=True, # callback=auth_provider_options ), + namespace: str = typer.Option( + "dev", + prompt=True, + # callback=auth_provider_options + ), + repository: str = typer.Option( + None, + prompt=True, + # callback=auth_provider_options + ), + ci_provider: str = typer.Option( + "github-actions", + prompt=True, + # callback=auth_provider_options + ), + terraform_state: str = typer.Option( + "remote", + prompt=True, + # callback=auth_provider_options + ), + kubernetes_version: str = typer.Option( + "latest", + prompt=True, + # callback=auth_provider_options + ), + ssl_cert: str = typer.Option( + "email", + prompt=True, + # callback=auth_provider_options + ), ): """ Initialize the nebari-config.yaml file. """ - print(f"Cloud provider: {cloud_provider}") - print(f"Project name: {project_name}") - print(f"Domain name: {domain_name}") - print(f"Auth provider: {auth_provider}") @app.command() From 372fa850ae62fb40d6ef80ef4c641b23d76ce796 Mon Sep 17 00:00:00 2001 From: iameskild Date: Tue, 6 Sep 2022 20:50:54 -0700 Subject: [PATCH 04/43] Order CLI commands --- qhub/cli/main.py | 15 +++++++++++---- setup.cfg | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index f56cb0198..035eeaf93 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -1,4 +1,6 @@ import typer +from click import Context +from typer.core import TyperGroup from qhub.cli._init import check_cloud_provider_creds from qhub.schema import ProviderEnum @@ -8,9 +10,17 @@ def enum_to_list(enum_cls): return [e.value for e in enum_cls] +class OrderCommands(TyperGroup): + def list_commands(self, ctx: Context): + """Return list of commands in the order appear.""" + return list(self.commands) + + app = typer.Typer( + cls=OrderCommands, help="Nebari CLI 🪴", add_completion=False, + no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}, ) @@ -27,6 +37,7 @@ def init( "--project-name", "--project", prompt=True, + # callback=project_name_convention ), domain_name: str = typer.Option( None, @@ -109,9 +120,5 @@ def destroy(): print("Destroy the Nebari") -def main(): - app() - - if __name__ == "__main__": app() diff --git a/setup.cfg b/setup.cfg index 314d5a97c..6ed83f96a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,7 +57,7 @@ dev = [options.entry_points] console_scripts = qhub = qhub.__main__:main - nebari = qhub.cli.main:app + nebari = qhub.cli.main.__main__ [tool:pytest] norecursedirs = _build .nox .ipynb_checkpoints From a9fac683bce349c43dc3189d6f57d260506c0f63 Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Mon, 12 Sep 2022 20:14:36 +0530 Subject: [PATCH 05/43] Nebari typer cli commands (#1432) * Nebari typer cli commands * add validate command * Change validate command * add precommit --- qhub/cli/main.py | 38 +++++++++++++++++++++++++++++++++----- setup.cfg | 2 +- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 035eeaf93..21c3559e2 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -1,9 +1,12 @@ +import pathlib + import typer from click import Context from typer.core import TyperGroup from qhub.cli._init import check_cloud_provider_creds -from qhub.schema import ProviderEnum +from qhub.schema import ProviderEnum, verify +from qhub.utils import load_yaml def enum_to_list(enum_cls): @@ -82,18 +85,44 @@ def init( ), ): """ - Initialize the nebari-config.yaml file. + Initialize nebari-config.yaml file. """ @app.command() -def validate(): +def validate( + config: str = typer.Option( + None, + "--config", + "-c", + help="qhub configuration yaml file path, please pass in as -c/--config flag", + ), + enable_commenting: bool = typer.Option( + False, "--enable_commenting", help="Toggle PR commenting on GitHub Actions" + ), +): """ Validate the config.yaml file. """ - print("Validate the config.yaml file") + # print(f"Validate the {config}") + + config_filename = pathlib.Path(config) + if not config_filename.is_file(): + raise ValueError( + f"Passed in configuration filename={config_filename} must exist." + ) + + config = load_yaml(config_filename) + + if enable_commenting: + # for PR's only + # comment_on_pr(config) + pass + else: + verify(config) + print("Successfully validated configuration") @app.command() @@ -101,7 +130,6 @@ def render(): """ Render the config.yaml file. """ - print("Render the congig.yaml file") @app.command() diff --git a/setup.cfg b/setup.cfg index 6ed83f96a..314d5a97c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,7 +57,7 @@ dev = [options.entry_points] console_scripts = qhub = qhub.__main__:main - nebari = qhub.cli.main.__main__ + nebari = qhub.cli.main:app [tool:pytest] norecursedirs = _build .nox .ipynb_checkpoints From a4453893483211a053975eeefef3a44429cf0699 Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Tue, 13 Sep 2022 21:18:36 +0530 Subject: [PATCH 06/43] Nebari Render Command (#1433) * Nebari Render Command * add render_template * Add dry-run --- qhub/cli/main.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 21c3559e2..6bcc21f2f 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -5,6 +5,7 @@ from typer.core import TyperGroup from qhub.cli._init import check_cloud_provider_creds +from qhub.render import render_template from qhub.schema import ProviderEnum, verify from qhub.utils import load_yaml @@ -126,10 +127,41 @@ def validate( @app.command() -def render(): +def render( + output: str = typer.Option( + "./", + "-o", + "--output", + help="output directory", + ), + config: str = typer.Option( + None, + "-c", + "--config", + help="qhub configuration yaml file", + ), + dry_run: bool = typer.Option( + False, + "--dry-run", + help="simulate rendering files without actually writing or updating any files", + ) + # TODO: debug why dry-run is not working? +): """ - Render the config.yaml file. + Dynamically render terraform scripts and other files from the nebari-config.yaml """ + config_filename = pathlib.Path(config) + + if not config_filename.is_file(): + raise ValueError( + f"passed in configuration filename={config_filename} must exist" + ) + + config_yaml = load_yaml(config_filename) + + verify(config_yaml) + + render_template(output, config, force=True, dry_run=dry_run) @app.command() From c6142d05560040e70c220190fcf7a769928eac1f Mon Sep 17 00:00:00 2001 From: iameskild Date: Thu, 15 Sep 2022 17:15:21 -0700 Subject: [PATCH 07/43] Updates to init --- qhub/cli/_init.py | 148 ++++++++++++++++++++++++++++++++++------------ qhub/cli/main.py | 25 ++++---- 2 files changed, 123 insertions(+), 50 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 23fa6eaed..2321fd630 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -1,27 +1,45 @@ import os +import rich import typer -from rich import print -from qhub.schema import ProviderEnum +from qhub.schema import AuthenticationEnum, ProviderEnum +MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, please refer to this guide on how to generate them:\n[light_green]{link_to_docs}[/light_green]\n" + +# links to external docs +CREATE_AWS_CREDS = ( + "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html" +) +CREATE_GCP_CREDS = ( + "https://cloud.google.com/iam/docs/creating-managing-service-accounts" +) +CREATE_DO_CREDS = ( + "https://docs.digitalocean.com/reference/api/create-personal-access-token" +) +CREATE_AZURE_CREDS = "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret#creating-a-service-principal-in-the-azure-portal" +CREATE_AUTH0_CREDS = "https://auth0.com/docs/get-started/auth0-overview/create-applications/machine-to-machine-apps" +CREATE_GITHUB_OAUTH_CREDS = "https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app" -def check_cloud_provider_creds(cloud_provider: str): - print("[green]Initializing the Nebari 🚀 [/green]") - print( - "\n[green]Note: Values that the user assign for each arguments will be reflected in the [red]config.yaml[/red] file. Later you can update by using[blue] nebari update[/blue] command [/green]" - ) +def check_cloud_provider_creds(cloud_provider: str): + """Validate that the necessary cloud credentials have been set as environment variables.""" cloud_provider = cloud_provider.lower() - if cloud_provider == ProviderEnum.aws.value and ( + rich.print("Creating and initializing your nebari-config.yaml :rocket:\n") + + # AWS + if cloud_provider == ProviderEnum.aws.value.lower() and ( not os.environ.get("AWS_ACCESS_KEY_ID") or not os.environ.get("AWS_SECRET_ACCESS_KEY") ): - print( - "Unable to location AWS credentials, please generate your AWS keys at this link:[blue] https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create.html[/blue]" + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Amazon Web Services", link_to_docs=CREATE_AWS_CREDS + ) ) + os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( "Please enter your AWS_ACCESS_KEY_ID", hide_input=True, @@ -31,12 +49,16 @@ def check_cloud_provider_creds(cloud_provider: str): hide_input=True, ) - elif cloud_provider == ProviderEnum.gcp.value and ( + # GCP + elif cloud_provider == ProviderEnum.gcp.value.lower() and ( not os.environ.get("GOOGLE_CREDENTIALS") or not os.environ.get("PROJECT_ID") ): - print( - "\nUnable to location AWS credentials,please generate your GCP credentials at this link: [link=https://cloud.google.com/iam/docs/creating-managing-service-accounts][/link]" + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Google Cloud Provider", link_to_docs=CREATE_GCP_CREDS + ) ) + os.environ["GOOGLE_CREDENTIALS"] = typer.prompt( "Please enter your GOOGLE_CREDENTIALS", hide_input=True, @@ -46,16 +68,18 @@ def check_cloud_provider_creds(cloud_provider: str): hide_input=True, ) - elif cloud_provider == ProviderEnum.do.value and ( + # DO + elif cloud_provider == ProviderEnum.do.value.lower() and ( not os.environ.get("DIGITALOCEAN_TOKEN") or not os.environ.get("SPACES_ACCESS_KEY_ID") or not os.environ.get("SPACES_SECRET_ACCESS_KEY") - or not os.environ.get("AWS_ACCESS_KEY_ID") - or not os.environ.get("AWS_SECRET_ACCESS_KEY") ): - print( - "\nUnable to location AWS credentials, please generate your Digital Ocean token at this link: [link=https://docs.digitalocean.com/reference/api/create-personal-access-token/][/link]" + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Digital Ocean", link_to_docs=CREATE_DO_CREDS + ) ) + os.environ["DIGITALOCEAN_TOKEN"] = typer.prompt( "Please enter your DIGITALOCEAN_TOKEN", hide_input=True, @@ -67,24 +91,20 @@ def check_cloud_provider_creds(cloud_provider: str): "Please enter your SPACES_SECRET_ACCESS_KEY", hide_input=True, ) - os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( - "Please set this variable with the same value as `AWS_ACCESS_KEY_ID`", - hide_input=True, - ) - os.environ["AWS_SECRET_ACCESS_KEY"] = typer.prompt( - "Please set this variable with the same value as `AWS_SECRET_ACCESS_KEY`", - hide_input=True, - ) - elif cloud_provider == "AZURE" and ( + # AZURE + elif cloud_provider == ProviderEnum.azure.value.lower() and ( not os.environ.get("ARM_CLIENT_ID") or not os.environ.get("ARM_CLIENT_SECRET") or not os.environ.get("ARM_SUBSCRIPTION_ID") or not os.environ.get("ARM_TENANT_ID") ): - print( - "\nUnable to location AWS credentials, please generate your AWS keys at this link: [link=https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret#creating-a-service-principal-in-the-azure-portal][/link]" + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Azure", link_to_docs=CREATE_AZURE_CREDS + ) ) + os.environ["ARM_CLIENT_ID"] = typer.prompt( "Please enter your ARM_CLIENT_ID", hide_input=True, @@ -102,15 +122,67 @@ def check_cloud_provider_creds(cloud_provider: str): hide_input=True, ) + return cloud_provider + + +def check_auth_provider_creds(auth_provider: str): + """Validating the the necessary auth provider credentials have been set as environment variables.""" + + auth_provider = auth_provider.lower() -# testing questionary package -# def auth_provider_options(auth_provider): -# import questionary + # Auth0 + if auth_provider == AuthenticationEnum.auth0.value.lower() and ( + not os.environ.get("AUTH0_CLIENT_ID") + or not os.environ.get("AUTH0_CLIENT_SECRET") + or not os.environ.get("AUTH0_DOMAIN") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Auth0", link_to_docs=CREATE_AUTH0_CREDS + ) + ) + + os.environ["AUTH0_CLIENT_ID"] = typer.prompt( + "Please enter your AUTH0_CLIENT_ID", + hide_input=True, + ) + os.environ["AUTH0_CLIENT_SECRET"] = typer.prompt( + "Please enter your AUTH0_CLIENT_SECRET", + hide_input=True, + ) + os.environ["AUTH0_DOMAIN"] = typer.prompt( + "Please enter your AUTH0_DOMAIN", + hide_input=True, + ) + + # GitHub + elif auth_provider == AuthenticationEnum.github.value.lower() and ( + not os.environ.get("GITHUB_CLIENT_ID") + or not os.environ.get("GITHUB_CLIENT_SECRET") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="GitHub OAuth App", link_to_docs=CREATE_GITHUB_OAUTH_CREDS + ) + ) + rich.print( + ( + ":warning: If you haven't done so already, please ensure the following:\n" + "The `Homepage URL` is set to: [light_green]https://[/light_green]\n" + "The `Authorization callback URL` is set to: [light_green]https:///auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" + "* `[light_green][/light_green]` should be the same one you provided above" + ) + ) + + os.environ["GITHUB_CLIENT_ID"] = typer.prompt( + "Please enter your GITHUB_CLIENT_ID", + hide_input=True, + ) + os.environ["GITHUB_CLIENT_SECRET"] = typer.prompt( + "Please enter your GITHUB_CLIENT_SECRET", + hide_input=True, + ) -# if auth_provider == None: -# auth_provider = questionary.select( -# "auth provider ...", -# choices=["password", "github", "auth0"], -# ).ask() + print(os.environ["GITHUB_CLIENT_ID"]) -# return auth_provider + return auth_provider diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 6bcc21f2f..3fb069492 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -4,9 +4,9 @@ from click import Context from typer.core import TyperGroup -from qhub.cli._init import check_cloud_provider_creds +from qhub.cli._init import check_auth_provider_creds, check_cloud_provider_creds from qhub.render import render_template -from qhub.schema import ProviderEnum, verify +from qhub.schema import AuthenticationEnum, ProviderEnum, verify from qhub.utils import load_yaml @@ -37,27 +37,28 @@ def init( callback=check_cloud_provider_creds, ), project_name: str = typer.Option( - None, + ..., "--project-name", "--project", - prompt=True, - # callback=project_name_convention + "-p", + prompt="Please enter your desired project name", ), domain_name: str = typer.Option( - None, + ..., "--domain-name", "--domain", - prompt=True, + "-d", + prompt="Please enter your desired domain name", ), auth_provider: str = typer.Option( "password", - prompt=True, - # callback=auth_provider_options + prompt="Please enter your desired authentication provider", + help=f"options: {enum_to_list(AuthenticationEnum)}", + callback=check_auth_provider_creds, ), namespace: str = typer.Option( "dev", prompt=True, - # callback=auth_provider_options ), repository: str = typer.Option( None, @@ -86,9 +87,9 @@ def init( ), ): """ - Initialize nebari-config.yaml file. - + Create and initialize your nebari-config.yaml file. """ + pass @app.command() From 9164c207c0469ac6c9e0b85eb359cbf00b4a237e Mon Sep 17 00:00:00 2001 From: iameskild Date: Sat, 17 Sep 2022 19:59:08 -0700 Subject: [PATCH 08/43] More work on init --- qhub/cli/_init.py | 169 ++++++++++++++++++++++++++++++---------------- qhub/cli/main.py | 77 +++++++++++++++++---- qhub/schema.py | 78 +++++++++++---------- 3 files changed, 215 insertions(+), 109 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 2321fd630..8826a2623 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -1,9 +1,11 @@ import os +from pathlib import Path import rich import typer +from dotenv import load_dotenv -from qhub.schema import AuthenticationEnum, ProviderEnum +from qhub.schema import AuthenticationEnum, ProviderEnum, project_name_convention MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, please refer to this guide on how to generate them:\n[light_green]{link_to_docs}[/light_green]\n" @@ -22,13 +24,40 @@ CREATE_GITHUB_OAUTH_CREDS = "https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app" +DOTENV_FILE = Path.cwd() / ".env" + + +def _load_dotenv(dotenv_file=DOTENV_FILE): + load_dotenv(dotenv_file) + + +def add_env_var(env_vars: dict): + + new_line = "{key}={value}\n" + + if not DOTENV_FILE.exists(): + rich.print( + ( + "Creating a `.env` file used to manage your cloud credentials" + f"and other important tokens.\nYou can view them here: {DOTENV_FILE.resolve()}" + ) + ) + + with open(DOTENV_FILE, "a+") as f: + for key, value in env_vars.items(): + rich.print(f"Writing {key} to `.env` file...") + f.writelines(new_line.format(key=key, value=value)) + + def check_cloud_provider_creds(cloud_provider: str): """Validate that the necessary cloud credentials have been set as environment variables.""" - cloud_provider = cloud_provider.lower() - rich.print("Creating and initializing your nebari-config.yaml :rocket:\n") + _load_dotenv() + cloud_provider = cloud_provider.lower() + env_vars = {} + # AWS if cloud_provider == ProviderEnum.aws.value.lower() and ( not os.environ.get("AWS_ACCESS_KEY_ID") @@ -40,11 +69,11 @@ def check_cloud_provider_creds(cloud_provider: str): ) ) - os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( + env_vars["AWS_ACCESS_KEY_ID"] = typer.prompt( "Please enter your AWS_ACCESS_KEY_ID", hide_input=True, ) - os.environ["AWS_SECRET_ACCESS_KEY"] = typer.prompt( + env_vars["AWS_SECRET_ACCESS_KEY"] = typer.prompt( "Please enter your AWS_SECRET_ACCESS_KEY", hide_input=True, ) @@ -59,11 +88,11 @@ def check_cloud_provider_creds(cloud_provider: str): ) ) - os.environ["GOOGLE_CREDENTIALS"] = typer.prompt( + env_vars["GOOGLE_CREDENTIALS"] = typer.prompt( "Please enter your GOOGLE_CREDENTIALS", hide_input=True, ) - os.environ["PROJECT_ID"] = typer.prompt( + env_vars["PROJECT_ID"] = typer.prompt( "Please enter your PROJECT_ID", hide_input=True, ) @@ -80,14 +109,14 @@ def check_cloud_provider_creds(cloud_provider: str): ) ) - os.environ["DIGITALOCEAN_TOKEN"] = typer.prompt( + env_vars["DIGITALOCEAN_TOKEN"] = typer.prompt( "Please enter your DIGITALOCEAN_TOKEN", hide_input=True, ) - os.environ["SPACES_ACCESS_KEY_ID"] = typer.prompt( + env_vars["SPACES_ACCESS_KEY_ID"] = typer.prompt( "Please enter your SPACES_ACCESS_KEY_ID", ) - os.environ["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( + env_vars["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( "Please enter your SPACES_SECRET_ACCESS_KEY", hide_input=True, ) @@ -105,84 +134,110 @@ def check_cloud_provider_creds(cloud_provider: str): ) ) - os.environ["ARM_CLIENT_ID"] = typer.prompt( + env_vars["ARM_CLIENT_ID"] = typer.prompt( "Please enter your ARM_CLIENT_ID", hide_input=True, ) - os.environ["ARM_CLIENT_SECRET"] = typer.prompt( + env_vars["ARM_CLIENT_SECRET"] = typer.prompt( "Please enter your ARM_CLIENT_SECRET", hide_input=True, ) - os.environ["ARM_SUBSCRIPTION_ID"] = typer.prompt( + env_vars["ARM_SUBSCRIPTION_ID"] = typer.prompt( "Please enter your ARM_SUBSCRIPTION_ID", hide_input=True, ) - os.environ["ARM_TENANT_ID"] = typer.prompt( + env_vars["ARM_TENANT_ID"] = typer.prompt( "Please enter your ARM_TENANT_ID", hide_input=True, ) + add_env_var(env_vars) + _load_dotenv() + return cloud_provider -def check_auth_provider_creds(auth_provider: str): +def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): """Validating the the necessary auth provider credentials have been set as environment variables.""" + _load_dotenv() + env_vars = {} auth_provider = auth_provider.lower() # Auth0 - if auth_provider == AuthenticationEnum.auth0.value.lower() and ( - not os.environ.get("AUTH0_CLIENT_ID") - or not os.environ.get("AUTH0_CLIENT_SECRET") - or not os.environ.get("AUTH0_DOMAIN") - ): - rich.print( - MISSING_CREDS_TEMPLATE.format( - provider="Auth0", link_to_docs=CREATE_AUTH0_CREDS + if auth_provider == AuthenticationEnum.auth0.value.lower(): + + if ( + not os.environ.get("AUTH0_CLIENT_ID") + or not os.environ.get("AUTH0_CLIENT_SECRET") + or not os.environ.get("AUTH0_DOMAIN") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Auth0", link_to_docs=CREATE_AUTH0_CREDS + ) ) - ) - os.environ["AUTH0_CLIENT_ID"] = typer.prompt( - "Please enter your AUTH0_CLIENT_ID", - hide_input=True, - ) - os.environ["AUTH0_CLIENT_SECRET"] = typer.prompt( - "Please enter your AUTH0_CLIENT_SECRET", - hide_input=True, - ) - os.environ["AUTH0_DOMAIN"] = typer.prompt( - "Please enter your AUTH0_DOMAIN", - hide_input=True, - ) + env_vars["AUTH0_CLIENT_ID"] = typer.prompt( + "Please enter your AUTH0_CLIENT_ID", + hide_input=True, + ) + env_vars["AUTH0_CLIENT_SECRET"] = typer.prompt( + "Please enter your AUTH0_CLIENT_SECRET", + hide_input=True, + ) + env_vars["AUTH0_DOMAIN"] = typer.prompt( + "Please enter your AUTH0_DOMAIN", + hide_input=True, + ) + + if not ctx.params.get("auth_auto_provision", False): + ctx.params["auth_auto_provision"] = typer.prompt( + "Do you wish for Nebari to automatically provision the Auth0 `Regular Web Application`?", + type=bool, + default=True, + ) # GitHub - elif auth_provider == AuthenticationEnum.github.value.lower() and ( - not os.environ.get("GITHUB_CLIENT_ID") - or not os.environ.get("GITHUB_CLIENT_SECRET") - ): - rich.print( - MISSING_CREDS_TEMPLATE.format( - provider="GitHub OAuth App", link_to_docs=CREATE_GITHUB_OAUTH_CREDS + elif auth_provider == AuthenticationEnum.github.value.lower(): + + if not os.environ.get("GITHUB_CLIENT_ID") or not os.environ.get( + "GITHUB_CLIENT_SECRET" + ): + + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="GitHub OAuth App", link_to_docs=CREATE_GITHUB_OAUTH_CREDS + ) ) - ) + + env_vars["GITHUB_CLIENT_ID"] = typer.prompt( + "Please enter your GITHUB_CLIENT_ID", + hide_input=True, + ) + env_vars["GITHUB_CLIENT_SECRET"] = typer.prompt( + "Please enter your GITHUB_CLIENT_SECRET", + hide_input=True, + ) + + domain_name = ctx.params.get("domain_name", "") rich.print( ( ":warning: If you haven't done so already, please ensure the following:\n" - "The `Homepage URL` is set to: [light_green]https://[/light_green]\n" - "The `Authorization callback URL` is set to: [light_green]https:///auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" - "* `[light_green][/light_green]` should be the same one you provided above" + f"The `Homepage URL` is set to: [light_green]https://{domain_name}[/light_green]\n" + f"The `Authorization callback URL` is set to: [light_green]https://{domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" ) ) - os.environ["GITHUB_CLIENT_ID"] = typer.prompt( - "Please enter your GITHUB_CLIENT_ID", - hide_input=True, - ) - os.environ["GITHUB_CLIENT_SECRET"] = typer.prompt( - "Please enter your GITHUB_CLIENT_SECRET", - hide_input=True, - ) - - print(os.environ["GITHUB_CLIENT_ID"]) + add_env_var(env_vars) + _load_dotenv() return auth_provider + + +def check_project_name(ctx: typer.Context, project_name: str): + project_name_convention( + project_name.lower(), {"provider": ctx.params["cloud_provider"]} + ) + + return project_name diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 3fb069492..e45d8a63e 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -4,10 +4,21 @@ from click import Context from typer.core import TyperGroup -from qhub.cli._init import check_auth_provider_creds, check_cloud_provider_creds +from qhub.cli._init import ( + check_auth_provider_creds, + check_cloud_provider_creds, + check_project_name, +) +from qhub.initialize import render_config from qhub.render import render_template -from qhub.schema import AuthenticationEnum, ProviderEnum, verify -from qhub.utils import load_yaml +from qhub.schema import ( + AuthenticationEnum, + CiEnum, + ProviderEnum, + TerraformStateEnum, + verify, +) +from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, load_yaml, yaml def enum_to_list(enum_cls): @@ -41,21 +52,25 @@ def init( "--project-name", "--project", "-p", - prompt="Please enter your desired project name", + prompt=True, + callback=check_project_name, ), domain_name: str = typer.Option( ..., "--domain-name", "--domain", "-d", - prompt="Please enter your desired domain name", + prompt=True, ), auth_provider: str = typer.Option( "password", - prompt="Please enter your desired authentication provider", + prompt=True, help=f"options: {enum_to_list(AuthenticationEnum)}", callback=check_auth_provider_creds, ), + auth_auto_provision: bool = typer.Option( + False, + ), namespace: str = typer.Option( "dev", prompt=True, @@ -63,33 +78,65 @@ def init( repository: str = typer.Option( None, prompt=True, - # callback=auth_provider_options + ), + repository_auto_provision: bool = typer.Option( + False, ), ci_provider: str = typer.Option( - "github-actions", + None, prompt=True, + help=f"options: {enum_to_list(CiEnum)}", # callback=auth_provider_options ), terraform_state: str = typer.Option( - "remote", - prompt=True, - # callback=auth_provider_options + "remote", prompt=True, help=f"options {enum_to_list(TerraformStateEnum)}" ), kubernetes_version: str = typer.Option( "latest", prompt=True, # callback=auth_provider_options ), - ssl_cert: str = typer.Option( - "email", + ssl_cert_email: str = typer.Option( + None, prompt=True, - # callback=auth_provider_options ), ): """ Create and initialize your nebari-config.yaml file. """ - pass + if QHUB_IMAGE_TAG: + print( + f"Modifying the image tags for the `default_images`, setting tags equal to: {QHUB_IMAGE_TAG}" + ) + + if QHUB_DASK_VERSION: + print( + f"Modifying the version of the `qhub_dask` package, setting version equal to: {QHUB_DASK_VERSION}" + ) + + config = render_config( + cloud_provider=cloud_provider, + project_name=project_name, + qhub_domain=domain_name, + auth_provider=auth_provider, + auth_auto_provision=auth_auto_provision, + ci_provider=ci_provider, + namespace=namespace, + repository=repository, + repository_auto_provision=repository_auto_provision, + kubernetes_version=kubernetes_version, + terraform_state=terraform_state, + ssl_cert_email=ssl_cert_email, + disable_prompt=False, # keep? + ) + + try: + with open("qhub-config.yaml", "x") as f: + yaml.dump(config, f) + except FileExistsError: + raise ValueError( + "A qhub-config.yaml file already exists. Please move or delete it and try again." + ) @app.command() diff --git a/qhub/schema.py b/qhub/schema.py index 9bd3ea5ce..e903e9f11 100644 --- a/qhub/schema.py +++ b/qhub/schema.py @@ -486,6 +486,45 @@ def enabled_must_have_fields(cls, values): letter_dash_underscore_pydantic = pydantic.constr(regex=namestr_regex) +def project_name_convention(value: typing.Any, values): + convention = """ + In order to successfully deploy QHub, there are some project naming conventions which need + to be followed. First, ensure your name is compatible with the specific one for + your chosen Cloud provider. In addition, the QHub project name should also obey the following + format requirements: + - Letters from A to Z (upper and lower case) and numbers; + - Maximum accepted length of the name string is 16 characters. + - If using AWS: names should not start with the string "aws"; + - If using Azure: names should not contain "-". + """ + if len(value) > 16: + raise ValueError( + "\n".join( + [ + convention, + "Maximum accepted length of the project name string is 16 characters.", + ] + ) + ) + elif values["provider"] == "azure" and ("-" in value): + raise ValueError( + "\n".join( + [convention, "Provider [azure] does not allow '-' in project name."] + ) + ) + elif values["provider"] == "aws" and value.startswith("aws"): + raise ValueError( + "\n".join( + [ + convention, + "Provider [aws] does not allow 'aws' as starting sequence in project name.", + ] + ) + ) + else: + return letter_dash_underscore_pydantic + + class Main(Base): provider: ProviderEnum project_name: str @@ -544,43 +583,8 @@ def is_version_accepted(cls, v): return v != "" and rounded_ver_parse(v) == rounded_ver_parse(__version__) @validator("project_name") - def project_name_convention(cls, value: typing.Any, values): - convention = """ - In order to successfully deploy QHub, there are some project naming conventions which need - to be followed. First, ensure your name is compatible with the specific one for - your chosen Cloud provider. In addition, the QHub project name should also obey the following - format requirements: - - Letters from A to Z (upper and lower case) and numbers; - - Maximum accepted length of the name string is 16 characters. - - If using AWS: names should not start with the string "aws"; - - If using Azure: names should not contain "-". - """ - if len(value) > 16: - raise ValueError( - "\n".join( - [ - convention, - "Maximum accepted length of the project name string is 16 characters.", - ] - ) - ) - elif values["provider"] == "azure" and ("-" in value): - raise ValueError( - "\n".join( - [convention, "Provider [azure] does not allow '-' in project name."] - ) - ) - elif values["provider"] == "aws" and value.startswith("aws"): - raise ValueError( - "\n".join( - [ - convention, - "Provider [aws] does not allow 'aws' as starting sequence in project name.", - ] - ) - ) - else: - return letter_dash_underscore_pydantic + def _project_name_convention(cls, value: typing.Any, values): + project_name_convention(value=value, values=values) def verify(config): From 6259384a8c283451f9e2dc4bfc912edeaca837b8 Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Mon, 19 Sep 2022 20:40:14 +0530 Subject: [PATCH 09/43] Nebari deploy and destroy commands (#1436) * Nebari deploy and destroy commands * Add typer confirm to destroy command * Run pre-commit Co-authored-by: eskild <42120229+iameskild@users.noreply.github.com> Co-authored-by: iameskild --- qhub/cli/main.py | 88 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index e45d8a63e..5279a9717 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -9,6 +9,8 @@ check_cloud_provider_creds, check_project_name, ) +from qhub.deploy import deploy_configuration +from qhub.destroy import destroy_configuration from qhub.initialize import render_config from qhub.render import render_template from qhub.schema import ( @@ -186,14 +188,13 @@ def render( None, "-c", "--config", - help="qhub configuration yaml file", + help="nebari configuration yaml file path", ), dry_run: bool = typer.Option( False, "--dry-run", help="simulate rendering files without actually writing or updating any files", - ) - # TODO: debug why dry-run is not working? + ), ): """ Dynamically render terraform scripts and other files from the nebari-config.yaml @@ -213,19 +214,92 @@ def render( @app.command() -def deploy(): +def deploy( + config: str = typer.Option( + ..., + "--config", + "-c", + help="nebari configuration yaml file path", + ), + output: str = typer.Option( + "./", + "-o", + "--output", + help="output directory", + ), + disable_prompt: bool = typer.Option( + False, + "--disable-prompt", + help="Disable human intervention", + ), + disable_render: bool = typer.Option( + False, + "--disable-render", + help="Disable auto-rendering in deploy stage", + ), +): """ Deploy the nebari """ - print("Deploy the Nebari") + config_filename = pathlib.Path(config) + if not config_filename.is_file(): + raise ValueError( + f"passed in configuration filename={config_filename} must exist" + ) + + config_yaml = load_yaml(config_filename) + + verify(config_yaml) + + if not disable_render: + render_template(output, config, force=True) + + deploy_configuration( + config_yaml, + dns_provider=False, + dns_auto_provision=False, + disable_prompt=disable_prompt, + skip_remote_state_provision=False, + ) @app.command() -def destroy(): +def destroy( + config: str = typer.Option(..., "-c", "--config", help="qhub configuration"), + output: str = typer.Option( + "./" "-o", + "--output", + help="output directory", + ), + disable_render: bool = typer.Option( + False, + "--disable-render", + help="Disable auto-rendering before destroy", + ), +): """ Destroy the nebari """ - print("Destroy the Nebari") + delete = typer.confirm("Are you sure you want to destroy it?") + + if not delete: + print("not destroying!") + raise typer.Abort() + else: + config_filename = pathlib.Path(config) + if not config_filename.is_file(): + raise ValueError( + f"passed in configuration filename={config_filename} must exist" + ) + + config_yaml = load_yaml(config_filename) + + verify(config_yaml) + + if not disable_render: + render_template(output, config, force=True) + + destroy_configuration(config_yaml) if __name__ == "__main__": From d3a58779eb8e35c23839f37870aa8844e8d07d8e Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 19 Sep 2022 22:57:50 -0700 Subject: [PATCH 10/43] Add guided-init --- qhub/cli/_init.py | 160 ++++++++++++++--------- qhub/cli/main.py | 318 +++++++++++++++++++++++++++++++++++++--------- qhub/schema.py | 16 +++ 3 files changed, 370 insertions(+), 124 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 8826a2623..7c043e6ef 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -5,9 +5,16 @@ import typer from dotenv import load_dotenv -from qhub.schema import AuthenticationEnum, ProviderEnum, project_name_convention +from qhub.initialize import render_config +from qhub.schema import ( + AuthenticationEnum, + InitInputs, + ProviderEnum, + project_name_convention, +) +from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, yaml -MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, please refer to this guide on how to generate them:\n[light_green]{link_to_docs}[/light_green]\n" +MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, please refer to this guide on how to generate them:\n\n[light_green]\t\t{link_to_docs}[/light_green]\n\n" # links to external docs CREATE_AWS_CREDS = ( @@ -24,6 +31,9 @@ CREATE_GITHUB_OAUTH_CREDS = "https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app" +############################ +### MOVE TO UTILS FOLDER ### +############################ DOTENV_FILE = Path.cwd() / ".env" @@ -38,21 +48,61 @@ def add_env_var(env_vars: dict): if not DOTENV_FILE.exists(): rich.print( ( - "Creating a `.env` file used to manage your cloud credentials" - f"and other important tokens.\nYou can view them here: {DOTENV_FILE.resolve()}" + "Creating a [purple].env[/purple] file used to manage secret credentials and other important environment variables.\n" + "You can view or modify these environment variables here:\n\n" + f"\t\t[purple]{DOTENV_FILE.resolve()}[/purple]\n\n" ) ) with open(DOTENV_FILE, "a+") as f: for key, value in env_vars.items(): - rich.print(f"Writing {key} to `.env` file...") + rich.print(f"Writing {key} to [purple].env[/purple] file...") f.writelines(new_line.format(key=key, value=value)) -def check_cloud_provider_creds(cloud_provider: str): - """Validate that the necessary cloud credentials have been set as environment variables.""" +############################ + + +def handle_init(inputs: InitInputs): + if QHUB_IMAGE_TAG: + print( + f"Modifying the image tags for the `default_images`, setting tags equal to: {QHUB_IMAGE_TAG}" + ) + + if QHUB_DASK_VERSION: + print( + f"Modifying the version of the `qhub_dask` package, setting version equal to: {QHUB_DASK_VERSION}" + ) + + print(inputs) + + config = render_config( + cloud_provider=inputs.cloud_provider, + project_name=inputs.project_name, + qhub_domain=inputs.domain_name, + namespace=inputs.namespace, + auth_provider=inputs.auth_provider, + auth_auto_provision=inputs.auth_auto_provision, + ci_provider=inputs.ci_provider, + repository=inputs.repository, + repository_auto_provision=inputs.repository_auto_provision, + kubernetes_version=inputs.kubernetes_version, + terraform_state=inputs.terraform_state, + ssl_cert_email=inputs.ssl_cert_email, + disable_prompt=False, # keep? + ) + + try: + with open("qhub-config.yaml", "x") as f: + yaml.dump(config, f) + except FileExistsError: + raise ValueError( + "A qhub-config.yaml file already exists. Please move or delete it and try again." + ) - rich.print("Creating and initializing your nebari-config.yaml :rocket:\n") + +def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): + """Validate that the necessary cloud credentials have been set as environment variables.""" _load_dotenv() cloud_provider = cloud_provider.lower() @@ -115,6 +165,7 @@ def check_cloud_provider_creds(cloud_provider: str): ) env_vars["SPACES_ACCESS_KEY_ID"] = typer.prompt( "Please enter your SPACES_ACCESS_KEY_ID", + hide_input=True, ) env_vars["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( "Please enter your SPACES_SECRET_ACCESS_KEY", @@ -165,70 +216,51 @@ def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): auth_provider = auth_provider.lower() # Auth0 - if auth_provider == AuthenticationEnum.auth0.value.lower(): - - if ( - not os.environ.get("AUTH0_CLIENT_ID") - or not os.environ.get("AUTH0_CLIENT_SECRET") - or not os.environ.get("AUTH0_DOMAIN") - ): - rich.print( - MISSING_CREDS_TEMPLATE.format( - provider="Auth0", link_to_docs=CREATE_AUTH0_CREDS - ) - ) - - env_vars["AUTH0_CLIENT_ID"] = typer.prompt( - "Please enter your AUTH0_CLIENT_ID", - hide_input=True, - ) - env_vars["AUTH0_CLIENT_SECRET"] = typer.prompt( - "Please enter your AUTH0_CLIENT_SECRET", - hide_input=True, - ) - env_vars["AUTH0_DOMAIN"] = typer.prompt( - "Please enter your AUTH0_DOMAIN", - hide_input=True, + if auth_provider == AuthenticationEnum.auth0.value.lower() and ( + not os.environ.get("AUTH0_CLIENT_ID") + or not os.environ.get("AUTH0_CLIENT_SECRET") + or not os.environ.get("AUTH0_DOMAIN") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Auth0", link_to_docs=CREATE_AUTH0_CREDS ) + ) - if not ctx.params.get("auth_auto_provision", False): - ctx.params["auth_auto_provision"] = typer.prompt( - "Do you wish for Nebari to automatically provision the Auth0 `Regular Web Application`?", - type=bool, - default=True, - ) + env_vars["AUTH0_CLIENT_ID"] = typer.prompt( + "Please enter your AUTH0_CLIENT_ID", + hide_input=True, + ) + env_vars["AUTH0_CLIENT_SECRET"] = typer.prompt( + "Please enter your AUTH0_CLIENT_SECRET", + hide_input=True, + ) + env_vars["AUTH0_DOMAIN"] = typer.prompt( + "Please enter your AUTH0_DOMAIN", + hide_input=True, + ) # GitHub - elif auth_provider == AuthenticationEnum.github.value.lower(): - - if not os.environ.get("GITHUB_CLIENT_ID") or not os.environ.get( - "GITHUB_CLIENT_SECRET" - ): - - rich.print( - MISSING_CREDS_TEMPLATE.format( - provider="GitHub OAuth App", link_to_docs=CREATE_GITHUB_OAUTH_CREDS - ) - ) - - env_vars["GITHUB_CLIENT_ID"] = typer.prompt( - "Please enter your GITHUB_CLIENT_ID", - hide_input=True, - ) - env_vars["GITHUB_CLIENT_SECRET"] = typer.prompt( - "Please enter your GITHUB_CLIENT_SECRET", - hide_input=True, - ) + elif auth_provider == AuthenticationEnum.github.value.lower() and ( + not os.environ.get("GITHUB_CLIENT_ID") + or not os.environ.get("GITHUB_CLIENT_SECRET") + ): - domain_name = ctx.params.get("domain_name", "") rich.print( - ( - ":warning: If you haven't done so already, please ensure the following:\n" - f"The `Homepage URL` is set to: [light_green]https://{domain_name}[/light_green]\n" - f"The `Authorization callback URL` is set to: [light_green]https://{domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" + MISSING_CREDS_TEMPLATE.format( + provider="GitHub OAuth App", link_to_docs=CREATE_GITHUB_OAUTH_CREDS ) ) + env_vars["GITHUB_CLIENT_ID"] = typer.prompt( + "Please enter your GITHUB_CLIENT_ID", + hide_input=True, + ) + env_vars["GITHUB_CLIENT_SECRET"] = typer.prompt( + "Please enter your GITHUB_CLIENT_SECRET", + hide_input=True, + ) + add_env_var(env_vars) _load_dotenv() @@ -236,6 +268,8 @@ def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): def check_project_name(ctx: typer.Context, project_name: str): + """Validate the project_name is acceptable. Depends on `cloud_provider`.""" + project_name_convention( project_name.lower(), {"provider": ctx.params["cloud_provider"]} ) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 5279a9717..417fec32b 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -1,5 +1,6 @@ -import pathlib +from pathlib import Path +import rich import typer from click import Context from typer.core import TyperGroup @@ -8,19 +9,20 @@ check_auth_provider_creds, check_cloud_provider_creds, check_project_name, + handle_init, ) from qhub.deploy import deploy_configuration from qhub.destroy import destroy_configuration -from qhub.initialize import render_config from qhub.render import render_template from qhub.schema import ( AuthenticationEnum, CiEnum, + InitInputs, ProviderEnum, TerraformStateEnum, verify, ) -from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, load_yaml, yaml +from qhub.utils import load_yaml def enum_to_list(enum_cls): @@ -38,23 +40,246 @@ def list_commands(self, ctx: Context): help="Nebari CLI 🪴", add_completion=False, no_args_is_help=True, + rich_markup_mode="rich", context_settings={"help_option_names": ["-h", "--help"]}, ) +@app.command() +def guided_init( + ctx: typer.Context, + disable_checks: bool = typer.Option( + default=False, + ), +): + """ + [bold green]START HERE[/bold green] if you're new to Nebari. This is the Guided Init wizard used to create and initialize your [purple]nebari-config.yaml[/purple] file. + + To get started simply run: + + [green]nebari guided-init[/green] + + This command asks a few important questions and when complete, will generate: + :sparkles: [purple]nebari-config.yaml[/purple], which: + contains all your Nebari cluster configuration details and, + is used as input to later commands such as [green]nebari render[/green], [green]nebari deploy[/green], etc. + :sparkles: [purple].env[/purple], which: + contains all your important environment variables (i.e. cloud credentials, tokens, etc.) and, + is only stored on your local machine and used as a convenience. + + [yellow]NOTE[/yellow] + This CLI command completes the same task, generating the [purple]nebari-config.yaml[/purple], as the generic + [green]nebari init[/green] command but does so without the need to enter all the flags required to get started. + """ + + rich.print( + ( + "Welcome to the Guided Init wizard!\n" + "You will be asked a few questions that are used to generate your [purple]nebari-config.yaml[/purple] and [purple].env[/purple] files.\n\n" + "For more detail about the [green]nebari init[/green] command, please refer to our docs:\n\n" + "\t\t[light_green]https://nebari-docs.netlify.app[/light_green]\n\n" + ) + ) + + if disable_checks: + rich.print( + "⚠️ Attempting to use the Guided Init wizard without any validation checks. There is no guarantee values provided will work! ⚠️\n\n" + ) + + import questionary + + qmark = " 🪴 " + + # pull in default values for each of the below + inputs = InitInputs() + + # CLOUD PROVIDER + inputs.cloud_provider = questionary.select( + "Where would you like to deploy your Nebari cluster?", + choices=enum_to_list(ProviderEnum), + qmark=qmark, + ).ask() + + if not disable_checks: + check_cloud_provider_creds(ctx, cloud_provider=inputs.cloud_provider) + + # specific context needed when `check_project_name` is called + ctx.params["cloud_provider"] = inputs.cloud_provider + + # PROJECT NAME + inputs.project_name = questionary.text( + "What project name would you like to use?", + qmark=qmark, + validate=lambda text: True if len(text) > 0 else "Please enter a value", + ).ask() + + if not disable_checks: + check_project_name(ctx, inputs.project_name) + + # DOMAIN NAME + inputs.domain_name = questionary.text( + "What domain name would you like to use?", + qmark=qmark, + validate=lambda text: True if len(text) > 0 else "Please enter a value", + ).ask() + + # NAMESPACE + inputs.namespace = questionary.text( + "What namespace would like to use?", + default=inputs.namespace, + qmark=qmark, + ).ask() + + # AUTH PROVIDER + inputs.auth_provider = questionary.select( + "What authentication provider would you like?", + choices=enum_to_list(AuthenticationEnum), + qmark=qmark, + ).ask() + + if not disable_checks: + check_auth_provider_creds(ctx, auth_provider=inputs.auth_provider) + + if inputs.auth_provider.lower() == AuthenticationEnum.auth0.value.lower(): + inputs.auth_auto_provision = questionary.confirm( + "Would you like us to auto provision the Auth0 Machine-to-Machine app?", + default=False, + qmark=qmark, + ).ask() + + elif inputs.auth_provider.lower() == AuthenticationEnum.github.value.lower(): + rich.print( + ( + ":warning: If you haven't done so already, please ensure the following:\n" + f"The `Homepage URL` is set to: [light_green]https://{inputs.domain_name}[/light_green]\n" + f"The `Authorization callback URL` is set to: [light_green]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" + ) + ) + + # REPOSITORY + if questionary.confirm( + "Would you like to store this project in a git repo?", + default=False, + qmark=qmark, + ).ask(): + + repo_url = "http://{git_provider}/{org_name}/{repo_name}" + + git_provider = questionary.select( + "Which git provider would you like to use?", + choices=["github.com", "gitlab.com"], + qmark=qmark, + ).ask() + + org_name = questionary.text( + f"Which user or organization will this repo live under? ({repo_url.format(git_provider=git_provider, org_name='', repo_name='')})", + qmark=qmark, + ).ask() + + repo_name = questionary.text( + f"And what is the name of this repo? ({repo_url.format(git_provider=git_provider, org_name=org_name, repo_name='')})", + qmark=qmark, + ).ask() + + inputs.repository = repo_url.format( + git_provider=git_provider, org_name=org_name, repo_name=repo_name + ) + + inputs.repository_auto_provision = questionary.confirm( + f"Would you like us to auto create the following git repo: {inputs.repository}?", + default=False, + qmark=qmark, + ).ask() + + # create `check_repository_creds` function + if not disable_checks: + pass + + # CICD + inputs.ci_provider = questionary.select( + "Would you like to adopt a GitOps workflow for this repository?", + choices=enum_to_list(CiEnum), + qmark=qmark, + ).ask() + + # SSL CERTIFICATE + ssl_cert = questionary.confirm( + "Would you like to add a Let's Encrypt SSL certificate to your cluster?", + default=False, + qmark=qmark, + ).ask() + + if ssl_cert: + inputs.ssl_cert_email = questionary.text( + "Which email address should Let's Encrypt associate the certificate with?", + qmark=qmark, + ).ask() + + # ADVANCED FEATURES + if questionary.confirm( + "Would you like to make advanced configuration changes (⚠️ caution is advised)?", + default=False, + qmark=qmark, + ).ask(): + + inputs.terraform_state = questionary.select( + "Where should the Terraform State be provisioned?", + choices=enum_to_list(TerraformStateEnum), + qmark=qmark, + ).ask() + + inputs.kubernetes_version = questionary.text( + "Which Kubernetes version would you like to use?", + qmark=qmark, + ).ask() + + handle_init(inputs) + + rich.print( + ( + "Congratulations, you have generated the all important [purple]nebari-config.yaml[/purple] file 🎉\n\n" + "You can always edit your [purple]nebari-config.yaml[/purple] file by editing the file directly." + "If you do make changes to you can ensure its still a valid configuration by running:\n\n" + "\t\t[green]nebari validate --config path/to/nebari-config.yaml[/green]\n\n" + ) + ) + + base_cmd = f"nebari init {inputs.cloud_provider}" + + def if_used(key, model=inputs, ignore_list=["cloud_provider"]): + if key not in ignore_list: + b = "--{key} {value}" + value = getattr(model, key) + if isinstance(value, str) and value != "": + return b.format(key=key, value=value).replace("_", "-") + if isinstance(value, bool) and value: + return b.format(key=key, value=value).replace("_", "-") + + cmds = " ".join( + [_ for _ in [if_used(_) for _ in inputs.dict().keys()] if _ is not None] + ) + + rich.print( + ( + "Here is the previous Guided Init if it was converted into a [green]nebari init[/green] command:\n\n" + f"\t\t[green]{base_cmd} {cmds}[/green]\n\n" + ) + ) + + @app.command() def init( cloud_provider: str = typer.Argument( - ..., + "local", help=f"options: {enum_to_list(ProviderEnum)}", callback=check_cloud_provider_creds, + is_eager=True, ), project_name: str = typer.Option( ..., "--project-name", "--project", "-p", - prompt=True, callback=check_project_name, ), domain_name: str = typer.Option( @@ -62,83 +287,57 @@ def init( "--domain-name", "--domain", "-d", - prompt=True, + ), + namespace: str = typer.Option( + "dev", ), auth_provider: str = typer.Option( "password", - prompt=True, help=f"options: {enum_to_list(AuthenticationEnum)}", callback=check_auth_provider_creds, ), auth_auto_provision: bool = typer.Option( False, ), - namespace: str = typer.Option( - "dev", - prompt=True, - ), repository: str = typer.Option( None, - prompt=True, ), repository_auto_provision: bool = typer.Option( False, ), ci_provider: str = typer.Option( None, - prompt=True, help=f"options: {enum_to_list(CiEnum)}", - # callback=auth_provider_options ), terraform_state: str = typer.Option( - "remote", prompt=True, help=f"options {enum_to_list(TerraformStateEnum)}" + "remote", help=f"options: {enum_to_list(TerraformStateEnum)}" ), kubernetes_version: str = typer.Option( "latest", - prompt=True, - # callback=auth_provider_options ), ssl_cert_email: str = typer.Option( None, - prompt=True, ), ): """ - Create and initialize your nebari-config.yaml file. + Create and initialize your [purple]nebari-config.yaml[/purple] file. """ - if QHUB_IMAGE_TAG: - print( - f"Modifying the image tags for the `default_images`, setting tags equal to: {QHUB_IMAGE_TAG}" - ) + inputs = InitInputs() - if QHUB_DASK_VERSION: - print( - f"Modifying the version of the `qhub_dask` package, setting version equal to: {QHUB_DASK_VERSION}" - ) + inputs.cloud_provider = cloud_provider + inputs.project_name = project_name + inputs.domain_name = domain_name + inputs.namespace = namespace + inputs.auth_provider = auth_provider + inputs.auth_auto_provision = auth_auto_provision + inputs.repository = repository + inputs.repository_auto_provision = repository_auto_provision + inputs.ci_provider = ci_provider + inputs.terraform_state = terraform_state + inputs.kubernetes_version = kubernetes_version + inputs.ssl_cert_email = ssl_cert_email - config = render_config( - cloud_provider=cloud_provider, - project_name=project_name, - qhub_domain=domain_name, - auth_provider=auth_provider, - auth_auto_provision=auth_auto_provision, - ci_provider=ci_provider, - namespace=namespace, - repository=repository, - repository_auto_provision=repository_auto_provision, - kubernetes_version=kubernetes_version, - terraform_state=terraform_state, - ssl_cert_email=ssl_cert_email, - disable_prompt=False, # keep? - ) - - try: - with open("qhub-config.yaml", "x") as f: - yaml.dump(config, f) - except FileExistsError: - raise ValueError( - "A qhub-config.yaml file already exists. Please move or delete it and try again." - ) + handle_init(inputs) @app.command() @@ -154,12 +353,9 @@ def validate( ), ): """ - Validate the config.yaml file. - + Validate the [purple]nebari-config.yaml[/purple] file. """ - # print(f"Validate the {config}") - - config_filename = pathlib.Path(config) + config_filename = Path(config) if not config_filename.is_file(): raise ValueError( f"Passed in configuration filename={config_filename} must exist." @@ -197,9 +393,9 @@ def render( ), ): """ - Dynamically render terraform scripts and other files from the nebari-config.yaml + Dynamically render the Terraform scripts and other files from your [purple]nebari-config.yaml[/purple] file. """ - config_filename = pathlib.Path(config) + config_filename = Path(config) if not config_filename.is_file(): raise ValueError( @@ -239,9 +435,9 @@ def deploy( ), ): """ - Deploy the nebari + Deploy the Nebari cluster from your [purple]nebari-config.yaml[/purple] file. """ - config_filename = pathlib.Path(config) + config_filename = Path(config) if not config_filename.is_file(): raise ValueError( f"passed in configuration filename={config_filename} must exist" @@ -278,7 +474,7 @@ def destroy( ), ): """ - Destroy the nebari + Destroy the Nebari cluster from your [purple]nebari-config.yaml[/purple] file. """ delete = typer.confirm("Are you sure you want to destroy it?") @@ -286,7 +482,7 @@ def destroy( print("not destroying!") raise typer.Abort() else: - config_filename = pathlib.Path(config) + config_filename = Path(config) if not config_filename.is_file(): raise ValueError( f"passed in configuration filename={config_filename} must exist" diff --git a/qhub/schema.py b/qhub/schema.py index e903e9f11..ddb36131d 100644 --- a/qhub/schema.py +++ b/qhub/schema.py @@ -525,6 +525,22 @@ def project_name_convention(value: typing.Any, values): return letter_dash_underscore_pydantic +# CLEAN UP +class InitInputs(Base): + cloud_provider: typing.Type[ProviderEnum] = "local" + project_name: str = "" + domain_name: str = "" + namespace: typing.Optional[letter_dash_underscore_pydantic] = "dev" + auth_provider: typing.Type[AuthenticationEnum] = "password" + auth_auto_provision: bool = False + repository: typing.Union[str, None] = None + repository_auto_provision: bool = False + ci_provider: typing.Optional[CiEnum] = None + terraform_state: typing.Optional[TerraformStateEnum] = None + kubernetes_version: typing.Union[str, None] = None + ssl_cert_email: typing.Union[str, None] = None + + class Main(Base): provider: ProviderEnum project_name: str From 0d67af2e26bf00963c88f329ace473c7ce28a16e Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Tue, 20 Sep 2022 11:39:49 +0530 Subject: [PATCH 11/43] Add deploy flags and change repository default value (#1441) * Nebari deploy and destroy commands * Add typer confirm to destroy command * Add deploy arguments and change repository default value * Add rendre design * Run pre-commit Co-authored-by: eskild <42120229+iameskild@users.noreply.github.com> Co-authored-by: iameskild --- qhub/cli/main.py | 15 +++++++++++++-- qhub/render.py | 25 +++++++++++++++++-------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 417fec32b..2de959dcc 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -423,6 +423,16 @@ def deploy( "--output", help="output directory", ), + dns_provider: str = typer.Option( + False, + "--dns-provider", + help="dns provider to use for registering domain name mapping", + ), + dns_auto_provision: bool = typer.Option( + False, + "--dns-auto-provision", + help="Attempt to automatically provision DNS. For Auth0 is requires environment variables AUTH0_DOMAIN, AUTH0_CLIENTID, AUTH0_CLIENT_SECRET", + ), disable_prompt: bool = typer.Option( False, "--disable-prompt", @@ -438,6 +448,7 @@ def deploy( Deploy the Nebari cluster from your [purple]nebari-config.yaml[/purple] file. """ config_filename = Path(config) + if not config_filename.is_file(): raise ValueError( f"passed in configuration filename={config_filename} must exist" @@ -452,8 +463,8 @@ def deploy( deploy_configuration( config_yaml, - dns_provider=False, - dns_auto_provision=False, + dns_provider=dns_provider, + dns_auto_provision=dns_auto_provision, disable_prompt=disable_prompt, skip_remote_state_provision=False, ) diff --git a/qhub/render.py b/qhub/render.py index 5eff1daf4..904c3b614 100644 --- a/qhub/render.py +++ b/qhub/render.py @@ -6,6 +6,8 @@ import sys from typing import Dict, List +from rich import print +from rich.table import Table from ruamel.yaml import YAML from qhub.deprecate import DEPRECATED_FILE_PATHS @@ -89,21 +91,28 @@ def render_template(output_directory, config_filename, force=False, dry_run=Fals ) if new: - print("The following files will be created:") + table = Table("The following files will be created:", style="deep_sky_blue1") for filename in sorted(new): - print(f" CREATED {filename}") + table.add_row(filename, style="spring_green1") + print(table) if updated: - print("The following files will be updated:") + table = Table("The following files will be updated:", style="deep_sky_blue1") for filename in sorted(updated): - print(f" UPDATED {filename}") + table.add_row(filename, style="spring_green1") + print(table) if deleted: - print("The following files will be deleted:") + table = Table("The following files will be deleted:", style="deep_sky_blue1") for filename in sorted(deleted): - print(f" DELETED {filename}") + table.add_row(filename, style="spring_green1") + print(table) if untracked: - print("The following files are untracked (only exist in output directory):") + table = Table( + "The following files are untracked (only exist in output directory):", + style="deep_sky_blue1", + ) for filename in sorted(updated): - print(f" UNTRACKED {filename}") + table.add_row(filename, style="spring_green1") + print(table) if dry_run: print("dry-run enabled no files will be created, updated, or deleted") From 6a31dee6ae03d84a9ae0aa9ec8925c390e88bcbb Mon Sep 17 00:00:00 2001 From: asmijafar20 Date: Tue, 20 Sep 2022 20:35:11 +0530 Subject: [PATCH 12/43] Nebari Typer CLI --- qhub/cli/main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 2de959dcc..f3fcb6584 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -23,7 +23,7 @@ verify, ) from qhub.utils import load_yaml - +from rich import print def enum_to_list(enum_cls): return [e.value for e in enum_cls] @@ -369,7 +369,7 @@ def validate( pass else: verify(config) - print("Successfully validated configuration") + print("[bold purple]Successfully validated configuration.[/bold purple]") @app.command() @@ -488,9 +488,7 @@ def destroy( Destroy the Nebari cluster from your [purple]nebari-config.yaml[/purple] file. """ delete = typer.confirm("Are you sure you want to destroy it?") - if not delete: - print("not destroying!") raise typer.Abort() else: config_filename = Path(config) From decb7b0c594efd8affe31521d00c3f41413cc180 Mon Sep 17 00:00:00 2001 From: asmijafar20 Date: Tue, 20 Sep 2022 20:42:17 +0530 Subject: [PATCH 13/43] Add pre-commit --- qhub/cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index f3fcb6584..2f214d5a5 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -3,6 +3,7 @@ import rich import typer from click import Context +from rich import print from typer.core import TyperGroup from qhub.cli._init import ( @@ -23,7 +24,7 @@ verify, ) from qhub.utils import load_yaml -from rich import print + def enum_to_list(enum_cls): return [e.value for e in enum_cls] From 4e668940c554cef1e88beb2be74c8e76df28e78a Mon Sep 17 00:00:00 2001 From: asmijafar20 Date: Thu, 22 Sep 2022 21:52:17 +0530 Subject: [PATCH 14/43] Add disable_checks in deploy command --- qhub/cli/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 2f214d5a5..f87b301cb 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -467,6 +467,7 @@ def deploy( dns_provider=dns_provider, dns_auto_provision=dns_auto_provision, disable_prompt=disable_prompt, + disable_checks=False, skip_remote_state_provision=False, ) From eafe6f5fc1916998fb4fa3ae7bfa37f498d22cf5 Mon Sep 17 00:00:00 2001 From: iameskild Date: Thu, 22 Sep 2022 15:12:44 -0700 Subject: [PATCH 15/43] Combine guided-init and init, part 1 --- .env | 0 qhub/cli/main.py | 90 ++++++++++++++++++++++++++---------------------- 2 files changed, 49 insertions(+), 41 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 000000000..e69de29bb diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 417fec32b..6bc574e17 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -1,5 +1,7 @@ +import os from pathlib import Path +import questionary import rich import typer from click import Context @@ -45,32 +47,12 @@ def list_commands(self, ctx: Context): ) -@app.command() -def guided_init( - ctx: typer.Context, - disable_checks: bool = typer.Option( - default=False, - ), -): - """ - [bold green]START HERE[/bold green] if you're new to Nebari. This is the Guided Init wizard used to create and initialize your [purple]nebari-config.yaml[/purple] file. - - To get started simply run: - - [green]nebari guided-init[/green] +def guided_init_wizard(ctx: typer.Context, guided_init: str): - This command asks a few important questions and when complete, will generate: - :sparkles: [purple]nebari-config.yaml[/purple], which: - contains all your Nebari cluster configuration details and, - is used as input to later commands such as [green]nebari render[/green], [green]nebari deploy[/green], etc. - :sparkles: [purple].env[/purple], which: - contains all your important environment variables (i.e. cloud credentials, tokens, etc.) and, - is only stored on your local machine and used as a convenience. + disable_checks = os.environ.get("QHUB_DISABLE_INIT_CHECKS", False) - [yellow]NOTE[/yellow] - This CLI command completes the same task, generating the [purple]nebari-config.yaml[/purple], as the generic - [green]nebari init[/green] command but does so without the need to enter all the flags required to get started. - """ + if not guided_init: + return guided_init rich.print( ( @@ -86,8 +68,6 @@ def guided_init( "⚠️ Attempting to use the Guided Init wizard without any validation checks. There is no guarantee values provided will work! ⚠️\n\n" ) - import questionary - qmark = " 🪴 " # pull in default values for each of the below @@ -266,6 +246,15 @@ def if_used(key, model=inputs, ignore_list=["cloud_provider"]): ) ) + raise typer.Exit() + + +guided_init_help_msg = ( + "[bold green]START HERE[/bold green] - this will gently guide you through a list of questions " + "to generate your [purple]nebari-config.yaml[/purple]. " + "It is an [i]alternative[/i] to passing the options listed below." +) + @app.command() def init( @@ -275,6 +264,12 @@ def init( callback=check_cloud_provider_creds, is_eager=True, ), + guided_init: bool = typer.Option( + False, + help=guided_init_help_msg, + callback=guided_init_wizard, + is_eager=True, + ), project_name: str = typer.Option( ..., "--project-name", @@ -321,23 +316,36 @@ def init( ): """ Create and initialize your [purple]nebari-config.yaml[/purple] file. - """ - inputs = InitInputs() - inputs.cloud_provider = cloud_provider - inputs.project_name = project_name - inputs.domain_name = domain_name - inputs.namespace = namespace - inputs.auth_provider = auth_provider - inputs.auth_auto_provision = auth_auto_provision - inputs.repository = repository - inputs.repository_auto_provision = repository_auto_provision - inputs.ci_provider = ci_provider - inputs.terraform_state = terraform_state - inputs.kubernetes_version = kubernetes_version - inputs.ssl_cert_email = ssl_cert_email + If you're new to Nebari, we recommend you use the Guided Init wizard. + To get started simply run: - handle_init(inputs) + [green]nebari init --guided-init[/green] + + This command will generate: [purple]nebari-config.yaml[/purple] :sparkles: + + This file contains all your Nebari cluster configuration details and, + is used as input to later commands such as [green]nebari render[/green], [green]nebari deploy[/green], etc. + """ + # """ + # Create and initialize your [purple]nebari-config.yaml[/purple] file. + # """ + # inputs = InitInputs() + + # inputs.cloud_provider = cloud_provider + # inputs.project_name = project_name + # inputs.domain_name = domain_name + # inputs.namespace = namespace + # inputs.auth_provider = auth_provider + # inputs.auth_auto_provision = auth_auto_provision + # inputs.repository = repository + # inputs.repository_auto_provision = repository_auto_provision + # inputs.ci_provider = ci_provider + # inputs.terraform_state = terraform_state + # inputs.kubernetes_version = kubernetes_version + # inputs.ssl_cert_email = ssl_cert_email + + # handle_init(inputs) @app.command() From fd6a227d8456f38404a33e93ae8ee2b948991c0e Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Sat, 24 Sep 2022 23:32:31 +0530 Subject: [PATCH 16/43] Remove `dotenv` work. (#1472) * Remove Usage: dotenv [OPTIONS] COMMAND [ARGS] * Remove pathlib --- qhub/cli/_init.py | 76 ++++++++++------------------------------------- qhub/cli/main.py | 4 +-- 2 files changed, 18 insertions(+), 62 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 7c043e6ef..e71309fed 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -1,9 +1,7 @@ import os -from pathlib import Path import rich import typer -from dotenv import load_dotenv from qhub.initialize import render_config from qhub.schema import ( @@ -31,38 +29,6 @@ CREATE_GITHUB_OAUTH_CREDS = "https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app" -############################ -### MOVE TO UTILS FOLDER ### -############################ -DOTENV_FILE = Path.cwd() / ".env" - - -def _load_dotenv(dotenv_file=DOTENV_FILE): - load_dotenv(dotenv_file) - - -def add_env_var(env_vars: dict): - - new_line = "{key}={value}\n" - - if not DOTENV_FILE.exists(): - rich.print( - ( - "Creating a [purple].env[/purple] file used to manage secret credentials and other important environment variables.\n" - "You can view or modify these environment variables here:\n\n" - f"\t\t[purple]{DOTENV_FILE.resolve()}[/purple]\n\n" - ) - ) - - with open(DOTENV_FILE, "a+") as f: - for key, value in env_vars.items(): - rich.print(f"Writing {key} to [purple].env[/purple] file...") - f.writelines(new_line.format(key=key, value=value)) - - -############################ - - def handle_init(inputs: InitInputs): if QHUB_IMAGE_TAG: print( @@ -104,9 +70,7 @@ def handle_init(inputs: InitInputs): def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): """Validate that the necessary cloud credentials have been set as environment variables.""" - _load_dotenv() cloud_provider = cloud_provider.lower() - env_vars = {} # AWS if cloud_provider == ProviderEnum.aws.value.lower() and ( @@ -119,11 +83,11 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) ) - env_vars["AWS_ACCESS_KEY_ID"] = typer.prompt( + os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( "Please enter your AWS_ACCESS_KEY_ID", hide_input=True, ) - env_vars["AWS_SECRET_ACCESS_KEY"] = typer.prompt( + os.environ["AWS_SECRET_ACCESS_KEY"] = typer.prompt( "Please enter your AWS_SECRET_ACCESS_KEY", hide_input=True, ) @@ -138,11 +102,11 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) ) - env_vars["GOOGLE_CREDENTIALS"] = typer.prompt( + os.environ["GOOGLE_CREDENTIALS"] = typer.prompt( "Please enter your GOOGLE_CREDENTIALS", hide_input=True, ) - env_vars["PROJECT_ID"] = typer.prompt( + os.environ["PROJECT_ID"] = typer.prompt( "Please enter your PROJECT_ID", hide_input=True, ) @@ -159,15 +123,15 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) ) - env_vars["DIGITALOCEAN_TOKEN"] = typer.prompt( + os.environ["DIGITALOCEAN_TOKEN"] = typer.prompt( "Please enter your DIGITALOCEAN_TOKEN", hide_input=True, ) - env_vars["SPACES_ACCESS_KEY_ID"] = typer.prompt( + os.environ["SPACES_ACCESS_KEY_ID"] = typer.prompt( "Please enter your SPACES_ACCESS_KEY_ID", hide_input=True, ) - env_vars["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( + os.environ["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( "Please enter your SPACES_SECRET_ACCESS_KEY", hide_input=True, ) @@ -185,34 +149,29 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) ) - env_vars["ARM_CLIENT_ID"] = typer.prompt( + os.environ["ARM_CLIENT_ID"] = typer.prompt( "Please enter your ARM_CLIENT_ID", hide_input=True, ) - env_vars["ARM_CLIENT_SECRET"] = typer.prompt( + os.environ["ARM_CLIENT_SECRET"] = typer.prompt( "Please enter your ARM_CLIENT_SECRET", hide_input=True, ) - env_vars["ARM_SUBSCRIPTION_ID"] = typer.prompt( + os.environ["ARM_SUBSCRIPTION_ID"] = typer.prompt( "Please enter your ARM_SUBSCRIPTION_ID", hide_input=True, ) - env_vars["ARM_TENANT_ID"] = typer.prompt( + os.environ["ARM_TENANT_ID"] = typer.prompt( "Please enter your ARM_TENANT_ID", hide_input=True, ) - add_env_var(env_vars) - _load_dotenv() - return cloud_provider def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): """Validating the the necessary auth provider credentials have been set as environment variables.""" - _load_dotenv() - env_vars = {} auth_provider = auth_provider.lower() # Auth0 @@ -227,15 +186,15 @@ def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): ) ) - env_vars["AUTH0_CLIENT_ID"] = typer.prompt( + os.environ["AUTH0_CLIENT_ID"] = typer.prompt( "Please enter your AUTH0_CLIENT_ID", hide_input=True, ) - env_vars["AUTH0_CLIENT_SECRET"] = typer.prompt( + os.environ["AUTH0_CLIENT_SECRET"] = typer.prompt( "Please enter your AUTH0_CLIENT_SECRET", hide_input=True, ) - env_vars["AUTH0_DOMAIN"] = typer.prompt( + os.environ["AUTH0_DOMAIN"] = typer.prompt( "Please enter your AUTH0_DOMAIN", hide_input=True, ) @@ -252,18 +211,15 @@ def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): ) ) - env_vars["GITHUB_CLIENT_ID"] = typer.prompt( + os.environ["GITHUB_CLIENT_ID"] = typer.prompt( "Please enter your GITHUB_CLIENT_ID", hide_input=True, ) - env_vars["GITHUB_CLIENT_SECRET"] = typer.prompt( + os.environ["GITHUB_CLIENT_SECRET"] = typer.prompt( "Please enter your GITHUB_CLIENT_SECRET", hide_input=True, ) - add_env_var(env_vars) - _load_dotenv() - return auth_provider diff --git a/qhub/cli/main.py b/qhub/cli/main.py index f87b301cb..792f3c8d3 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -239,8 +239,8 @@ def guided_init( rich.print( ( "Congratulations, you have generated the all important [purple]nebari-config.yaml[/purple] file 🎉\n\n" - "You can always edit your [purple]nebari-config.yaml[/purple] file by editing the file directly." - "If you do make changes to you can ensure its still a valid configuration by running:\n\n" + "You can always edit your [purple]nebari-config.yaml[/purple] file by editing the file directly.\n" + "If you do make changes to it you can ensure its still a valid configuration by running:\n\n" "\t\t[green]nebari validate --config path/to/nebari-config.yaml[/green]\n\n" ) ) From a9ba96693830e35b7cde7b91534bfb3227bf1d24 Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 26 Sep 2022 19:00:01 -0700 Subject: [PATCH 17/43] guided-init clean up --- qhub/cli/_init.py | 265 +++++++++++++++++++++++++++++++++++++++++++++- qhub/cli/main.py | 259 +++++--------------------------------------- 2 files changed, 290 insertions(+), 234 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index e71309fed..e4fd86e5f 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -1,18 +1,22 @@ import os +import questionary import rich import typer from qhub.initialize import render_config from qhub.schema import ( AuthenticationEnum, + CiEnum, InitInputs, ProviderEnum, + TerraformStateEnum, project_name_convention, ) from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, yaml MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, please refer to this guide on how to generate them:\n\n[light_green]\t\t{link_to_docs}[/light_green]\n\n" +LINKS_TO_DOCS_TEMPLATE = "For more details, please refer to our docs:\n\n\t[light_green]{link_to_docs}[/light_green]\n\n" # links to external docs CREATE_AWS_CREDS = ( @@ -28,6 +32,14 @@ CREATE_AUTH0_CREDS = "https://auth0.com/docs/get-started/auth0-overview/create-applications/machine-to-machine-apps" CREATE_GITHUB_OAUTH_CREDS = "https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app" +# links to Nebari docs +DOCS_HOME = "https://nebari-docs.netlify.app" +CHOOSE_CLOUD_PROVIDER = "https://nebari-docs.netlify.app/getting-started/deploy" + + +def enum_to_list(enum_cls): + return [e.value.lower() for e in enum_cls] + def handle_init(inputs: InitInputs): if QHUB_IMAGE_TAG: @@ -40,7 +52,9 @@ def handle_init(inputs: InitInputs): f"Modifying the version of the `qhub_dask` package, setting version equal to: {QHUB_DASK_VERSION}" ) - print(inputs) + # this will force the `set_kubernetes_version` to grab the latest version + if inputs.kubernetes_version == "latest": + inputs.kubernetes_version = None config = render_config( cloud_provider=inputs.cloud_provider, @@ -231,3 +245,252 @@ def check_project_name(ctx: typer.Context, project_name: str): ) return project_name + + +def guided_init_wizard(ctx: typer.Context, guided_init: str): + """ + Guided Init Wizard is a user-friendly questionnaire used to help generate the `nebari-config.yaml`. + """ + qmark = " " + disable_checks = os.environ.get("QHUB_DISABLE_INIT_CHECKS", False) + + if not guided_init: + return guided_init + + rich.print( + ( + "\n\t\t[bold]Welcome to the Guided Init wizard![/bold]\n\n" + "You will be asked a few questions that are used to generate your [purple]nebari-config.yaml[/purple]. " + f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=DOCS_HOME)}" + ) + ) + + if disable_checks: + rich.print( + "⚠️ Attempting to use the Guided Init wizard without any validation checks. There is no guarantee values provided will work! ⚠️\n\n" + ) + + # pull in default values for each of the below + inputs = InitInputs() + + # CLOUD PROVIDER + rich.print( + ( + "\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " + "is where you want this Kubernetes cluster deployed. " + f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=CHOOSE_CLOUD_PROVIDER)}" + "\n\t❕ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " + "[italic]Currently only available on Linux OS.[/italic]" + "\n\t❕ [purple]existing[/purple] refers to an existing Kubernetes cluster that Nebari can be deployed on.\n" + ) + ) + inputs.cloud_provider = questionary.select( + "Where would you like to deploy your Nebari cluster?", + choices=enum_to_list(ProviderEnum), + qmark=qmark, + ).ask() + + if not disable_checks: + check_cloud_provider_creds(ctx, cloud_provider=inputs.cloud_provider) + + # specific context needed when `check_project_name` is called + ctx.params["cloud_provider"] = inputs.cloud_provider + + # PROJECT NAME + rich.print( + ( + "\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n\n" + ) + ) + inputs.project_name = questionary.text( + "What project name would you like to use?", + qmark=qmark, + validate=lambda text: True if len(text) > 0 else "Please enter a value", + ).ask() + + if not disable_checks: + check_project_name(ctx, inputs.project_name) + + # DOMAIN NAME + rich.print( + ( + "\n 🪴 Great! Now it's time to decide on the domain name (i.e the URL) that Nebari will be accessible from. " + "This should be domain that you own.\n\n" + ) + ) + inputs.domain_name = questionary.text( + "What domain name would you like to use?", + qmark=qmark, + validate=lambda text: True if len(text) > 0 else "Please enter a value", + ).ask() + + # AUTH PROVIDER + rich.print( + ( + "\n 🪴 Nebari comes with [green]Keycloak[/green], an open-source identity and access management tool. This is how users, groups and roles " + "are managed on the platform. To connect Keycloak with an identity provider, you can select one now.\n\n" + "\n\t❕ [purple]password[/purple] is the default option and is not connected to any external identity provider.\n" + ) + ) + inputs.auth_provider = questionary.select( + "What authentication provider would you like?", + choices=enum_to_list(AuthenticationEnum), + qmark=qmark, + ).ask() + + if not disable_checks: + check_auth_provider_creds(ctx, auth_provider=inputs.auth_provider) + + if inputs.auth_provider.lower() == AuthenticationEnum.auth0.value.lower(): + inputs.auth_auto_provision = questionary.confirm( + "Would you like us to auto provision the Auth0 Machine-to-Machine app?", + default=False, + qmark=qmark, + ).ask() + + elif inputs.auth_provider.lower() == AuthenticationEnum.github.value.lower(): + rich.print( + ( + ":warning: If you haven't done so already, please ensure the following:\n" + f"The `Homepage URL` is set to: [light_green]https://{inputs.domain_name}[/light_green]\n" + f"The `Authorization callback URL` is set to: [light_green]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" + ) + ) + + # GITOPS - REPOSITORY, CICD + rich.print( + ( + "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you wish to adopt a GitOps approach to managing this platform, " + "we will walk you through a set of questions to get that setup.\n\n" + ) + ) + if questionary.confirm( + "Would you like to adopt a GitOps approach to managing Nebari?", + default=False, + qmark=qmark, + ).ask(): + + repo_url = "http://{git_provider}/{org_name}/{repo_name}" + + git_provider = questionary.select( + "Which git provider would you like to use?", + choices=["github.com", "gitlab.com"], + qmark=qmark, + ).ask() + + org_name = questionary.text( + f"Which user or organization will this repository live under? ({repo_url.format(git_provider=git_provider, org_name='', repo_name='')})", + qmark=qmark, + ).ask() + + repo_name = questionary.text( + f"And what will the name of this repository be? ({repo_url.format(git_provider=git_provider, org_name=org_name, repo_name='')})", + qmark=qmark, + ).ask() + + inputs.repository = repo_url.format( + git_provider=git_provider, org_name=org_name, repo_name=repo_name + ) + + if git_provider == "github.com": + inputs.repository_auto_provision = questionary.confirm( + f"Would you like the following git repository to be automatically created: {inputs.repository}?", + default=False, + qmark=qmark, + ).ask() + + # TODO: create `check_repository_creds` function + if not disable_checks: + pass + + if git_provider == "github.com": + inputs.ci_provider = CiEnum.github_actions.value.lower() + elif git_provider == "gitlab.com": + inputs.ci_provider = CiEnum.gitlab_ci.value.lower() + + # SSL CERTIFICATE + rich.print( + ( + "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you want your Nebari domain to use a Let's Encrypt SSL certificate, " + "all we need is an email address from you.\n\n" + ) + ) + ssl_cert = questionary.confirm( + "Would you like to add a Let's Encrypt SSL certificate to your domain?", + default=False, + qmark=qmark, + ).ask() + + if ssl_cert: + inputs.ssl_cert_email = questionary.text( + "Which email address should Let's Encrypt associate the certificate with?", + qmark=qmark, + ).ask() + + # ADVANCED FEATURES + rich.print( + ( + "\n 🪴 This next section is [italic]optional[/italic] and includes advanced configuration changes. " + "⚠️ caution is advised!\n\n" + ) + ) + if questionary.confirm( + "Would you like to make advanced configuration changes ?", + default=False, + qmark=qmark, + ).ask(): + + # NAMESPACE + inputs.namespace = questionary.text( + "What namespace would like to use?", + default=inputs.namespace, + qmark=qmark, + ).ask() + + # TERRAFORM STATE + inputs.terraform_state = questionary.select( + "Where should the Terraform State be provisioned?", + choices=enum_to_list(TerraformStateEnum), + qmark=qmark, + ).ask() + + # KUBERNETES VERSION + inputs.kubernetes_version = questionary.text( + "Which Kubernetes version would you like to use?", + qmark=qmark, + ).ask() + + handle_init(inputs) + + rich.print( + ( + "Congratulations, you have generated the all important [purple]nebari-config.yaml[/purple] file 🎉\n\n" + "You can always edit your [purple]nebari-config.yaml[/purple] file by editing the file directly.\n" + "If you do make changes to it you can ensure its still a valid configuration by running:\n\n" + "\t\t[green]nebari validate --config path/to/nebari-config.yaml[/green]\n\n" + ) + ) + + base_cmd = f"nebari init {inputs.cloud_provider}" + + def if_used(key, model=inputs, ignore_list=["cloud_provider"]): + if key not in ignore_list: + b = "--{key} {value}" + value = getattr(model, key) + if isinstance(value, str) and (value != "" or value is not None): + return b.format(key=key, value=value).replace("_", "-") + if isinstance(value, bool) and value: + return b.format(key=key, value=value).replace("_", "-") + + cmds = " ".join( + [_ for _ in [if_used(_) for _ in inputs.dict().keys()] if _ is not None] + ) + + rich.print( + ( + "Here is the previous Guided Init if it was converted into a [green]nebari init[/green] command:\n\n" + f"\t\t[green]{base_cmd} {cmds}[/green]\n\n" + ) + ) + + raise typer.Exit() diff --git a/qhub/cli/main.py b/qhub/cli/main.py index b450da5f4..c8cc766d7 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -1,8 +1,5 @@ -import os from pathlib import Path -import questionary -import rich import typer from click import Context from rich import print @@ -12,6 +9,8 @@ check_auth_provider_creds, check_cloud_provider_creds, check_project_name, + enum_to_list, + guided_init_wizard, handle_init, ) from qhub.deploy import deploy_configuration @@ -27,9 +26,7 @@ ) from qhub.utils import load_yaml - -def enum_to_list(enum_cls): - return [e.value for e in enum_cls] +SECOND_COMMAND_GROUP_NAME = "Additional Commands" class OrderCommands(TyperGroup): @@ -48,208 +45,6 @@ def list_commands(self, ctx: Context): ) -def guided_init_wizard(ctx: typer.Context, guided_init: str): - - disable_checks = os.environ.get("QHUB_DISABLE_INIT_CHECKS", False) - - if not guided_init: - return guided_init - - rich.print( - ( - "Welcome to the Guided Init wizard!\n" - "You will be asked a few questions that are used to generate your [purple]nebari-config.yaml[/purple] and [purple].env[/purple] files.\n\n" - "For more detail about the [green]nebari init[/green] command, please refer to our docs:\n\n" - "\t\t[light_green]https://nebari-docs.netlify.app[/light_green]\n\n" - ) - ) - - if disable_checks: - rich.print( - "⚠️ Attempting to use the Guided Init wizard without any validation checks. There is no guarantee values provided will work! ⚠️\n\n" - ) - - qmark = " 🪴 " - - # pull in default values for each of the below - inputs = InitInputs() - - # CLOUD PROVIDER - inputs.cloud_provider = questionary.select( - "Where would you like to deploy your Nebari cluster?", - choices=enum_to_list(ProviderEnum), - qmark=qmark, - ).ask() - - if not disable_checks: - check_cloud_provider_creds(ctx, cloud_provider=inputs.cloud_provider) - - # specific context needed when `check_project_name` is called - ctx.params["cloud_provider"] = inputs.cloud_provider - - # PROJECT NAME - inputs.project_name = questionary.text( - "What project name would you like to use?", - qmark=qmark, - validate=lambda text: True if len(text) > 0 else "Please enter a value", - ).ask() - - if not disable_checks: - check_project_name(ctx, inputs.project_name) - - # DOMAIN NAME - inputs.domain_name = questionary.text( - "What domain name would you like to use?", - qmark=qmark, - validate=lambda text: True if len(text) > 0 else "Please enter a value", - ).ask() - - # NAMESPACE - inputs.namespace = questionary.text( - "What namespace would like to use?", - default=inputs.namespace, - qmark=qmark, - ).ask() - - # AUTH PROVIDER - inputs.auth_provider = questionary.select( - "What authentication provider would you like?", - choices=enum_to_list(AuthenticationEnum), - qmark=qmark, - ).ask() - - if not disable_checks: - check_auth_provider_creds(ctx, auth_provider=inputs.auth_provider) - - if inputs.auth_provider.lower() == AuthenticationEnum.auth0.value.lower(): - inputs.auth_auto_provision = questionary.confirm( - "Would you like us to auto provision the Auth0 Machine-to-Machine app?", - default=False, - qmark=qmark, - ).ask() - - elif inputs.auth_provider.lower() == AuthenticationEnum.github.value.lower(): - rich.print( - ( - ":warning: If you haven't done so already, please ensure the following:\n" - f"The `Homepage URL` is set to: [light_green]https://{inputs.domain_name}[/light_green]\n" - f"The `Authorization callback URL` is set to: [light_green]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" - ) - ) - - # REPOSITORY - if questionary.confirm( - "Would you like to store this project in a git repo?", - default=False, - qmark=qmark, - ).ask(): - - repo_url = "http://{git_provider}/{org_name}/{repo_name}" - - git_provider = questionary.select( - "Which git provider would you like to use?", - choices=["github.com", "gitlab.com"], - qmark=qmark, - ).ask() - - org_name = questionary.text( - f"Which user or organization will this repo live under? ({repo_url.format(git_provider=git_provider, org_name='', repo_name='')})", - qmark=qmark, - ).ask() - - repo_name = questionary.text( - f"And what is the name of this repo? ({repo_url.format(git_provider=git_provider, org_name=org_name, repo_name='')})", - qmark=qmark, - ).ask() - - inputs.repository = repo_url.format( - git_provider=git_provider, org_name=org_name, repo_name=repo_name - ) - - inputs.repository_auto_provision = questionary.confirm( - f"Would you like us to auto create the following git repo: {inputs.repository}?", - default=False, - qmark=qmark, - ).ask() - - # create `check_repository_creds` function - if not disable_checks: - pass - - # CICD - inputs.ci_provider = questionary.select( - "Would you like to adopt a GitOps workflow for this repository?", - choices=enum_to_list(CiEnum), - qmark=qmark, - ).ask() - - # SSL CERTIFICATE - ssl_cert = questionary.confirm( - "Would you like to add a Let's Encrypt SSL certificate to your cluster?", - default=False, - qmark=qmark, - ).ask() - - if ssl_cert: - inputs.ssl_cert_email = questionary.text( - "Which email address should Let's Encrypt associate the certificate with?", - qmark=qmark, - ).ask() - - # ADVANCED FEATURES - if questionary.confirm( - "Would you like to make advanced configuration changes (⚠️ caution is advised)?", - default=False, - qmark=qmark, - ).ask(): - - inputs.terraform_state = questionary.select( - "Where should the Terraform State be provisioned?", - choices=enum_to_list(TerraformStateEnum), - qmark=qmark, - ).ask() - - inputs.kubernetes_version = questionary.text( - "Which Kubernetes version would you like to use?", - qmark=qmark, - ).ask() - - handle_init(inputs) - - rich.print( - ( - "Congratulations, you have generated the all important [purple]nebari-config.yaml[/purple] file 🎉\n\n" - "You can always edit your [purple]nebari-config.yaml[/purple] file by editing the file directly.\n" - "If you do make changes to it you can ensure its still a valid configuration by running:\n\n" - "\t\t[green]nebari validate --config path/to/nebari-config.yaml[/green]\n\n" - ) - ) - - base_cmd = f"nebari init {inputs.cloud_provider}" - - def if_used(key, model=inputs, ignore_list=["cloud_provider"]): - if key not in ignore_list: - b = "--{key} {value}" - value = getattr(model, key) - if isinstance(value, str) and value != "": - return b.format(key=key, value=value).replace("_", "-") - if isinstance(value, bool) and value: - return b.format(key=key, value=value).replace("_", "-") - - cmds = " ".join( - [_ for _ in [if_used(_) for _ in inputs.dict().keys()] if _ is not None] - ) - - rich.print( - ( - "Here is the previous Guided Init if it was converted into a [green]nebari init[/green] command:\n\n" - f"\t\t[green]{base_cmd} {cmds}[/green]\n\n" - ) - ) - - raise typer.Exit() - - guided_init_help_msg = ( "[bold green]START HERE[/bold green] - this will gently guide you through a list of questions " "to generate your [purple]nebari-config.yaml[/purple]. " @@ -318,38 +113,36 @@ def init( """ Create and initialize your [purple]nebari-config.yaml[/purple] file. + This command will create and initialize your [purple]nebari-config.yaml[/purple] :sparkles: + + This file contains all your Nebari cluster configuration details and, + is used as input to later commands such as [green]nebari render[/green], [green]nebari deploy[/green], etc. + If you're new to Nebari, we recommend you use the Guided Init wizard. To get started simply run: [green]nebari init --guided-init[/green] - This command will generate: [purple]nebari-config.yaml[/purple] :sparkles: - - This file contains all your Nebari cluster configuration details and, - is used as input to later commands such as [green]nebari render[/green], [green]nebari deploy[/green], etc. """ - # """ - # Create and initialize your [purple]nebari-config.yaml[/purple] file. - # """ - # inputs = InitInputs() - - # inputs.cloud_provider = cloud_provider - # inputs.project_name = project_name - # inputs.domain_name = domain_name - # inputs.namespace = namespace - # inputs.auth_provider = auth_provider - # inputs.auth_auto_provision = auth_auto_provision - # inputs.repository = repository - # inputs.repository_auto_provision = repository_auto_provision - # inputs.ci_provider = ci_provider - # inputs.terraform_state = terraform_state - # inputs.kubernetes_version = kubernetes_version - # inputs.ssl_cert_email = ssl_cert_email - - # handle_init(inputs) + inputs = InitInputs() + inputs.cloud_provider = cloud_provider + inputs.project_name = project_name + inputs.domain_name = domain_name + inputs.namespace = namespace + inputs.auth_provider = auth_provider + inputs.auth_auto_provision = auth_auto_provision + inputs.repository = repository + inputs.repository_auto_provision = repository_auto_provision + inputs.ci_provider = ci_provider + inputs.terraform_state = terraform_state + inputs.kubernetes_version = kubernetes_version + inputs.ssl_cert_email = ssl_cert_email -@app.command() + handle_init(inputs) + + +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) def validate( config: str = typer.Option( None, @@ -381,7 +174,7 @@ def validate( print("[bold purple]Successfully validated configuration.[/bold purple]") -@app.command() +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) def render( output: str = typer.Option( "./", From ecc5f6570d5c74cdd1bc68b835ea235d24ccdbc7 Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Tue, 27 Sep 2022 09:00:50 +0530 Subject: [PATCH 18/43] 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: From 5a771e40fb191f9abd604b361c841582aafb62b5 Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 26 Sep 2022 20:31:46 -0700 Subject: [PATCH 19/43] clean up --- .env | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index e69de29bb..000000000 From 28404e52becfae97420576f79435d4ff46e32e45 Mon Sep 17 00:00:00 2001 From: iameskild Date: Tue, 27 Sep 2022 11:13:19 -0700 Subject: [PATCH 20/43] update help messages --- qhub/cli/_keycloak.py | 7 ++++++- qhub/cli/main.py | 46 +++++++++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/qhub/cli/_keycloak.py b/qhub/cli/_keycloak.py index 0ae76c212..3d8870b21 100644 --- a/qhub/cli/_keycloak.py +++ b/qhub/cli/_keycloak.py @@ -5,7 +5,12 @@ from qhub.keycloak import do_keycloak -app_keycloak = typer.Typer() +app_keycloak = typer.Typer( + add_completion=False, + no_args_is_help=True, + rich_markup_mode="rich", + context_settings={"help_option_names": ["-h", "--help"]}, +) @app_keycloak.command() diff --git a/qhub/cli/main.py b/qhub/cli/main.py index e58d5ac09..f9a19cfc1 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -34,6 +34,14 @@ from qhub.utils import load_yaml SECOND_COMMAND_GROUP_NAME = "Additional Commands" +GUIDED_INIT_MSG = ( + "[bold green]START HERE[/bold green] - this will gently guide you through a list of questions " + "to generate your [purple]nebari-config.yaml[/purple]. " + "It is an [i]alternative[/i] to passing the options listed below." +) +KEYCLOAK_COMMAND_MSG = ( + "Interact with the Nebari Keycloak identity and access management tool." +) class OrderCommands(TyperGroup): @@ -50,13 +58,11 @@ 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 = ( - "[bold green]START HERE[/bold green] - this will gently guide you through a list of questions " - "to generate your [purple]nebari-config.yaml[/purple]. " - "It is an [i]alternative[/i] to passing the options listed below." +app.add_typer( + app_keycloak, + name="keycloak", + help=KEYCLOAK_COMMAND_MSG, + rich_help_panel=SECOND_COMMAND_GROUP_NAME, ) @@ -70,7 +76,7 @@ def init( ), guided_init: bool = typer.Option( False, - help=guided_init_help_msg, + help=GUIDED_INIT_MSG, callback=guided_init_wizard, is_eager=True, ), @@ -163,7 +169,7 @@ def validate( ), ): """ - Validate the [purple]nebari-config.yaml[/purple] file. + Validate the values in the [purple]nebari-config.yaml[/purple] file are acceptable. """ config_filename = Path(config) if not config_filename.is_file(): @@ -320,7 +326,7 @@ def destroy( destroy_configuration(config_yaml) -@app.command() +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) def cost( path: str = typer.Option( None, @@ -354,7 +360,10 @@ def cost( ), ): """ - Cost-Estimate + Estimate the cost of deploying Nebari based on your [purple]nebari-config.yaml[/purple]. [italic]Experimental.[/italic] + + [italic]This is still only experimental using Infracost under the hood. + The estimated value is a base cost and does not include usage costs.[/italic] """ infracost_report( path=path, @@ -365,7 +374,7 @@ def cost( ) -@app.command() +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) def upgrade( config: str = typer.Option( ..., @@ -380,7 +389,11 @@ def upgrade( ), ): """ - Upgrade + Upgrade your [purple]nebari-config.yaml[/purple] from pre-0.4.0 to 0.4.0. + + Due to several breaking changes that came with the 0.4.0 release, this utility is available to help + update your [purple]nebari-config.yaml[/purple] to comply with the introduced changes. + See the project [green]RELEASE.md[/green] for details. """ config_filename = Path(config) if not config_filename.is_file(): @@ -391,7 +404,7 @@ def upgrade( do_upgrade(config_filename, attempt_fixes=attempt_fixes) -@app.command() +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) def support( config_filename: str = typer.Option( ..., @@ -407,7 +420,10 @@ def support( ), ): """ - Support + Support tool to write all Kubernetes logs locally and compress them into a zip file. + + The Nebari team recommends k9s to manage and inspect the state of the cluster. + However, this command occasionally helpful for debugging purposes should the logs need to be shared. """ kube_config.load_kube_config() From 3911f13134f15ed931820529f1cf3cf8e72a1aad Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:19:50 -0700 Subject: [PATCH 21/43] Update qhub/cli/_init.py Co-authored-by: Pavithra Eswaramoorthy --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index e4fd86e5f..91a9f5089 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -315,7 +315,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): rich.print( ( "\n 🪴 Great! Now it's time to decide on the domain name (i.e the URL) that Nebari will be accessible from. " - "This should be domain that you own.\n\n" + "This should be a domain that you own.\n\n" ) ) inputs.domain_name = questionary.text( From 562a8fc16f436b0df7dd0071c499c8e712b77fd8 Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:25:17 -0700 Subject: [PATCH 22/43] Update qhub/cli/_init.py Co-authored-by: Pavithra Eswaramoorthy --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 91a9f5089..5dfd3afab 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -435,7 +435,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): ) ) if questionary.confirm( - "Would you like to make advanced configuration changes ?", + "Would you like to make advanced configuration changes?", default=False, qmark=qmark, ).ask(): From 06d882b0243d86aa84ec64138b7c80a6ba348212 Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 3 Oct 2022 11:39:54 -0700 Subject: [PATCH 23/43] Changes from review, part 1 --- qhub/cli/_init.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 5dfd3afab..7d064e3cc 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -296,10 +296,20 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # specific context needed when `check_project_name` is called ctx.params["cloud_provider"] = inputs.cloud_provider + name_guidelines = """ + The project name must adhere to the following requirements: + - Letters from A to Z (upper and lower case) and numbers + - Maximum accepted length of the name string is 16 characters + """ + if inputs.cloud_provider == ProviderEnum.aws.value.lower(): + name_guidelines += "- Should NOT start with the string `aws`\n" + elif inputs.cloud_provider == ProviderEnum.azure.value.lower(): + name_guidelines += "- Should NOT contain `-`\n" + # PROJECT NAME rich.print( ( - "\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n\n" + f"\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n{name_guidelines}\n\n" ) ) inputs.project_name = questionary.text( @@ -361,7 +371,8 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): rich.print( ( "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you wish to adopt a GitOps approach to managing this platform, " - "we will walk you through a set of questions to get that setup.\n\n" + "we will walk you through a set of questions to get that setup. With this setup, Nebari will use GitHub Actions workflows (or GitLab equivalent) " + "to automatically handle the future deployments of your infrastructure.\n\n" ) ) if questionary.confirm( @@ -430,7 +441,9 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # ADVANCED FEATURES rich.print( ( - "\n 🪴 This next section is [italic]optional[/italic] and includes advanced configuration changes. " + # TODO once docs are updated, add links for more info on these changes + "\n 🪴 This next section is [italic]optional[/italic] and includes advanced configuration changes to the " + "Terraform state, Kubernetes Namespace and Kubernetes version.\n" "⚠️ caution is advised!\n\n" ) ) @@ -440,13 +453,6 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): qmark=qmark, ).ask(): - # NAMESPACE - inputs.namespace = questionary.text( - "What namespace would like to use?", - default=inputs.namespace, - qmark=qmark, - ).ask() - # TERRAFORM STATE inputs.terraform_state = questionary.select( "Where should the Terraform State be provisioned?", @@ -454,6 +460,13 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): qmark=qmark, ).ask() + # NAMESPACE + inputs.namespace = questionary.text( + "What namespace would like to use?", + default=inputs.namespace, + qmark=qmark, + ).ask() + # KUBERNETES VERSION inputs.kubernetes_version = questionary.text( "Which Kubernetes version would you like to use?", From 20f09d4887d80b2ee4746dbad37b9799d97c038a Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:40:46 -0700 Subject: [PATCH 24/43] Update qhub/cli/_init.py Co-authored-by: Pavithra Eswaramoorthy --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 7d064e3cc..1a2125c18 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -479,7 +479,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): ( "Congratulations, you have generated the all important [purple]nebari-config.yaml[/purple] file 🎉\n\n" "You can always edit your [purple]nebari-config.yaml[/purple] file by editing the file directly.\n" - "If you do make changes to it you can ensure its still a valid configuration by running:\n\n" + "If you do make changes to it you can ensure it's still a valid configuration by running:\n\n" "\t\t[green]nebari validate --config path/to/nebari-config.yaml[/green]\n\n" ) ) From 62dcbfd5146700f062d2d644da005ade57a9ae70 Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:41:07 -0700 Subject: [PATCH 25/43] Update qhub/cli/_init.py Co-authored-by: Pavithra Eswaramoorthy --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 1a2125c18..0d2ee6642 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -501,7 +501,7 @@ def if_used(key, model=inputs, ignore_list=["cloud_provider"]): rich.print( ( - "Here is the previous Guided Init if it was converted into a [green]nebari init[/green] command:\n\n" + "For reference, if the previous Guided Init answers were converted into a direct [green]nebari init[/green] command, it would be:\n\n" f"\t\t[green]{base_cmd} {cmds}[/green]\n\n" ) ) From 2abd34a242708169ce0fd48d61258baba77e243c Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:44:23 -0700 Subject: [PATCH 26/43] Update qhub/cli/_init.py Co-authored-by: Tania Allard --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 0d2ee6642..83c286dff 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -16,7 +16,7 @@ from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, yaml MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, please refer to this guide on how to generate them:\n\n[light_green]\t\t{link_to_docs}[/light_green]\n\n" -LINKS_TO_DOCS_TEMPLATE = "For more details, please refer to our docs:\n\n\t[light_green]{link_to_docs}[/light_green]\n\n" +LINKS_TO_DOCS_TEMPLATE = "For more details, refer to the Nebari docs:\n\n\t[light_green]{link_to_docs}[/light_green]\n\n" # links to external docs CREATE_AWS_CREDS = ( From 4c543178853b39eecbc133ddb60616208e96acaf Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:44:44 -0700 Subject: [PATCH 27/43] Update qhub/cli/_init.py Co-authored-by: Tania Allard --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 83c286dff..5a7a2b69a 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -44,7 +44,7 @@ def enum_to_list(enum_cls): def handle_init(inputs: InitInputs): if QHUB_IMAGE_TAG: print( - f"Modifying the image tags for the `default_images`, setting tags equal to: {QHUB_IMAGE_TAG}" + f"Modifying the image tags for the `default_images`, setting tags to: {QHUB_IMAGE_TAG}" ) if QHUB_DASK_VERSION: From 580ad0b81fabfdfb15a56bf75a23a236733ca781 Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:45:19 -0700 Subject: [PATCH 28/43] Update qhub/cli/_init.py Co-authored-by: Tania Allard --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 5a7a2b69a..ebf4e5bf2 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -49,7 +49,7 @@ def handle_init(inputs: InitInputs): if QHUB_DASK_VERSION: print( - f"Modifying the version of the `qhub_dask` package, setting version equal to: {QHUB_DASK_VERSION}" + f"Modifying the version of the `qhub_dask` package, setting version to: {QHUB_DASK_VERSION}" ) # this will force the `set_kubernetes_version` to grab the latest version From 3b3374834c526eefdea79d26854e318672328488 Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:45:40 -0700 Subject: [PATCH 29/43] Update qhub/cli/_init.py Co-authored-by: Tania Allard --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index ebf4e5bf2..eaee02574 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -15,7 +15,7 @@ ) from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, yaml -MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, please refer to this guide on how to generate them:\n\n[light_green]\t\t{link_to_docs}[/light_green]\n\n" +MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, refer to this guide on how to generate them:\n\n[light_green]\t\t{link_to_docs}[/light_green]\n\n" LINKS_TO_DOCS_TEMPLATE = "For more details, refer to the Nebari docs:\n\n\t[light_green]{link_to_docs}[/light_green]\n\n" # links to external docs From cd59e6daa9d79e8897480b84ef5a727b55dd1276 Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:45:53 -0700 Subject: [PATCH 30/43] Update qhub/cli/_init.py Co-authored-by: Tania Allard --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index eaee02574..6624b3284 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -260,7 +260,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): rich.print( ( "\n\t\t[bold]Welcome to the Guided Init wizard![/bold]\n\n" - "You will be asked a few questions that are used to generate your [purple]nebari-config.yaml[/purple]. " + "You will be asked a few questions to generate your [purple]nebari-config.yaml[/purple]. " f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=DOCS_HOME)}" ) ) From 05dcb8c7eeac36d23d467a2e46180164e3eeb502 Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:46:07 -0700 Subject: [PATCH 31/43] Update qhub/cli/main.py Co-authored-by: Tania Allard --- qhub/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index f9a19cfc1..d98f41297 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -35,7 +35,7 @@ SECOND_COMMAND_GROUP_NAME = "Additional Commands" GUIDED_INIT_MSG = ( - "[bold green]START HERE[/bold green] - this will gently guide you through a list of questions " + "[bold green]START HERE[/bold green] - this will guide you step-by-step " "to generate your [purple]nebari-config.yaml[/purple]. " "It is an [i]alternative[/i] to passing the options listed below." ) From fe4fb796d64c32ebbb57dd60b91fc563765933cb Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 3 Oct 2022 12:53:32 -0700 Subject: [PATCH 32/43] Changes from review, part 2 --- qhub/cli/_init.py | 463 +++++++++++++++++++++++---------------------- qhub/initialize.py | 13 -- 2 files changed, 241 insertions(+), 235 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 6624b3284..b269de67a 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -15,7 +15,7 @@ ) from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, yaml -MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, refer to this guide on how to generate them:\n\n[light_green]\t\t{link_to_docs}[/light_green]\n\n" +MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, refer to this guide on how to generate them:\n\n[light_green]\t{link_to_docs}[/light_green]\n\n" LINKS_TO_DOCS_TEMPLATE = "For more details, refer to the Nebari docs:\n\n\t[light_green]{link_to_docs}[/light_green]\n\n" # links to external docs @@ -98,11 +98,11 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( - "Please enter your AWS_ACCESS_KEY_ID", + "Paste your AWS_ACCESS_KEY_ID", hide_input=True, ) os.environ["AWS_SECRET_ACCESS_KEY"] = typer.prompt( - "Please enter your AWS_SECRET_ACCESS_KEY", + "Paste your AWS_SECRET_ACCESS_KEY", hide_input=True, ) @@ -117,11 +117,11 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) os.environ["GOOGLE_CREDENTIALS"] = typer.prompt( - "Please enter your GOOGLE_CREDENTIALS", + "Paste your GOOGLE_CREDENTIALS", hide_input=True, ) os.environ["PROJECT_ID"] = typer.prompt( - "Please enter your PROJECT_ID", + "Paste your PROJECT_ID", hide_input=True, ) @@ -138,15 +138,15 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) os.environ["DIGITALOCEAN_TOKEN"] = typer.prompt( - "Please enter your DIGITALOCEAN_TOKEN", + "Paste your DIGITALOCEAN_TOKEN", hide_input=True, ) os.environ["SPACES_ACCESS_KEY_ID"] = typer.prompt( - "Please enter your SPACES_ACCESS_KEY_ID", + "Paste your SPACES_ACCESS_KEY_ID", hide_input=True, ) os.environ["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( - "Please enter your SPACES_SECRET_ACCESS_KEY", + "Paste your SPACES_SECRET_ACCESS_KEY", hide_input=True, ) @@ -164,19 +164,19 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): ) os.environ["ARM_CLIENT_ID"] = typer.prompt( - "Please enter your ARM_CLIENT_ID", + "Paste your ARM_CLIENT_ID", hide_input=True, ) os.environ["ARM_CLIENT_SECRET"] = typer.prompt( - "Please enter your ARM_CLIENT_SECRET", + "Paste your ARM_CLIENT_SECRET", hide_input=True, ) os.environ["ARM_SUBSCRIPTION_ID"] = typer.prompt( - "Please enter your ARM_SUBSCRIPTION_ID", + "Paste your ARM_SUBSCRIPTION_ID", hide_input=True, ) os.environ["ARM_TENANT_ID"] = typer.prompt( - "Please enter your ARM_TENANT_ID", + "Paste your ARM_TENANT_ID", hide_input=True, ) @@ -201,15 +201,15 @@ def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): ) os.environ["AUTH0_CLIENT_ID"] = typer.prompt( - "Please enter your AUTH0_CLIENT_ID", + "Paste your AUTH0_CLIENT_ID", hide_input=True, ) os.environ["AUTH0_CLIENT_SECRET"] = typer.prompt( - "Please enter your AUTH0_CLIENT_SECRET", + "Paste your AUTH0_CLIENT_SECRET", hide_input=True, ) os.environ["AUTH0_DOMAIN"] = typer.prompt( - "Please enter your AUTH0_DOMAIN", + "Paste your AUTH0_DOMAIN", hide_input=True, ) @@ -226,11 +226,11 @@ def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): ) os.environ["GITHUB_CLIENT_ID"] = typer.prompt( - "Please enter your GITHUB_CLIENT_ID", + "Paste your GITHUB_CLIENT_ID", hide_input=True, ) os.environ["GITHUB_CLIENT_SECRET"] = typer.prompt( - "Please enter your GITHUB_CLIENT_SECRET", + "Paste your GITHUB_CLIENT_SECRET", hide_input=True, ) @@ -257,253 +257,272 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): if not guided_init: return guided_init - rich.print( - ( - "\n\t\t[bold]Welcome to the Guided Init wizard![/bold]\n\n" - "You will be asked a few questions to generate your [purple]nebari-config.yaml[/purple]. " - f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=DOCS_HOME)}" - ) - ) - - if disable_checks: + try: rich.print( - "⚠️ Attempting to use the Guided Init wizard without any validation checks. There is no guarantee values provided will work! ⚠️\n\n" + ( + "\n\t[bold]Welcome to the Guided Init wizard![/bold]\n\n" + "You will be asked a few questions to generate your [purple]nebari-config.yaml[/purple]. " + f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=DOCS_HOME)}" + ) ) - # pull in default values for each of the below - inputs = InitInputs() + if disable_checks: + rich.print( + "⚠️ Attempting to use the Guided Init wizard without any validation checks. There is no guarantee values provided will work! ⚠️\n\n" + ) + + # pull in default values for each of the below + inputs = InitInputs() - # CLOUD PROVIDER - rich.print( - ( - "\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " - "is where you want this Kubernetes cluster deployed. " - f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=CHOOSE_CLOUD_PROVIDER)}" - "\n\t❕ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " - "[italic]Currently only available on Linux OS.[/italic]" - "\n\t❕ [purple]existing[/purple] refers to an existing Kubernetes cluster that Nebari can be deployed on.\n" + # CLOUD PROVIDER + rich.print( + ( + "\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " + "is where you want this Kubernetes cluster deployed. " + f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=CHOOSE_CLOUD_PROVIDER)}" + "\n\t❕ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " + "[italic]Currently only available on Linux OS.[/italic]" + "\n\t❕ [purple]existing[/purple] refers to an existing Kubernetes cluster that Nebari can be deployed on.\n" + ) ) - ) - inputs.cloud_provider = questionary.select( - "Where would you like to deploy your Nebari cluster?", - choices=enum_to_list(ProviderEnum), - qmark=qmark, - ).ask() - - if not disable_checks: - check_cloud_provider_creds(ctx, cloud_provider=inputs.cloud_provider) - - # specific context needed when `check_project_name` is called - ctx.params["cloud_provider"] = inputs.cloud_provider - - name_guidelines = """ - The project name must adhere to the following requirements: - - Letters from A to Z (upper and lower case) and numbers - - Maximum accepted length of the name string is 16 characters - """ - if inputs.cloud_provider == ProviderEnum.aws.value.lower(): - name_guidelines += "- Should NOT start with the string `aws`\n" - elif inputs.cloud_provider == ProviderEnum.azure.value.lower(): - name_guidelines += "- Should NOT contain `-`\n" + # try: + inputs.cloud_provider = questionary.select( + "Where would you like to deploy your Nebari cluster?", + choices=enum_to_list(ProviderEnum), + qmark=qmark, + ).unsafe_ask() + # except KeyboardInterrupt: + # print("I'm HERE") + # raise typer.Exit() - # PROJECT NAME - rich.print( - ( - f"\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n{name_guidelines}\n\n" + # print("WHY AM I HERE?") + if not disable_checks: + check_cloud_provider_creds(ctx, cloud_provider=inputs.cloud_provider) + + # specific context needed when `check_project_name` is called + ctx.params["cloud_provider"] = inputs.cloud_provider + + name_guidelines = """ + The project name must adhere to the following requirements: + - Letters from A to Z (upper and lower case) and numbers + - Maximum accepted length of the name string is 16 characters + """ + if inputs.cloud_provider == ProviderEnum.aws.value.lower(): + name_guidelines += "- Should NOT start with the string `aws`\n" + elif inputs.cloud_provider == ProviderEnum.azure.value.lower(): + name_guidelines += "- Should NOT contain `-`\n" + + # PROJECT NAME + rich.print( + ( + f"\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n{name_guidelines}\n\n" + ) ) - ) - inputs.project_name = questionary.text( - "What project name would you like to use?", - qmark=qmark, - validate=lambda text: True if len(text) > 0 else "Please enter a value", - ).ask() + inputs.project_name = questionary.text( + "What project name would you like to use?", + qmark=qmark, + validate=lambda text: True if len(text) > 0 else "Please enter a value", + ).unsafe_ask() - if not disable_checks: - check_project_name(ctx, inputs.project_name) + if not disable_checks: + check_project_name(ctx, inputs.project_name) - # DOMAIN NAME - rich.print( - ( - "\n 🪴 Great! Now it's time to decide on the domain name (i.e the URL) that Nebari will be accessible from. " - "This should be a domain that you own.\n\n" - ) - ) - inputs.domain_name = questionary.text( - "What domain name would you like to use?", - qmark=qmark, - validate=lambda text: True if len(text) > 0 else "Please enter a value", - ).ask() - - # AUTH PROVIDER - rich.print( - ( - "\n 🪴 Nebari comes with [green]Keycloak[/green], an open-source identity and access management tool. This is how users, groups and roles " - "are managed on the platform. To connect Keycloak with an identity provider, you can select one now.\n\n" - "\n\t❕ [purple]password[/purple] is the default option and is not connected to any external identity provider.\n" + # DOMAIN NAME + rich.print( + ( + "\n 🪴 Great! Now it's time to decide on the domain name (i.e the URL) that Nebari will be accessible from. " + "This should be a domain that you own.\n\n" + ) ) - ) - inputs.auth_provider = questionary.select( - "What authentication provider would you like?", - choices=enum_to_list(AuthenticationEnum), - qmark=qmark, - ).ask() - - if not disable_checks: - check_auth_provider_creds(ctx, auth_provider=inputs.auth_provider) - - if inputs.auth_provider.lower() == AuthenticationEnum.auth0.value.lower(): - inputs.auth_auto_provision = questionary.confirm( - "Would you like us to auto provision the Auth0 Machine-to-Machine app?", - default=False, + inputs.domain_name = questionary.text( + "What domain name would you like to use?", qmark=qmark, - ).ask() + validate=lambda text: True if len(text) > 0 else "Please enter a value", + ).unsafe_ask() - elif inputs.auth_provider.lower() == AuthenticationEnum.github.value.lower(): + # AUTH PROVIDER rich.print( ( - ":warning: If you haven't done so already, please ensure the following:\n" - f"The `Homepage URL` is set to: [light_green]https://{inputs.domain_name}[/light_green]\n" - f"The `Authorization callback URL` is set to: [light_green]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" + "\n 🪴 Nebari comes with [green]Keycloak[/green], an open-source identity and access management tool. This is how users, groups and roles " + "are managed on the platform. To connect Keycloak with an identity provider, you can select one now.\n\n" + "\n\t❕ [purple]password[/purple] is the default option and is not connected to any external identity provider.\n" ) ) + inputs.auth_provider = questionary.select( + "What authentication provider would you like?", + choices=enum_to_list(AuthenticationEnum), + qmark=qmark, + ).unsafe_ask() - # GITOPS - REPOSITORY, CICD - rich.print( - ( - "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you wish to adopt a GitOps approach to managing this platform, " - "we will walk you through a set of questions to get that setup. With this setup, Nebari will use GitHub Actions workflows (or GitLab equivalent) " - "to automatically handle the future deployments of your infrastructure.\n\n" - ) - ) - if questionary.confirm( - "Would you like to adopt a GitOps approach to managing Nebari?", - default=False, - qmark=qmark, - ).ask(): + if not disable_checks: + check_auth_provider_creds(ctx, auth_provider=inputs.auth_provider) - repo_url = "http://{git_provider}/{org_name}/{repo_name}" + if inputs.auth_provider.lower() == AuthenticationEnum.auth0.value.lower(): + inputs.auth_auto_provision = questionary.confirm( + "Would you like us to auto provision the Auth0 Machine-to-Machine app?", + default=False, + qmark=qmark, + ).unsafe_ask() + + elif inputs.auth_provider.lower() == AuthenticationEnum.github.value.lower(): + rich.print( + ( + ":warning: If you haven't done so already, please ensure the following:\n" + f"The `Homepage URL` is set to: [light_green]https://{inputs.domain_name}[/light_green]\n" + f"The `Authorization callback URL` is set to: [light_green]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" + ) + ) - git_provider = questionary.select( - "Which git provider would you like to use?", - choices=["github.com", "gitlab.com"], + # GITOPS - REPOSITORY, CICD + rich.print( + ( + "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you wish to adopt a GitOps approach to managing this platform, " + "we will walk you through a set of questions to get that setup. With this setup, Nebari will use GitHub Actions workflows (or GitLab equivalent) " + "to automatically handle the future deployments of your infrastructure.\n\n" + ) + ) + if questionary.confirm( + "Would you like to adopt a GitOps approach to managing Nebari?", + default=False, qmark=qmark, - ).ask() + ).unsafe_ask(): - org_name = questionary.text( - f"Which user or organization will this repository live under? ({repo_url.format(git_provider=git_provider, org_name='', repo_name='')})", - qmark=qmark, - ).ask() + repo_url = "http://{git_provider}/{org_name}/{repo_name}" - repo_name = questionary.text( - f"And what will the name of this repository be? ({repo_url.format(git_provider=git_provider, org_name=org_name, repo_name='')})", - qmark=qmark, - ).ask() + git_provider = questionary.select( + "Which git provider would you like to use?", + choices=["github.com", "gitlab.com"], + qmark=qmark, + ).unsafe_ask() - inputs.repository = repo_url.format( - git_provider=git_provider, org_name=org_name, repo_name=repo_name - ) + org_name = questionary.text( + f"Which user or organization will this repository live under? ({repo_url.format(git_provider=git_provider, org_name='', repo_name='')})", + qmark=qmark, + ).unsafe_ask() - if git_provider == "github.com": - inputs.repository_auto_provision = questionary.confirm( - f"Would you like the following git repository to be automatically created: {inputs.repository}?", - default=False, + repo_name = questionary.text( + f"And what will the name of this repository be? ({repo_url.format(git_provider=git_provider, org_name=org_name, repo_name='')})", qmark=qmark, - ).ask() + ).unsafe_ask() - # TODO: create `check_repository_creds` function - if not disable_checks: - pass + inputs.repository = repo_url.format( + git_provider=git_provider, org_name=org_name, repo_name=repo_name + ) - if git_provider == "github.com": - inputs.ci_provider = CiEnum.github_actions.value.lower() - elif git_provider == "gitlab.com": - inputs.ci_provider = CiEnum.gitlab_ci.value.lower() + if git_provider == "github.com": + inputs.repository_auto_provision = questionary.confirm( + f"Would you like the following git repository to be automatically created: {inputs.repository}?", + default=False, + qmark=qmark, + ).unsafe_ask() - # SSL CERTIFICATE - rich.print( - ( - "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you want your Nebari domain to use a Let's Encrypt SSL certificate, " - "all we need is an email address from you.\n\n" + # TODO: create `check_repository_creds` function + if not disable_checks: + pass + + if git_provider == "github.com": + inputs.ci_provider = CiEnum.github_actions.value.lower() + elif git_provider == "gitlab.com": + inputs.ci_provider = CiEnum.gitlab_ci.value.lower() + + # SSL CERTIFICATE + rich.print( + ( + "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you want your Nebari domain to use a Let's Encrypt SSL certificate, " + "all we need is an email address from you.\n\n" + ) ) - ) - ssl_cert = questionary.confirm( - "Would you like to add a Let's Encrypt SSL certificate to your domain?", - default=False, - qmark=qmark, - ).ask() - - if ssl_cert: - inputs.ssl_cert_email = questionary.text( - "Which email address should Let's Encrypt associate the certificate with?", + ssl_cert = questionary.confirm( + "Would you like to add a Let's Encrypt SSL certificate to your domain?", + default=False, qmark=qmark, - ).ask() + ).unsafe_ask() - # ADVANCED FEATURES - rich.print( - ( - # TODO once docs are updated, add links for more info on these changes - "\n 🪴 This next section is [italic]optional[/italic] and includes advanced configuration changes to the " - "Terraform state, Kubernetes Namespace and Kubernetes version.\n" - "⚠️ caution is advised!\n\n" + if ssl_cert: + inputs.ssl_cert_email = questionary.text( + "Which email address should Let's Encrypt associate the certificate with?", + qmark=qmark, + ).unsafe_ask() + + # ADVANCED FEATURES + rich.print( + ( + # TODO once docs are updated, add links for more info on these changes + "\n 🪴 This next section is [italic]optional[/italic] and includes advanced configuration changes to the " + "Terraform state, Kubernetes Namespace and Kubernetes version." + "\n ⚠️ caution is advised!\n\n" + ) ) - ) - if questionary.confirm( - "Would you like to make advanced configuration changes?", - default=False, - qmark=qmark, - ).ask(): - - # TERRAFORM STATE - inputs.terraform_state = questionary.select( - "Where should the Terraform State be provisioned?", - choices=enum_to_list(TerraformStateEnum), + if questionary.confirm( + "Would you like to make advanced configuration changes?", + default=False, qmark=qmark, - ).ask() + ).unsafe_ask(): - # NAMESPACE - inputs.namespace = questionary.text( - "What namespace would like to use?", - default=inputs.namespace, - qmark=qmark, - ).ask() + # TERRAFORM STATE + inputs.terraform_state = questionary.select( + "Where should the Terraform State be provisioned?", + choices=enum_to_list(TerraformStateEnum), + qmark=qmark, + ).unsafe_ask() - # KUBERNETES VERSION - inputs.kubernetes_version = questionary.text( - "Which Kubernetes version would you like to use?", - qmark=qmark, - ).ask() + # NAMESPACE + inputs.namespace = questionary.text( + "What namespace would like to use?", + default=inputs.namespace, + qmark=qmark, + ).unsafe_ask() - handle_init(inputs) + # KUBERNETES VERSION + inputs.kubernetes_version = questionary.text( + "Which Kubernetes version would you like to use?", + qmark=qmark, + ).unsafe_ask() + + handle_init(inputs) - rich.print( - ( - "Congratulations, you have generated the all important [purple]nebari-config.yaml[/purple] file 🎉\n\n" - "You can always edit your [purple]nebari-config.yaml[/purple] file by editing the file directly.\n" - "If you do make changes to it you can ensure it's still a valid configuration by running:\n\n" - "\t\t[green]nebari validate --config path/to/nebari-config.yaml[/green]\n\n" + rich.print( + ( + "\n\n\t:sparkles: [bold]Congratulations[/bold], you have generated the all important [purple]nebari-config.yaml[/purple] file :sparkles:\n\n" + "You can always make changes to your [purple]nebari-config.yaml[/purple] file by editing the file directly.\n" + "If you do make changes to it you can ensure it's still a valid configuration by running:\n\n" + "\t[green]nebari validate --config path/to/nebari-config.yaml[/green]\n\n" + ) ) - ) - base_cmd = f"nebari init {inputs.cloud_provider}" + base_cmd = f"nebari init {inputs.cloud_provider}" - def if_used(key, model=inputs, ignore_list=["cloud_provider"]): - if key not in ignore_list: - b = "--{key} {value}" - value = getattr(model, key) - if isinstance(value, str) and (value != "" or value is not None): - return b.format(key=key, value=value).replace("_", "-") - if isinstance(value, bool) and value: - return b.format(key=key, value=value).replace("_", "-") + def if_used(key, model=inputs, ignore_list=["cloud_provider"]): + if key not in ignore_list: + b = "--{key} {value}" + value = getattr(model, key) + if isinstance(value, str) and (value != "" or value is not None): + return b.format(key=key, value=value).replace("_", "-") + if isinstance(value, bool) and value: + return b.format(key=key, value=value).replace("_", "-") - cmds = " ".join( - [_ for _ in [if_used(_) for _ in inputs.dict().keys()] if _ is not None] - ) + cmds = " ".join( + [_ for _ in [if_used(_) for _ in inputs.dict().keys()] if _ is not None] + ) - rich.print( - ( - "For reference, if the previous Guided Init answers were converted into a direct [green]nebari init[/green] command, it would be:\n\n" - f"\t\t[green]{base_cmd} {cmds}[/green]\n\n" + rich.print( + ( + "For reference, if the previous Guided Init answers were converted into a direct [green]nebari init[/green] command, it would be:\n\n" + f"\t[green]{base_cmd} {cmds}[/green]\n\n" + ) ) - ) + + rich.print( + ( + "You can now deploy your Nebari instance with:\n\n" + "\t[green]nebari deploy -c nebari-config.yaml[/green]\n\n" + "For more information, run [green]nebari deploy --help[/green] or check out the documentation: " + "[light_green]https://www.nebari.dev/how-tos/[/light_green]" + ) + ) + + except KeyboardInterrupt: + rich.print("\nUser quit the Guided Init.\n\n ") + raise typer.Exit() raise typer.Exit() diff --git a/qhub/initialize.py b/qhub/initialize.py index c19cc869d..18c9c2f84 100644 --- a/qhub/initialize.py +++ b/qhub/initialize.py @@ -337,25 +337,12 @@ def render_config( f.write(default_password) os.chmod(default_password_filename, 0o700) - print( - f"Securely generated default random password={default_password} for Keycloak root user stored at path={default_password_filename}" - ) - config["theme"]["jupyterhub"]["hub_title"] = f"QHub - { project_name }" config["theme"]["jupyterhub"][ "welcome" ] = f"""Welcome to { qhub_domain }. It is maintained by Quansight staff. The hub's configuration is stored in a github repository based on https://github.com/Quansight/qhub/. To provide feedback and report any technical problems, please use the github issue tracker.""" if auth_provider == "github": - config["security"]["authentication"] = AUTH_OAUTH_GITHUB.copy() - print( - "Visit https://github.com/settings/developers and create oauth application" - ) - print(f" set the homepage to: https://{qhub_domain}/") - print( - f" set the callback_url to: https://{qhub_domain}/auth/realms/qhub/broker/github/endpoint" - ) - if not disable_prompt: config["security"]["authentication"]["config"]["client_id"] = input( "Github client_id: " From 6b921687339c951d3869383c6f3ae63615029925 Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 3 Oct 2022 13:09:23 -0700 Subject: [PATCH 33/43] Update env, clean up --- environment-dev.yaml | 5 +++++ qhub/initialize.py | 1 + setup.cfg | 1 + 3 files changed, 7 insertions(+) diff --git a/environment-dev.yaml b/environment-dev.yaml index 1b9101cff..6d3c808c9 100644 --- a/environment-dev.yaml +++ b/environment-dev.yaml @@ -10,9 +10,14 @@ dependencies: - bcrypt - python-kubernetes - rich + - typer + - pip + - pip: + - questionary # dev dependencies - flake8 ==3.8.4 - black ==22.3.0 + - importlib-metadata<5.0 - twine - pytest - diagrams diff --git a/qhub/initialize.py b/qhub/initialize.py index 18c9c2f84..c94b14aff 100644 --- a/qhub/initialize.py +++ b/qhub/initialize.py @@ -343,6 +343,7 @@ def render_config( ] = f"""Welcome to { qhub_domain }. It is maintained by Quansight staff. The hub's configuration is stored in a github repository based on https://github.com/Quansight/qhub/. To provide feedback and report any technical problems, please use the github issue tracker.""" if auth_provider == "github": + config["security"]["authentication"] = AUTH_OAUTH_GITHUB.copy() if not disable_prompt: config["security"]["authentication"]["config"]["client_id"] = input( "Github client_id: " diff --git a/setup.cfg b/setup.cfg index 314d5a97c..2e9f3c04d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ install_requires = dev = flake8==3.8.4 black==22.3.0 + importlib-metadata<5.0 twine pytest pytest-timeout From 44ef8dfacf09557d1b34201c3684792cd5e35491 Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 3 Oct 2022 15:10:16 -0700 Subject: [PATCH 34/43] Changes from review, part 3 --- qhub/cli/_init.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index b269de67a..cb81d277e 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -277,12 +277,12 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # CLOUD PROVIDER rich.print( ( - "\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " + "\n\n\n\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " "is where you want this Kubernetes cluster deployed. " f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=CHOOSE_CLOUD_PROVIDER)}" - "\n\t❕ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " + "\n\t❗️ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " "[italic]Currently only available on Linux OS.[/italic]" - "\n\t❕ [purple]existing[/purple] refers to an existing Kubernetes cluster that Nebari can be deployed on.\n" + "\n\t❗️ [purple]existing[/purple] refers to an existing Kubernetes cluster that Nebari can be deployed on.\n" ) ) # try: @@ -315,7 +315,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # PROJECT NAME rich.print( ( - f"\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n{name_guidelines}\n\n" + f"\n\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n{name_guidelines}\n\n" ) ) inputs.project_name = questionary.text( @@ -330,7 +330,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # DOMAIN NAME rich.print( ( - "\n 🪴 Great! Now it's time to decide on the domain name (i.e the URL) that Nebari will be accessible from. " + "\n\n 🪴 Great! Now it's time to decide on the domain name (i.e the URL) that Nebari will be accessible from. " "This should be a domain that you own.\n\n" ) ) @@ -343,9 +343,9 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # AUTH PROVIDER rich.print( ( - "\n 🪴 Nebari comes with [green]Keycloak[/green], an open-source identity and access management tool. This is how users, groups and roles " + "\n\n 🪴 Nebari comes with [green]Keycloak[/green], an open-source identity and access management tool. This is how users, groups and roles " "are managed on the platform. To connect Keycloak with an identity provider, you can select one now.\n\n" - "\n\t❕ [purple]password[/purple] is the default option and is not connected to any external identity provider.\n" + "\n\t❗️ [purple]password[/purple] is the default option and is not connected to any external identity provider.\n" ) ) inputs.auth_provider = questionary.select( @@ -376,7 +376,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # GITOPS - REPOSITORY, CICD rich.print( ( - "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you wish to adopt a GitOps approach to managing this platform, " + "\n\n 🪴 This next section is [italic]optional[/italic] but recommended. If you wish to adopt a GitOps approach to managing this platform, " "we will walk you through a set of questions to get that setup. With this setup, Nebari will use GitHub Actions workflows (or GitLab equivalent) " "to automatically handle the future deployments of your infrastructure.\n\n" ) @@ -428,7 +428,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # SSL CERTIFICATE rich.print( ( - "\n 🪴 This next section is [italic]optional[/italic] but recommended. If you want your Nebari domain to use a Let's Encrypt SSL certificate, " + "\n\n 🪴 This next section is [italic]optional[/italic] but recommended. If you want your Nebari domain to use a Let's Encrypt SSL certificate, " "all we need is an email address from you.\n\n" ) ) @@ -448,7 +448,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): rich.print( ( # TODO once docs are updated, add links for more info on these changes - "\n 🪴 This next section is [italic]optional[/italic] and includes advanced configuration changes to the " + "\n\n 🪴 This next section is [italic]optional[/italic] and includes advanced configuration changes to the " "Terraform state, Kubernetes Namespace and Kubernetes version." "\n ⚠️ caution is advised!\n\n" ) From edcb910f74a8745f2514b90766298c75fa28571a Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 3 Oct 2022 15:12:40 -0700 Subject: [PATCH 35/43] Minor update --- qhub/cli/_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index cb81d277e..756c04e02 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -277,7 +277,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # CLOUD PROVIDER rich.print( ( - "\n\n\n\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " + "\n\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " "is where you want this Kubernetes cluster deployed. " f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=CHOOSE_CLOUD_PROVIDER)}" "\n\t❗️ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " From 4285ebfeda1f4389f343e445594b729a20b08f55 Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 3 Oct 2022 15:39:43 -0700 Subject: [PATCH 36/43] Changes based review, part 4 --- qhub/cli/_init.py | 28 +++++++++++++++++----------- qhub/schema.py | 6 +++--- setup.cfg | 2 ++ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 756c04e02..ed1821176 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -15,8 +15,10 @@ ) from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, yaml -MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, refer to this guide on how to generate them:\n\n[light_green]\t{link_to_docs}[/light_green]\n\n" -LINKS_TO_DOCS_TEMPLATE = "For more details, refer to the Nebari docs:\n\n\t[light_green]{link_to_docs}[/light_green]\n\n" +MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, refer to this guide on how to generate them:\n\n[blue1]\t{link_to_docs}[/blue1]\n\n" +LINKS_TO_DOCS_TEMPLATE = ( + "For more details, refer to the Nebari docs:\n\n\t[blue1]{link_to_docs}[/blue1]\n\n" +) # links to external docs CREATE_AWS_CREDS = ( @@ -33,8 +35,8 @@ CREATE_GITHUB_OAUTH_CREDS = "https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app" # links to Nebari docs -DOCS_HOME = "https://nebari-docs.netlify.app" -CHOOSE_CLOUD_PROVIDER = "https://nebari-docs.netlify.app/getting-started/deploy" +DOCS_HOME = "https://nebari.dev" +CHOOSE_CLOUD_PROVIDER = "https://nebari.dev/getting-started/deploy" def enum_to_list(enum_cls): @@ -42,6 +44,9 @@ def enum_to_list(enum_cls): def handle_init(inputs: InitInputs): + """ + Take the inputs from the `nebari init` command, render the config and write it to a local yaml file. + """ if QHUB_IMAGE_TAG: print( f"Modifying the image tags for the `default_images`, setting tags to: {QHUB_IMAGE_TAG}" @@ -277,7 +282,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # CLOUD PROVIDER rich.print( ( - "\n\n 🪴 Nebari runs on a Kubernetes cluster so one of the first choices that needs to be made " + "\n\n 🪴 Nebari runs on a Kubernetes cluster: where do you want this Kubernetes cluster deployed? " "is where you want this Kubernetes cluster deployed. " f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=CHOOSE_CLOUD_PROVIDER)}" "\n\t❗️ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " @@ -330,7 +335,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # DOMAIN NAME rich.print( ( - "\n\n 🪴 Great! Now it's time to decide on the domain name (i.e the URL) that Nebari will be accessible from. " + "\n\n 🪴 Great! Now you need to provide a valid domain name (i.e. the URL) to access your Nebri instance. " "This should be a domain that you own.\n\n" ) ) @@ -343,7 +348,8 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # AUTH PROVIDER rich.print( ( - "\n\n 🪴 Nebari comes with [green]Keycloak[/green], an open-source identity and access management tool. This is how users, groups and roles " + # TODO once docs are updated, add links for more details + "\n\n 🪴 Nebari comes with [green]Keycloak[/green], an open-source identity and access management tool. This is how users and permissions " "are managed on the platform. To connect Keycloak with an identity provider, you can select one now.\n\n" "\n\t❗️ [purple]password[/purple] is the default option and is not connected to any external identity provider.\n" ) @@ -368,8 +374,8 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): rich.print( ( ":warning: If you haven't done so already, please ensure the following:\n" - f"The `Homepage URL` is set to: [light_green]https://{inputs.domain_name}[/light_green]\n" - f"The `Authorization callback URL` is set to: [light_green]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/light_green]\n\n" + f"The `Homepage URL` is set to: [blue1]https://{inputs.domain_name}[/blue1]\n" + f"The `Authorization callback URL` is set to: [blue1]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/blue1]\n\n" ) ) @@ -468,7 +474,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # NAMESPACE inputs.namespace = questionary.text( - "What namespace would like to use?", + "What would you like the main Kubernetes namespace to be called?", default=inputs.namespace, qmark=qmark, ).unsafe_ask() @@ -517,7 +523,7 @@ def if_used(key, model=inputs, ignore_list=["cloud_provider"]): "You can now deploy your Nebari instance with:\n\n" "\t[green]nebari deploy -c nebari-config.yaml[/green]\n\n" "For more information, run [green]nebari deploy --help[/green] or check out the documentation: " - "[light_green]https://www.nebari.dev/how-tos/[/light_green]" + "[blue1]https://www.nebari.dev/how-tos/[/blue1]" ) ) diff --git a/qhub/schema.py b/qhub/schema.py index 9957b1ea0..fef94a694 100644 --- a/qhub/schema.py +++ b/qhub/schema.py @@ -489,9 +489,9 @@ def enabled_must_have_fields(cls, values): def project_name_convention(value: typing.Any, values): convention = """ - In order to successfully deploy QHub, there are some project naming conventions which need - to be followed. First, ensure your name is compatible with the specific one for - your chosen Cloud provider. In addition, the QHub project name should also obey the following + There are some project naming conventions which need to be followed. + First, ensure your name is compatible with the specific one for + your chosen Cloud provider. In addition, the project name should also obey the following format requirements: - Letters from A to Z (upper and lower case) and numbers; - Maximum accepted length of the name string is 16 characters. diff --git a/setup.cfg b/setup.cfg index 2e9f3c04d..5a17722e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,8 @@ install_requires = python-keycloak importlib_metadata;python_version<"3.8" rich + typer + questionary [options.extras_require] dev = From 8a5739c486c03f4fa1be3ce040327e48bd085b66 Mon Sep 17 00:00:00 2001 From: iameskild Date: Mon, 3 Oct 2022 19:29:24 -0700 Subject: [PATCH 37/43] more clean up --- qhub/cli/_init.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index ed1821176..797145a22 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -296,11 +296,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): choices=enum_to_list(ProviderEnum), qmark=qmark, ).unsafe_ask() - # except KeyboardInterrupt: - # print("I'm HERE") - # raise typer.Exit() - # print("WHY AM I HERE?") if not disable_checks: check_cloud_provider_creds(ctx, cloud_provider=inputs.cloud_provider) From 6ba50e60ef0d5c6b2f7719322159b0a7b7a131b1 Mon Sep 17 00:00:00 2001 From: Asmi Jafar Date: Tue, 4 Oct 2022 08:41:11 +0530 Subject: [PATCH 38/43] Add `version` options to Nebari CLI (#1476) * Add version Options to Nebari * Add pre-commit Co-authored-by: eskild <42120229+iameskild@users.noreply.github.com> --- qhub/cli/main.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/qhub/cli/main.py b/qhub/cli/main.py index d98f41297..25a6eb21a 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Optional from zipfile import ZipFile import typer @@ -32,6 +33,7 @@ ) from qhub.upgrade import do_upgrade from qhub.utils import load_yaml +from qhub.version import __version__ SECOND_COMMAND_GROUP_NAME = "Additional Commands" GUIDED_INIT_MSG = ( @@ -66,6 +68,21 @@ def list_commands(self, ctx: Context): ) +@app.callback(invoke_without_command=True) +def version( + version: Optional[bool] = typer.Option( + None, + "-v", + "--version", + help="Nebari version number", + is_eager=True, + ), +): + if version: + print(__version__) + raise typer.Exit() + + @app.command() def init( cloud_provider: str = typer.Argument( From acfe29dae06e3bc51df0b4c32633c1ce5734266b Mon Sep 17 00:00:00 2001 From: asmijafar20 Date: Tue, 4 Oct 2022 21:01:04 +0530 Subject: [PATCH 39/43] Change colour from blue1 to green and spring_green to blue --- qhub/cli/_init.py | 10 +++++----- qhub/render.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 797145a22..ff9de168f 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -15,9 +15,9 @@ ) from qhub.utils import QHUB_DASK_VERSION, QHUB_IMAGE_TAG, yaml -MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, refer to this guide on how to generate them:\n\n[blue1]\t{link_to_docs}[/blue1]\n\n" +MISSING_CREDS_TEMPLATE = "Unable to locate your {provider} credentials, refer to this guide on how to generate them:\n\n[green]\t{link_to_docs}[/green]\n\n" LINKS_TO_DOCS_TEMPLATE = ( - "For more details, refer to the Nebari docs:\n\n\t[blue1]{link_to_docs}[/blue1]\n\n" + "For more details, refer to the Nebari docs:\n\n\t[green]{link_to_docs}[/green]\n\n" ) # links to external docs @@ -370,8 +370,8 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): rich.print( ( ":warning: If you haven't done so already, please ensure the following:\n" - f"The `Homepage URL` is set to: [blue1]https://{inputs.domain_name}[/blue1]\n" - f"The `Authorization callback URL` is set to: [blue1]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/blue1]\n\n" + f"The `Homepage URL` is set to: [green]https://{inputs.domain_name}[/green]\n" + f"The `Authorization callback URL` is set to: [green]https://{inputs.domain_name}/auth/realms/qhub/broker/github/endpoint[/green]\n\n" ) ) @@ -519,7 +519,7 @@ def if_used(key, model=inputs, ignore_list=["cloud_provider"]): "You can now deploy your Nebari instance with:\n\n" "\t[green]nebari deploy -c nebari-config.yaml[/green]\n\n" "For more information, run [green]nebari deploy --help[/green] or check out the documentation: " - "[blue1]https://www.nebari.dev/how-tos/[/blue1]" + "[green]https://www.nebari.dev/how-tos/[/green]" ) ) diff --git a/qhub/render.py b/qhub/render.py index a8c8ec815..609c9bce5 100644 --- a/qhub/render.py +++ b/qhub/render.py @@ -96,17 +96,17 @@ def render_template(output_directory, config_filename, force=False, dry_run=Fals if new: table = Table("The following files will be created:", style="deep_sky_blue1") for filename in sorted(new): - table.add_row(filename, style="spring_green1") + table.add_row(filename, style="green") print(table) if updated: table = Table("The following files will be updated:", style="deep_sky_blue1") for filename in sorted(updated): - table.add_row(filename, style="spring_green1") + table.add_row(filename, style="green") print(table) if deleted: table = Table("The following files will be deleted:", style="deep_sky_blue1") for filename in sorted(deleted): - table.add_row(filename, style="spring_green1") + table.add_row(filename, style="green") print(table) if untracked: table = Table( @@ -114,7 +114,7 @@ def render_template(output_directory, config_filename, force=False, dry_run=Fals style="deep_sky_blue1", ) for filename in sorted(updated): - table.add_row(filename, style="spring_green1") + table.add_row(filename, style="green") print(table) if dry_run: From 3fbf04449a5d34fe491517d2d955f5ad661a67af Mon Sep 17 00:00:00 2001 From: asmijafar20 Date: Wed, 5 Oct 2022 01:45:42 +0530 Subject: [PATCH 40/43] Add suggested changes, Remove extra spaces --- qhub/cli/_init.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index ff9de168f..802e064fa 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -60,6 +60,10 @@ def handle_init(inputs: InitInputs): # this will force the `set_kubernetes_version` to grab the latest version if inputs.kubernetes_version == "latest": inputs.kubernetes_version = None + print( + "The latest available Kubernetes version will be installed if none was provided" + ) + config = render_config( cloud_provider=inputs.cloud_provider, @@ -282,7 +286,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # CLOUD PROVIDER rich.print( ( - "\n\n 🪴 Nebari runs on a Kubernetes cluster: where do you want this Kubernetes cluster deployed? " + "\n 🪴 Nebari runs on a Kubernetes cluster: Where do you want this Kubernetes cluster deployed? " "is where you want this Kubernetes cluster deployed. " f"{LINKS_TO_DOCS_TEMPLATE.format(link_to_docs=CHOOSE_CLOUD_PROVIDER)}" "\n\t❗️ [purple]local[/purple] requires Docker and Kubernetes running on your local machine. " @@ -316,7 +320,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # PROJECT NAME rich.print( ( - f"\n\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n{name_guidelines}\n\n" + f"\n 🪴 Next, give your Nebari instance a project name. This name is what your Kubernetes cluster will be referred to as.\n{name_guidelines}\n" ) ) inputs.project_name = questionary.text( @@ -413,7 +417,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): if git_provider == "github.com": inputs.repository_auto_provision = questionary.confirm( - f"Would you like the following git repository to be automatically created: {inputs.repository}?", + f"Would you like nebari to create a remote repository on {git_provider}?", default=False, qmark=qmark, ).unsafe_ask() @@ -481,6 +485,8 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): qmark=qmark, ).unsafe_ask() + + handle_init(inputs) rich.print( From 54119a988274c0d03e5287ec6537ee5caafbf65c Mon Sep 17 00:00:00 2001 From: asmijafar20 Date: Wed, 5 Oct 2022 01:47:48 +0530 Subject: [PATCH 41/43] Add pre-commit --- qhub/cli/_init.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 802e064fa..4f4c94328 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -63,7 +63,6 @@ def handle_init(inputs: InitInputs): print( "The latest available Kubernetes version will be installed if none was provided" ) - config = render_config( cloud_provider=inputs.cloud_provider, @@ -485,8 +484,6 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): qmark=qmark, ).unsafe_ask() - - handle_init(inputs) rich.print( From 089da20126f28cdd3a54b9f27c4efed09e3d9b75 Mon Sep 17 00:00:00 2001 From: asmijafar20 Date: Fri, 7 Oct 2022 02:41:34 +0530 Subject: [PATCH 42/43] change kubernetes msg --- qhub/cli/_init.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 4f4c94328..5cf5dc4f9 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -60,9 +60,6 @@ def handle_init(inputs: InitInputs): # this will force the `set_kubernetes_version` to grab the latest version if inputs.kubernetes_version == "latest": inputs.kubernetes_version = None - print( - "The latest available Kubernetes version will be installed if none was provided" - ) config = render_config( cloud_provider=inputs.cloud_provider, @@ -480,7 +477,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # KUBERNETES VERSION inputs.kubernetes_version = questionary.text( - "Which Kubernetes version would you like to use?", + "Which Kubernetes version would you like to use (if none provided; latest version will be installed)?", qmark=qmark, ).unsafe_ask() From 11f6f3cd519dfdfde1333997d73435fd814b89d6 Mon Sep 17 00:00:00 2001 From: eskild <42120229+iameskild@users.noreply.github.com> Date: Fri, 7 Oct 2022 10:17:37 -0700 Subject: [PATCH 43/43] Update CI to use new nebari cli entrypoint (#1480) * Update CI to use new nebari cli entrypoint * Add --disable-prompt to nebari init * Update name of keycloak subcommands * Reorder command * Update command * Minor update to test_e2e * Update hostname used for test_deployment * Another minor update to the tests_e2e * Add --disable-prompt to nebari destroy * use --disable-prompt in ci test * Update .github/workflows/kubernetes_test.yaml Co-authored-by: Vinicius D. Cerutti <51954708+viniciusdc@users.noreply.github.com> * Update kubernetes_test.yaml * Update .github/workflows/kubernetes_test.yaml Co-authored-by: Tania Allard * Update qhub/cli/main.py Co-authored-by: Tania Allard * Fix yaml * Fix yaml again Co-authored-by: Vinicius D. Cerutti <51954708+viniciusdc@users.noreply.github.com> Co-authored-by: Tania Allard --- .github/workflows/kubernetes_test.yaml | 42 ++++++++++++------------- .github/workflows/test-provider.yaml | 24 +++++++------- qhub/cli/_init.py | 17 ++++++++-- qhub/cli/_keycloak.py | 4 +-- qhub/cli/main.py | 18 +++++++++-- qhub/schema.py | 3 ++ tests_deployment/constants.py | 2 +- tests_deployment/test_dask_gateway.py | 4 +-- tests_deployment/test_jupyterhub_ssh.py | 2 +- tests_deployment/utils.py | 8 +++-- tests_e2e/cypress/integration/main.js | 2 +- tests_e2e/cypress/plugins/index.js | 4 +-- 12 files changed, 80 insertions(+), 50 deletions(-) diff --git a/.github/workflows/kubernetes_test.yaml b/.github/workflows/kubernetes_test.yaml index 56cf6bdbf..dbbcaf732 100644 --- a/.github/workflows/kubernetes_test.yaml +++ b/.github/workflows/kubernetes_test.yaml @@ -47,11 +47,11 @@ jobs: with: python-version: 3.8 miniconda-version: "latest" - - name: Install QHub + - name: Install Nebari run: | conda install -c anaconda pip pip install .[dev] - - name: Download and Install Kind and Kubectl + - name: Download and Install Kubectl run: | mkdir -p bin pushd bin @@ -73,32 +73,32 @@ jobs: ip route - name: Add DNS entry to hosts run: | - sudo echo "172.18.1.100 github-actions.qhub.dev" | sudo tee -a /etc/hosts - - name: Initialize QHub Cloud + sudo echo "172.18.1.100 github-actions.nebari.dev" | sudo tee -a /etc/hosts + - name: Initialize Nebari Cloud run: | mkdir -p local-deployment cd local-deployment - qhub init local --project=thisisatest --domain github-actions.qhub.dev --auth-provider=password + nebari init local --project=thisisatest --domain github-actions.nebari.dev --auth-provider=password # Need smaller profiles on Local Kind - sed -i -E 's/(cpu_guarantee):\s+[0-9\.]+/\1: 0.25/g' "qhub-config.yaml" - sed -i -E 's/(mem_guarantee):\s+[A-Za-z0-9\.]+/\1: 0.25G/g' "qhub-config.yaml" + sed -i -E 's/(cpu_guarantee):\s+[0-9\.]+/\1: 0.25/g' "nebari-config.yaml" + sed -i -E 's/(mem_guarantee):\s+[A-Za-z0-9\.]+/\1: 0.25G/g' "nebari-config.yaml" - cat qhub-config.yaml - - name: Deploy QHub Cloud + cat nebari-config.yaml + - name: Deploy Nebari run: | cd local-deployment - qhub deploy --config qhub-config.yaml --disable-prompt + nebari deploy --config nebari-config.yaml --disable-prompt - name: Basic kubectl checks after deployment if: always() run: | kubectl get all,cm,secret,ing -A - - name: Check github-actions.qhub.dev resolves + - name: Check github-actions.nebari.dev resolves run: | - nslookup github-actions.qhub.dev + nslookup github-actions.nebari.dev - name: Curl jupyterhub login page run: | - curl -k https://github-actions.qhub.dev/hub/home -i + curl -k https://github-actions.nebari.dev/hub/home -i ### CYPRESS TESTS - name: Setup Node @@ -113,8 +113,8 @@ jobs: sudo apt-get -y update sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb - - name: Get qhub-config.yaml full path - run: echo "QHUB_CONFIG_PATH=`realpath ./local-deployment/qhub-config.yaml`" >> "$GITHUB_ENV" + - name: Get nebari-config.yaml full path + run: echo "NEBARI_CONFIG_PATH=`realpath ./local-deployment/nebari-config.yaml`" >> "$GITHUB_ENV" - name: Create example-user run: | @@ -124,13 +124,13 @@ jobs: echo "CYPRESS_EXAMPLE_USER_NAME=${CYPRESS_EXAMPLE_USER_NAME}" >> $GITHUB_ENV echo "CYPRESS_EXAMPLE_USER_PASSWORD=${CYPRESS_EXAMPLE_USER_PASSWORD}" >> $GITHUB_ENV - qhub keycloak --config "${QHUB_CONFIG_PATH}" adduser "${CYPRESS_EXAMPLE_USER_NAME}" "${CYPRESS_EXAMPLE_USER_PASSWORD}" - qhub keycloak --config "${QHUB_CONFIG_PATH}" listusers + nebari keycloak adduser --user "${CYPRESS_EXAMPLE_USER_NAME}" "${CYPRESS_EXAMPLE_USER_PASSWORD}" --config "${NEBARI_CONFIG_PATH}" + nebari keycloak listusers --config "${NEBARI_CONFIG_PATH}" - name: Cypress run uses: cypress-io/github-action@v2 env: - CYPRESS_BASE_URL: https://github-actions.qhub.dev/ + CYPRESS_BASE_URL: https://github-actions.nebari.dev/ with: working-directory: tests_e2e @@ -155,7 +155,7 @@ jobs: run: | export JUPYTERHUB_USERNAME=${CYPRESS_EXAMPLE_USER_NAME} export JUPYTERHUB_PASSWORD=${CYPRESS_EXAMPLE_USER_PASSWORD} - jhubctl --verbose run --hub=https://github-actions.qhub.dev \ + jhubctl --verbose run --hub=https://github-actions.nebari.dev \ --auth-type=keycloak \ --validate --no-verify-ssl \ --kernel python3 \ @@ -163,7 +163,7 @@ jobs: --notebook tests_deployment/assets/notebook/simple.ipynb \ ### CLEANUP AFTER TESTS - - name: Cleanup qhub deployment + - name: Cleanup nebari deployment run: | cd local-deployment - qhub destroy --config qhub-config.yaml + nebari destroy --config nebari-config.yaml --disable-prompt diff --git a/.github/workflows/test-provider.yaml b/.github/workflows/test-provider.yaml index 26838773b..056fd675f 100644 --- a/.github/workflows/test-provider.yaml +++ b/.github/workflows/test-provider.yaml @@ -1,4 +1,4 @@ -name: "Test QHub Provider" +name: "Test Nebari Provider" on: pull_request: @@ -36,7 +36,7 @@ env: jobs: test-render-providers: - name: 'Test QHub Provider' + name: 'Test Nebari Provider' runs-on: ubuntu-latest strategy: matrix: @@ -75,19 +75,19 @@ jobs: - name: Use az CLI if: ${{ matrix.provider == 'azure' }} run: az version - - name: Install QHub + - name: Install Nebari run: | pip install .[dev] - - name: QHub Initialize + - name: Nebari Initialize run: | - qhub init "${{ matrix.provider }}" --project "TestProvider" --domain "${{ matrix.provider }}.qhub.dev" --auth-provider github --disable-prompt --ci-provider ${{ matrix.cicd }} - cat "qhub-config.yaml" - - name: QHub Render + nebari init "${{ matrix.provider }}" --project "TestProvider" --domain "${{ matrix.provider }}.nebari.dev" --auth-provider github --disable-prompt --ci-provider ${{ matrix.cicd }} + cat "nebari-config.yaml" + - name: Nebari Render run: | - qhub render -c "qhub-config.yaml" -o "qhub-${{ matrix.provider }}-${{ matrix.cicd }}-deployment" - cp "qhub-config.yaml" "qhub-${{ matrix.provider }}-${{ matrix.cicd }}-deployment/qhub-config.yaml" - - name: QHub Render Artifact + nebari render -c "nebari-config.yaml" -o "nebari-${{ matrix.provider }}-${{ matrix.cicd }}-deployment" + cp "nebari-config.yaml" "nebari-${{ matrix.provider }}-${{ matrix.cicd }}-deployment/nebari-config.yaml" + - name: Nebari Render Artifact uses: actions/upload-artifact@master with: - name: "qhub-${{ matrix.provider }}-${{ matrix.cicd }}-artifact" - path: "qhub-${{ matrix.provider }}-${{ matrix.cicd }}-deployment" + name: "nebari-${{ matrix.provider }}-${{ matrix.cicd }}-artifact" + path: "nebari-${{ matrix.provider }}-${{ matrix.cicd }}-deployment" diff --git a/qhub/cli/_init.py b/qhub/cli/_init.py index 5cf5dc4f9..38b3e5b73 100644 --- a/qhub/cli/_init.py +++ b/qhub/cli/_init.py @@ -74,11 +74,16 @@ def handle_init(inputs: InitInputs): kubernetes_version=inputs.kubernetes_version, terraform_state=inputs.terraform_state, ssl_cert_email=inputs.ssl_cert_email, - disable_prompt=False, # keep? + disable_prompt=inputs.disable_prompt, ) + # TODO remove when Typer CLI is out of BETA + whoami = "qhub" + if inputs.nebari: + whoami = "nebari" + try: - with open("qhub-config.yaml", "x") as f: + with open(f"{whoami}-config.yaml", "x") as f: yaml.dump(config, f) except FileExistsError: raise ValueError( @@ -89,6 +94,9 @@ def handle_init(inputs: InitInputs): def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): """Validate that the necessary cloud credentials have been set as environment variables.""" + if ctx.params.get("disable_prompt"): + return cloud_provider + cloud_provider = cloud_provider.lower() # AWS @@ -191,6 +199,9 @@ def check_cloud_provider_creds(ctx: typer.Context, cloud_provider: str): def check_auth_provider_creds(ctx: typer.Context, auth_provider: str): """Validating the the necessary auth provider credentials have been set as environment variables.""" + if ctx.params.get("disable_prompt"): + return auth_provider + auth_provider = auth_provider.lower() # Auth0 @@ -278,6 +289,8 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # pull in default values for each of the below inputs = InitInputs() + # TODO remove when Typer CLI is out of BETA + inputs.nebari = True # CLOUD PROVIDER rich.print( diff --git a/qhub/cli/_keycloak.py b/qhub/cli/_keycloak.py index 3d8870b21..8c1784014 100644 --- a/qhub/cli/_keycloak.py +++ b/qhub/cli/_keycloak.py @@ -13,7 +13,7 @@ ) -@app_keycloak.command() +@app_keycloak.command(name="adduser") def add_user( add_users: Tuple[str, str] = typer.Option( ..., "--user", help="Provide both: " @@ -34,7 +34,7 @@ def add_user( do_keycloak(config_filename, *args) -@app_keycloak.command() +@app_keycloak.command(name="listusers") def list_users( config_filename: str = typer.Option( ..., diff --git a/qhub/cli/main.py b/qhub/cli/main.py index 25a6eb21a..4232f5ed1 100644 --- a/qhub/cli/main.py +++ b/qhub/cli/main.py @@ -140,6 +140,10 @@ def init( ssl_cert_email: str = typer.Option( None, ), + disable_prompt: bool = typer.Option( + False, + is_eager=True, + ), ): """ Create and initialize your [purple]nebari-config.yaml[/purple] file. @@ -169,6 +173,9 @@ def init( inputs.terraform_state = terraform_state inputs.kubernetes_version = kubernetes_version inputs.ssl_cert_email = ssl_cert_email + inputs.disable_prompt = disable_prompt + # TODO remove when Typer CLI is out of BETA + inputs.nebari = True handle_init(inputs) @@ -319,13 +326,18 @@ def destroy( "--disable-render", help="Disable auto-rendering before destroy", ), + disable_prompt: bool = typer.Option( + False, + "--disable-prompt", + help="Destroy entire Nebari cluster without confirmation request. Suggested for CI use.", + ), ): """ Destroy the Nebari cluster from your [purple]nebari-config.yaml[/purple] file. """ - delete = typer.confirm("Are you sure you want to destroy it?") - if not delete: - raise typer.Abort() + if not disable_prompt: + if typer.confirm("Are you sure you want to destroy your Nebari cluster?"): + raise typer.Abort() else: config_filename = Path(config) if not config_filename.is_file(): diff --git a/qhub/schema.py b/qhub/schema.py index fef94a694..37884c5f0 100644 --- a/qhub/schema.py +++ b/qhub/schema.py @@ -540,6 +540,9 @@ class InitInputs(Base): terraform_state: typing.Optional[TerraformStateEnum] = None kubernetes_version: typing.Union[str, None] = None ssl_cert_email: typing.Union[str, None] = None + disable_prompt: bool = False + # TODO remove when Typer CLI is out of BETA + nebari: bool = False class Main(Base): diff --git a/tests_deployment/constants.py b/tests_deployment/constants.py index c9b9a6276..cfeb6c26e 100644 --- a/tests_deployment/constants.py +++ b/tests_deployment/constants.py @@ -1,6 +1,6 @@ import os -QHUB_HOSTNAME = os.environ.get("QHUB_HOSTNAME", "github-actions.qhub.dev") +NEBARI_HOSTNAME = os.environ.get("NEBARI_HOSTNAME", "github-actions.nebari.dev") GATEWAY_ENDPOINT = "gateway" KEYCLOAK_USERNAME = os.environ["KEYCLOAK_USERNAME"] diff --git a/tests_deployment/test_dask_gateway.py b/tests_deployment/test_dask_gateway.py index b1d4b0870..73a291b92 100644 --- a/tests_deployment/test_dask_gateway.py +++ b/tests_deployment/test_dask_gateway.py @@ -16,9 +16,9 @@ def dask_gateway_object(): "dask-gateway-pytest-token" ) return dask_gateway.Gateway( - address=f"https://{constants.QHUB_HOSTNAME}/{constants.GATEWAY_ENDPOINT}", + address=f"https://{constants.NEBARI_HOSTNAME}/{constants.GATEWAY_ENDPOINT}", auth="jupyterhub", - proxy_address=f"tcp://{constants.QHUB_HOSTNAME}:8786", + proxy_address=f"tcp://{constants.NEBARI_HOSTNAME}:8786", ) diff --git a/tests_deployment/test_jupyterhub_ssh.py b/tests_deployment/test_jupyterhub_ssh.py index 7bd9a19e4..ff47b77d5 100644 --- a/tests_deployment/test_jupyterhub_ssh.py +++ b/tests_deployment/test_jupyterhub_ssh.py @@ -25,7 +25,7 @@ def paramiko_object(): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy) client.connect( - hostname=constants.QHUB_HOSTNAME, + hostname=constants.NEBARI_HOSTNAME, port=8022, username=constants.KEYCLOAK_USERNAME, password=api_token, diff --git a/tests_deployment/utils.py b/tests_deployment/utils.py index 0b7819b4d..dd28ff799 100644 --- a/tests_deployment/utils.py +++ b/tests_deployment/utils.py @@ -10,7 +10,9 @@ def get_jupyterhub_session(): session = requests.Session() - r = session.get(f"https://{constants.QHUB_HOSTNAME}/hub/oauth_login", verify=False) + r = session.get( + f"https://{constants.NEBARI_HOSTNAME}/hub/oauth_login", verify=False + ) auth_url = re.search('action="([^"]+)"', r.content.decode("utf8")).group(1) r = session.post( @@ -29,9 +31,9 @@ def get_jupyterhub_session(): def get_jupyterhub_token(note="jupyterhub-tests-deployment"): session = get_jupyterhub_session() r = session.post( - f"https://{constants.QHUB_HOSTNAME}/hub/api/users/{constants.KEYCLOAK_USERNAME}/tokens", + f"https://{constants.NEBARI_HOSTNAME}/hub/api/users/{constants.KEYCLOAK_USERNAME}/tokens", headers={ - "Referer": f"https://{constants.QHUB_HOSTNAME}/hub/token", + "Referer": f"https://{constants.NEBARI_HOSTNAME}/hub/token", }, json={ "note": note, diff --git a/tests_e2e/cypress/integration/main.js b/tests_e2e/cypress/integration/main.js index 9927a9127..be5f469d0 100644 --- a/tests_e2e/cypress/integration/main.js +++ b/tests_e2e/cypress/integration/main.js @@ -1,6 +1,6 @@ const { divide } = require("lodash"); -const security_authentication_type = Cypress.env('qhub_security_authentication_type'); +const security_authentication_type = Cypress.env('nebari_security_authentication_type'); const EXAMPLE_USER_NAME = Cypress.env('EXAMPLE_USER_NAME') || 'example-user'; diff --git a/tests_e2e/cypress/plugins/index.js b/tests_e2e/cypress/plugins/index.js index 12c9cd221..826dd50a4 100644 --- a/tests_e2e/cypress/plugins/index.js +++ b/tests_e2e/cypress/plugins/index.js @@ -14,14 +14,14 @@ module.exports = (on, config) => { try { - let fileContents = fs.readFileSync(process.env.QHUB_CONFIG_PATH, 'utf8'); + let fileContents = fs.readFileSync(process.env.NEBARI_CONFIG_PATH, 'utf8'); let data = yaml.load(fileContents); console.log(data); new_config['env'] = _.fromPairs( _.map(yaml_fields, - field => ['qhub_'+field.replace(/\./g, '_') , _.get(data, field, '')] + field => ['nebari_'+field.replace(/\./g, '_') , _.get(data, field, '')] ) );