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/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/cli/_init.py b/qhub/cli/_init.py new file mode 100644 index 000000000..38b3e5b73 --- /dev/null +++ b/qhub/cli/_init.py @@ -0,0 +1,543 @@ +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, 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[green]{link_to_docs}[/green]\n\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" + +# links to Nebari docs +DOCS_HOME = "https://nebari.dev" +CHOOSE_CLOUD_PROVIDER = "https://nebari.dev/getting-started/deploy" + + +def enum_to_list(enum_cls): + return [e.value.lower() for e in 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}" + ) + + if QHUB_DASK_VERSION: + print( + 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 + if inputs.kubernetes_version == "latest": + inputs.kubernetes_version = None + + 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=inputs.disable_prompt, + ) + + # TODO remove when Typer CLI is out of BETA + whoami = "qhub" + if inputs.nebari: + whoami = "nebari" + + try: + with open(f"{whoami}-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." + ) + + +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 + 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") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Amazon Web Services", link_to_docs=CREATE_AWS_CREDS + ) + ) + + os.environ["AWS_ACCESS_KEY_ID"] = typer.prompt( + "Paste your AWS_ACCESS_KEY_ID", + hide_input=True, + ) + os.environ["AWS_SECRET_ACCESS_KEY"] = typer.prompt( + "Paste your AWS_SECRET_ACCESS_KEY", + hide_input=True, + ) + + # GCP + elif cloud_provider == ProviderEnum.gcp.value.lower() and ( + not os.environ.get("GOOGLE_CREDENTIALS") or not os.environ.get("PROJECT_ID") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Google Cloud Provider", link_to_docs=CREATE_GCP_CREDS + ) + ) + + os.environ["GOOGLE_CREDENTIALS"] = typer.prompt( + "Paste your GOOGLE_CREDENTIALS", + hide_input=True, + ) + os.environ["PROJECT_ID"] = typer.prompt( + "Paste your PROJECT_ID", + hide_input=True, + ) + + # 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") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Digital Ocean", link_to_docs=CREATE_DO_CREDS + ) + ) + + os.environ["DIGITALOCEAN_TOKEN"] = typer.prompt( + "Paste your DIGITALOCEAN_TOKEN", + hide_input=True, + ) + os.environ["SPACES_ACCESS_KEY_ID"] = typer.prompt( + "Paste your SPACES_ACCESS_KEY_ID", + hide_input=True, + ) + os.environ["SPACES_SECRET_ACCESS_KEY"] = typer.prompt( + "Paste your SPACES_SECRET_ACCESS_KEY", + hide_input=True, + ) + + # 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") + ): + rich.print( + MISSING_CREDS_TEMPLATE.format( + provider="Azure", link_to_docs=CREATE_AZURE_CREDS + ) + ) + + os.environ["ARM_CLIENT_ID"] = typer.prompt( + "Paste your ARM_CLIENT_ID", + hide_input=True, + ) + os.environ["ARM_CLIENT_SECRET"] = typer.prompt( + "Paste your ARM_CLIENT_SECRET", + hide_input=True, + ) + os.environ["ARM_SUBSCRIPTION_ID"] = typer.prompt( + "Paste your ARM_SUBSCRIPTION_ID", + hide_input=True, + ) + os.environ["ARM_TENANT_ID"] = typer.prompt( + "Paste your ARM_TENANT_ID", + hide_input=True, + ) + + 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.""" + + if ctx.params.get("disable_prompt"): + return auth_provider + + 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 + ) + ) + + os.environ["AUTH0_CLIENT_ID"] = typer.prompt( + "Paste your AUTH0_CLIENT_ID", + hide_input=True, + ) + os.environ["AUTH0_CLIENT_SECRET"] = typer.prompt( + "Paste your AUTH0_CLIENT_SECRET", + hide_input=True, + ) + os.environ["AUTH0_DOMAIN"] = typer.prompt( + "Paste 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 + ) + ) + + os.environ["GITHUB_CLIENT_ID"] = typer.prompt( + "Paste your GITHUB_CLIENT_ID", + hide_input=True, + ) + os.environ["GITHUB_CLIENT_SECRET"] = typer.prompt( + "Paste your GITHUB_CLIENT_SECRET", + hide_input=True, + ) + + return auth_provider + + +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"]} + ) + + 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 + + try: + rich.print( + ( + "\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)}" + ) + ) + + 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() + # TODO remove when Typer CLI is out of BETA + inputs.nebari = True + + # CLOUD PROVIDER + rich.print( + ( + "\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. " + "[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" + ) + ) + # try: + inputs.cloud_provider = questionary.select( + "Where would you like to deploy your Nebari cluster?", + choices=enum_to_list(ProviderEnum), + qmark=qmark, + ).unsafe_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" + + # 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" + ) + ) + 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) + + # DOMAIN NAME + rich.print( + ( + "\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" + ) + ) + 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", + ).unsafe_ask() + + # AUTH PROVIDER + rich.print( + ( + # 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" + ) + ) + inputs.auth_provider = questionary.select( + "What authentication provider would you like?", + choices=enum_to_list(AuthenticationEnum), + qmark=qmark, + ).unsafe_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, + ).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: [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" + ) + ) + + # GITOPS - REPOSITORY, CICD + rich.print( + ( + "\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" + ) + ) + if questionary.confirm( + "Would you like to adopt a GitOps approach to managing Nebari?", + default=False, + qmark=qmark, + ).unsafe_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, + ).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, + ).unsafe_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, + ).unsafe_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 nebari to create a remote repository on {git_provider}?", + default=False, + qmark=qmark, + ).unsafe_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\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, + ).unsafe_ask() + + 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\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, + ).unsafe_ask(): + + # TERRAFORM STATE + inputs.terraform_state = questionary.select( + "Where should the Terraform State be provisioned?", + choices=enum_to_list(TerraformStateEnum), + qmark=qmark, + ).unsafe_ask() + + # NAMESPACE + inputs.namespace = questionary.text( + "What would you like the main Kubernetes namespace to be called?", + default=inputs.namespace, + qmark=qmark, + ).unsafe_ask() + + # KUBERNETES VERSION + inputs.kubernetes_version = questionary.text( + "Which Kubernetes version would you like to use (if none provided; latest version will be installed)?", + qmark=qmark, + ).unsafe_ask() + + handle_init(inputs) + + 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}" + + 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( + ( + "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: " + "[green]https://www.nebari.dev/how-tos/[/green]" + ) + ) + + except KeyboardInterrupt: + rich.print("\nUser quit the Guided Init.\n\n ") + raise typer.Exit() + + raise typer.Exit() diff --git a/qhub/cli/_keycloak.py b/qhub/cli/_keycloak.py new file mode 100644 index 000000000..8c1784014 --- /dev/null +++ b/qhub/cli/_keycloak.py @@ -0,0 +1,52 @@ +from pathlib import Path +from typing import Tuple + +import typer + +from qhub.keycloak import do_keycloak + +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(name="adduser") +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(name="listusers") +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 new file mode 100644 index 000000000..4232f5ed1 --- /dev/null +++ b/qhub/cli/main.py @@ -0,0 +1,521 @@ +from pathlib import Path +from typing import Optional +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 ( + check_auth_provider_creds, + check_cloud_provider_creds, + check_project_name, + enum_to_list, + 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 +from qhub.schema import ( + AuthenticationEnum, + CiEnum, + InitInputs, + ProviderEnum, + TerraformStateEnum, + verify, +) +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 = ( + "[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." +) +KEYCLOAK_COMMAND_MSG = ( + "Interact with the Nebari Keycloak identity and access management tool." +) + + +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, + rich_markup_mode="rich", + context_settings={"help_option_names": ["-h", "--help"]}, +) +app.add_typer( + app_keycloak, + name="keycloak", + help=KEYCLOAK_COMMAND_MSG, + rich_help_panel=SECOND_COMMAND_GROUP_NAME, +) + + +@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( + "local", + help=f"options: {enum_to_list(ProviderEnum)}", + callback=check_cloud_provider_creds, + is_eager=True, + ), + guided_init: bool = typer.Option( + False, + help=GUIDED_INIT_MSG, + callback=guided_init_wizard, + is_eager=True, + ), + project_name: str = typer.Option( + ..., + "--project-name", + "--project", + "-p", + callback=check_project_name, + ), + domain_name: str = typer.Option( + ..., + "--domain-name", + "--domain", + "-d", + ), + namespace: str = typer.Option( + "dev", + ), + auth_provider: str = typer.Option( + "password", + help=f"options: {enum_to_list(AuthenticationEnum)}", + callback=check_auth_provider_creds, + ), + auth_auto_provision: bool = typer.Option( + False, + ), + repository: str = typer.Option( + None, + ), + repository_auto_provision: bool = typer.Option( + False, + ), + ci_provider: str = typer.Option( + None, + help=f"options: {enum_to_list(CiEnum)}", + ), + terraform_state: str = typer.Option( + "remote", help=f"options: {enum_to_list(TerraformStateEnum)}" + ), + kubernetes_version: str = typer.Option( + "latest", + ), + 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. + + 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] + + """ + 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 + inputs.disable_prompt = disable_prompt + # TODO remove when Typer CLI is out of BETA + inputs.nebari = True + + handle_init(inputs) + + +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) +def validate( + config: str = typer.Option( + ..., + "--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 values in the [purple]nebari-config.yaml[/purple] file are acceptable. + """ + config_filename = 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("[bold purple]Successfully validated configuration.[/bold purple]") + + +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) +def render( + output: str = typer.Option( + "./", + "-o", + "--output", + help="output directory", + ), + config: str = typer.Option( + ..., + "-c", + "--config", + 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", + ), +): + """ + Dynamically render the Terraform scripts and other files 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" + ) + + config_yaml = load_yaml(config_filename) + + verify(config_yaml) + + render_template(output, config, force=True, dry_run=dry_run) + + +@app.command() +def deploy( + config: str = typer.Option( + ..., + "--config", + "-c", + help="nebari configuration yaml file path", + ), + output: str = typer.Option( + "./", + "-o", + "--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", + help="Disable human intervention", + ), + disable_render: bool = typer.Option( + False, + "--disable-render", + help="Disable auto-rendering in deploy stage", + ), +): + """ + 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" + ) + + 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=dns_provider, + dns_auto_provision=dns_auto_provision, + disable_prompt=disable_prompt, + disable_checks=False, + skip_remote_state_provision=False, + ) + + +@app.command() +def destroy( + config: str = typer.Option( + ..., "-c", "--config", help="qhub configuration file path" + ), + output: str = typer.Option( + "./" "-o", + "--output", + help="output directory", + ), + disable_render: bool = typer.Option( + False, + "--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. + """ + 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(): + 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) + + +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) +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", + ), +): + """ + 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, + dashboard=True, + file=file, + currency_code=currency, + compare=False, + ) + + +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) +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 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(): + raise ValueError( + f"passed in configuration filename={config_filename} must exist" + ) + + do_upgrade(config_filename, attempt_fixes=attempt_fixes) + + +@app.command(rich_help_panel=SECOND_COMMAND_GROUP_NAME) +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 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() + + 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/initialize.py b/qhub/initialize.py index c19cc869d..c94b14aff 100644 --- a/qhub/initialize.py +++ b/qhub/initialize.py @@ -337,10 +337,6 @@ 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" @@ -348,14 +344,6 @@ def render_config( 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: " 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/render.py b/qhub/render.py index 01f8d66bf..609c9bce5 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 @@ -92,21 +94,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="green") + 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="green") + 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="green") + 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="green") + print(table) if dry_run: print("dry-run enabled no files will be created, updated, or deleted") diff --git a/qhub/schema.py b/qhub/schema.py index 4b4c3aa23..37884c5f0 100644 --- a/qhub/schema.py +++ b/qhub/schema.py @@ -487,6 +487,64 @@ 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 = """ + 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. + - 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 + + +# 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 + disable_prompt: bool = False + # TODO remove when Typer CLI is out of BETA + nebari: bool = False + + class Main(Base): provider: ProviderEnum project_name: str @@ -545,43 +603,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): 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: diff --git a/setup.cfg b/setup.cfg index 64c312c53..5a17722e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,11 +38,14 @@ install_requires = python-keycloak importlib_metadata;python_version<"3.8" rich + typer + questionary [options.extras_require] dev = flake8==3.8.4 black==22.3.0 + importlib-metadata<5.0 twine pytest pytest-timeout @@ -57,6 +60,7 @@ dev = [options.entry_points] console_scripts = qhub = qhub.__main__:main + nebari = qhub.cli.main:app [tool:pytest] norecursedirs = _build .nox .ipynb_checkpoints 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, '')] ) );