diff --git a/.github/workflows/test-provider.yaml b/.github/workflows/test-provider.yaml index 339b6a0b58..6382cdf12a 100644 --- a/.github/workflows/test-provider.yaml +++ b/.github/workflows/test-provider.yaml @@ -75,7 +75,7 @@ jobs: python-version: "3.11" - name: Retrieve secret from Vault - uses: hashicorp/vault-action@v2.5.0 + uses: hashicorp/vault-action@v3.0.0 with: method: jwt url: "https://quansight-vault-public-vault-b2379fa7.d415e30e.z1.hashicorp.cloud:8200" diff --git a/.github/workflows/test_aws_integration.yaml b/.github/workflows/test_aws_integration.yaml index 36112ccd50..3c8a0d04af 100644 --- a/.github/workflows/test_aws_integration.yaml +++ b/.github/workflows/test_aws_integration.yaml @@ -1,4 +1,4 @@ -name: test-aws-integration +name: AWS Deployment on: schedule: @@ -32,12 +32,12 @@ env: AWS_DEFAULT_REGION: "us-west-2" NEBARI_GH_BRANCH: ${{ github.event.inputs.branch || 'develop' }} NEBARI_IMAGE_TAG: ${{ github.event.inputs.image-tag || 'main' }} - TF_LOG: ${{ github.event.inputs.tf-log-level || 'info' }} - + TF_LOG: ${{ github.event.inputs.tf-log-level || 'info' }}∏ jobs: test-aws-integration: runs-on: ubuntu-latest + if: ${{ vars.SKIP_AWS_INTEGRATION_TEST != 'true' }} permissions: id-token: write contents: read @@ -59,7 +59,7 @@ jobs: playwright install - name: Retrieve secret from Vault - uses: hashicorp/vault-action@v2.5.0 + uses: hashicorp/vault-action@v3.0.0 with: method: jwt url: "https://quansight-vault-public-vault-b2379fa7.d415e30e.z1.hashicorp.cloud:8200" diff --git a/.github/workflows/test_azure_integration.yaml b/.github/workflows/test_azure_integration.yaml new file mode 100644 index 0000000000..aae0d85fb3 --- /dev/null +++ b/.github/workflows/test_azure_integration.yaml @@ -0,0 +1,89 @@ +name: Azure Deployment + +on: + schedule: + - cron: "0 0 * * MON" + workflow_dispatch: + inputs: + branch: + description: 'Nebari branch to deploy, test, destroy' + required: true + default: develop + type: string + image-tag: + description: 'Nebari image tag created by the nebari-docker-images repo' + required: true + default: main + type: string + tf-log-level: + description: 'Change Terraform log levels' + required: false + default: info + type: choice + options: + - info + - warn + - debug + - trace + - error + +env: + NEBARI_GH_BRANCH: ${{ github.event.inputs.branch || 'develop' }} + NEBARI_IMAGE_TAG: ${{ github.event.inputs.image-tag || 'main' }} + TF_LOG: ${{ github.event.inputs.tf-log-level || 'info' }} + +jobs: + test-azure-integration: + runs-on: ubuntu-latest + if: ${{ vars.SKIP_AZURE_INTEGRATION_TEST != 'true' }} + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ env.NEBARI_GH_BRANCH }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install Nebari + run: | + pip install .[dev] + conda install --quiet --yes conda-build + playwright install + + - name: Retrieve secret from Vault + uses: hashicorp/vault-action@v3.0.0 + with: + method: jwt + url: "https://quansight-vault-public-vault-b2379fa7.d415e30e.z1.hashicorp.cloud:8200" + namespace: "admin/quansight" + role: "repository-nebari-dev-nebari-role" + secrets: | + kv/data/repository/nebari-dev/nebari/azure/nebari-dev-ci/github-nebari-dev-repo-ci client_id | ARM_CLIENT_ID; + kv/data/repository/nebari-dev/nebari/azure/nebari-dev-ci/github-nebari-dev-repo-ci tenant_id | ARM_TENANT_ID; + kv/data/repository/nebari-dev/nebari/azure/nebari-dev-ci/github-nebari-dev-repo-ci subscription_id | ARM_SUBSCRIPTION_ID; + kv/data/repository/nebari-dev/nebari/cloudflare/internal-devops@quansight.com/nebari-dev-ci token | CLOUDFLARE_TOKEN; + + - name: 'Azure login' + uses: azure/login@v2 + with: + client-id: ${{ env.ARM_CLIENT_ID }} + tenant-id: ${{ env.ARM_TENANT_ID }} + subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }} + + - name: Integration Tests + run: | + pytest --version + pytest tests/tests_integration/ -vvv -s --cloud azure + env: + NEBARI_SECRET__default_images__jupyterhub: "quay.io/nebari/nebari-jupyterhub:${{ env.NEBARI_IMAGE_TAG }}" + NEBARI_SECRET__default_images__jupyterlab: "quay.io/nebari/nebari-jupyterlab:${{ env.NEBARI_IMAGE_TAG }}" + NEBARI_SECRET__default_images__dask_worker: "quay.io/nebari/nebari-dask-worker:${{ env.NEBARI_IMAGE_TAG }}" + ARM_CLIENT_ID: ${{ env.ARM_CLIENT_ID }} + ARM_TENANT_ID: ${{ env.ARM_TENANT_ID }} diff --git a/.github/workflows/test_do_integration.yaml b/.github/workflows/test_do_integration.yaml index dcfacf3175..d9ef1a7b45 100644 --- a/.github/workflows/test_do_integration.yaml +++ b/.github/workflows/test_do_integration.yaml @@ -1,4 +1,4 @@ -name: test-gcp-integration +name: Digital Ocean Deployment on: schedule: @@ -36,6 +36,7 @@ env: jobs: test-do-integration: runs-on: ubuntu-latest + if: ${{ vars.SKIP_DO_INTEGRATION_TEST != 'true' }} permissions: id-token: write contents: read @@ -57,7 +58,7 @@ jobs: playwright install - name: Retrieve secret from Vault - uses: hashicorp/vault-action@v2.5.0 + uses: hashicorp/vault-action@v3.0.0 with: method: jwt url: "https://quansight-vault-public-vault-b2379fa7.d415e30e.z1.hashicorp.cloud:8200" diff --git a/.github/workflows/test_gcp_integration.yaml b/.github/workflows/test_gcp_integration.yaml index 0418e0af40..71bb6c0791 100644 --- a/.github/workflows/test_gcp_integration.yaml +++ b/.github/workflows/test_gcp_integration.yaml @@ -1,4 +1,4 @@ -name: test-gcp-integration +name: GCP Deployment on: schedule: @@ -32,10 +32,10 @@ env: NEBARI_IMAGE_TAG: ${{ github.event.inputs.image-tag || 'main' }} TF_LOG: ${{ github.event.inputs.tf-log-level || 'info' }} - jobs: test-gcp-integration: runs-on: ubuntu-latest + if: ${{ vars.SKIP_GCP_INTEGRATION_TEST != 'true' }} permissions: id-token: write contents: read @@ -58,7 +58,7 @@ jobs: playwright install - name: Retrieve secret from Vault - uses: hashicorp/vault-action@v2.5.0 + uses: hashicorp/vault-action@v3.0.0 with: method: jwt url: "https://quansight-vault-public-vault-b2379fa7.d415e30e.z1.hashicorp.cloud:8200" diff --git a/.github/workflows/test_helm_charts.yaml b/.github/workflows/test_helm_charts.yaml index 1d86eb92e4..3f0ed8834e 100644 --- a/.github/workflows/test_helm_charts.yaml +++ b/.github/workflows/test_helm_charts.yaml @@ -1,5 +1,5 @@ # Right now the trigger is set to run on every Monday at 13:00 UTC, -# or when the workflow file is modified. An aditional manual trigger +# or when the workflow file is modified. An additional manual trigger # is also available. name: "Validate Helm Charts downloads" diff --git a/.github/workflows/test_local_integration.yaml b/.github/workflows/test_local_integration.yaml index 81810abfe1..6d605787a6 100644 --- a/.github/workflows/test_local_integration.yaml +++ b/.github/workflows/test_local_integration.yaml @@ -33,6 +33,7 @@ on: pr_number: required: true type: string + workflow_dispatch: # When the cancel-in-progress: true option is specified, any concurrent jobs or workflows using the same # concurrency group will cancel both the pending and currently running jobs or workflows. This allows only @@ -192,7 +193,10 @@ jobs: ### CLEANUP AFTER TESTS - name: Cleanup nebari deployment - if: always() + # Since this is not critical for most pull requests and takes more than half of the time + # in the CI, it makes sense to only run on merge to main or workflow_dispatch to speed + # up feedback cycle + if: github.ref_name == 'develop' || github.event_name == 'workflow_dispatch' working-directory: local-deployment run: | nebari destroy --config nebari-config.yaml --disable-prompt diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000000..2a8bf120fa --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,46 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Code Scanning + +on: + push: + branches: [ "develop", "release/*" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "develop" ] + schedule: + - cron: '19 23 * * 6' + +permissions: + contents: read + +jobs: + SAST: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Trivy config Scan + runs-on: "ubuntu-20.04" + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner in fs mode + uses: aquasecurity/trivy-action@master + with: + scan-type: 'config' + hide-progress: true + format: 'sarif' + output: 'trivy-results.sarif' + ignore-unfixed: true + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e9dcd9147..187584d2d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ ci: repos: # general - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: end-of-file-fixer exclude: "^docs-sphinx/cli.html" @@ -36,28 +36,32 @@ repos: - id: check-executables-have-shebangs exclude: "^src/_nebari/template/" + - repo: https://github.com/crate-ci/typos + rev: v1.23.6 + hooks: + - id: typos + - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell args: [ - "--builtin=rare,clear,informal,names", - "--skip=_build,*/build/*,*/node_modules/*,nebari.egg-info,*.git,*.js,*.json,*.yaml,*.yml", - "--ignore-words-list=AKS,aks", "--write", ] language: python + additional_dependencies: + - tomli # python - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.8.0 hooks: - id: black args: ["--line-length=88", "--exclude=/src/_nebari/template/"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 + rev: v0.5.6 hooks: - id: ruff args: ["--fix"] @@ -73,7 +77,7 @@ repos: # terraform - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.88.4 + rev: v1.92.1 hooks: - id: terraform_fmt args: diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..5294ea08d0 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: + - name: "Nebari Development Team" +type: software +title: "Nebari" +version: 2024.4.1 +date-released: 2024-04-20 +url: "https://www.nebari.dev" +repository-code: "https://github.com/nebari-dev/nebari" diff --git a/README.md b/README.md index c693dfb22f..c6a81a17c4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ | :---------- | :-----| | Project | [![License](https://img.shields.io/badge/License-BSD%203--Clause-gray.svg?colorA=2D2A56&colorB=5936D9&style=flat.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Nebari documentation](https://img.shields.io/badge/%F0%9F%93%96%20Read-the%20docs-gray.svg?colorA=2D2A56&colorB=5936D9&style=flat.svg)](https://www.nebari.dev/docs/welcome) [![PyPI](https://img.shields.io/pypi/v/nebari)](https://badge.fury.io/py/nebari) [![conda version](https://img.shields.io/conda/vn/conda-forge/nebari)]((https://anaconda.org/conda-forge/nebari)) | | Community | [![GH discussions](https://img.shields.io/badge/%F0%9F%92%AC%20-Participate%20in%20discussions-gray.svg?colorA=2D2A56&colorB=5936D9&style=flat.svg)](https://github.com/nebari-dev/nebari/discussions) [![Open an issue](https://img.shields.io/badge/%F0%9F%93%9D%20Open-an%20issue-gray.svg?colorA=2D2A56&colorB=5936D9&style=flat.svg)](https://github.com/nebari-dev/nebari/issues/new/choose) [![Community guidelines](https://img.shields.io/badge/🤝%20Community-guidelines-gray.svg?colorA=2D2A56&colorB=5936D9&style=flat.svg)](https://www.nebari.dev/docs/community/) | -| CI | [![Kubernetes Tests](https://github.com/nebari-dev/nebari/actions/workflows/test_local_integration.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/kubernetes_test.yaml) [![Tests](https://github.com/nebari-dev/nebari/actions/workflows/test.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test.yaml) [![Test Nebari Provider](https://github.com/nebari-dev/nebari/actions/workflows/test-provider.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test-provider.yaml) | +| CI | [![Kubernetes Tests](https://github.com/nebari-dev/nebari/actions/workflows/test_local_integration.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/kubernetes_test.yaml) [![Tests](https://github.com/nebari-dev/nebari/actions/workflows/test.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test.yaml) [![Test Nebari Provider](https://github.com/nebari-dev/nebari/actions/workflows/test-provider.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test-provider.yaml)| +| Cloud Providers | [![AWS Deployment Status](https://github.com/nebari-dev/nebari/actions/workflows/test_aws_integration.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test_aws_integration.yaml) [![Azure Deployment Status](https://github.com/nebari-dev/nebari/actions/workflows/test_azure_integration.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test_azure_integration.yaml) [![GCP Deployment Status](https://github.com/nebari-dev/nebari/actions/workflows/test_gcp_integration.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test_gcp_integration.yaml) [![Digital Ocean Deployment Status](https://github.com/nebari-dev/nebari/actions/workflows/test_do_integration.yaml/badge.svg)](https://github.com/nebari-dev/nebari/actions/workflows/test_do_integration.yaml)| ## Table of contents diff --git a/RELEASE.md b/RELEASE.md index c3ea55dda9..14f15b15fa 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -9,6 +9,93 @@ This file is copied to nebari-dev/nebari-docs using a GitHub Action. --> --- +### Release 2024.7.1 - August 8, 2024 + +> NOTE: Support for Digital Ocean deployments using CLI commands and related Terraform modules is being deprecated. Although Digital Ocean will no longer be directly supported in future releases, you can still deploy to Digital Ocean infrastructure using the current `existing` deployment option. + +## What's Changed +* Enable authentication by default in jupyter-server by @krassowski in https://github.com/nebari-dev/nebari/pull/2288 +* remove dns sleep by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2550 +* Conda-store permissions v2 + load roles from keycloak by @aktech in https://github.com/nebari-dev/nebari/pull/2531 +* Restrict public access and add bucket encryption using cmk by @dcmcand in https://github.com/nebari-dev/nebari/pull/2525 +* Add overwrite to AWS coredns addon by @dcmcand in https://github.com/nebari-dev/nebari/pull/2538 +* Add a default roles at initialisation by @aktech in https://github.com/nebari-dev/nebari/pull/2546 +* Hide gallery section if no exhibits are configured by @krassowski in https://github.com/nebari-dev/nebari/pull/2549 +* Add note about ~/.bash_profile by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2575 +* Expose jupyterlab-gallery branch and depth options by @krassowski in https://github.com/nebari-dev/nebari/pull/2556 +* #2566 Upgrade Jupyterhub ssh image by @arjxn-py in https://github.com/nebari-dev/nebari/pull/2576 +* Stop copying unnecessary files into user home directory by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2578 +* Include deprecation notes for init/deploy subcommands by @viniciusdc in https://github.com/nebari-dev/nebari/pull/2582 +* Only download jar if file doesn't exist by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2588 +* Remove unnecessary experimental flag by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2606 +* Add typos spell checker to pre-commit by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2568 +* Enh 2451 skip conditionals by @BrianCashProf in https://github.com/nebari-dev/nebari/pull/2569 +* Improve codespell support: adjust and concentrate config to pyproject.toml and fix more typos by @yarikoptic in https://github.com/nebari-dev/nebari/pull/2583 +* Move codespell config to pyproject.toml only by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2611 +* Add `depends_on` for bucket encryption by @viniciusdc in https://github.com/nebari-dev/nebari/pull/2615 + +## New Contributors +* @BrianCashProf made their first contribution in https://github.com/nebari-dev/nebari/pull/2569 +* @yarikoptic made their first contribution in https://github.com/nebari-dev/nebari/pull/2583 + + +**Full Changelog**: https://github.com/nebari-dev/nebari/compare/2024.6.1...2024.7.1 + + +### Release 2024.6.1 - June 26, 2024 + +> NOTE: This release includes an upgrade to the `kube-prometheus-stack` Helm chart, resulting in a newer version of Grafana. When upgrading your Nebari cluster, you will be prompted to have Nebari update some CRDs and delete a DaemonSet on your behalf. If you prefer, you can also run the commands yourself, which will be shown to you. If you have any custom dashboards, you'll also need to back them up by [exporting them as JSON](https://grafana.com/docs/grafana/latest/dashboards/share-dashboards-panels/#export-a-dashboard-as-json), so you can [import them](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/import-dashboards/#import-a-dashboard) after upgrading. + +### What's Changed +* Fetch JupyterHub roles from Keycloak by @krassowski in https://github.com/nebari-dev/nebari/pull/2447 +* Update selector for Start server button to use button tag by @krassowski in https://github.com/nebari-dev/nebari/pull/2464 +* Reduce GCP Fixed Costs by 50% by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2453 +* Restore JupyterHub updates from PR-2427 by @viniciusdc in https://github.com/nebari-dev/nebari/pull/2465 +* Workload identity by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2460 +* Fix test using a non-specific selector by @krassowski in https://github.com/nebari-dev/nebari/pull/2475 +* add verify=false since we use self signed cert in tests by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2481 +* fix forward auth when using custom cert by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2479 +* Upgrade to JupyterHub 5.0.0b2 by @krassowski in https://github.com/nebari-dev/nebari/pull/2468 +* upgrade instructions for PR 2453 by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2466 +* Use Helm Chart for JupyterHub 5.0.0 final by @krassowski in https://github.com/nebari-dev/nebari/pull/2484 +* Parse and insert keycloak roles scopes into JupyterHub by @aktech in https://github.com/nebari-dev/nebari/pull/2471 +* Add CITATION file by @pavithraes in https://github.com/nebari-dev/nebari/pull/2455 +* CI: add azure integration by @fangchenli in https://github.com/nebari-dev/nebari/pull/2061 +* Create trivy.yml by @dcmcand in https://github.com/nebari-dev/nebari/pull/2458 +* don't run azure deployment on PRs, only on schedule and manual trigger by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2498 +* add cloud provider deployment status badges to README.md by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2407 +* Upgrade kube-prometheus-stack helm chart by @marcelovilla in https://github.com/nebari-dev/nebari/pull/2472 +* upgrade note by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2502 +* Remove VSCode from jhub_apps default services by @jbouder in https://github.com/nebari-dev/nebari/pull/2503 +* Explicit config by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2294 +* fix general node scaling bug for azure by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/2517 +* Skip running cleanup on pull requests by @aktech in https://github.com/nebari-dev/nebari/pull/2488 +* 1792 Add docstrings to `upgrade.py` by @arjxn-py in https://github.com/nebari-dev/nebari/pull/2512 +* set's min TLS version for azure storage account to TLS 1.2 by @dcmcand in https://github.com/nebari-dev/nebari/pull/2522 +* Fix conda-store and Traefik Grafana Dashboards by @marcelovilla in https://github.com/nebari-dev/nebari/pull/2540 +* Implement support for jupyterlab-gallery config by @krassowski in https://github.com/nebari-dev/nebari/pull/2501 +* Add option to run CRDs updates and DaemonSet deletion on user's behalf. by @marcelovilla in https://github.com/nebari-dev/nebari/pull/2544 + +### New Contributors +* @arjxn-py made their first contribution in https://github.com/nebari-dev/nebari/pull/2512 + +**Full Changelog**: https://github.com/nebari-dev/nebari/compare/2024.5.1...2024.6.1 + +### Release 2024.5.1 - May 13, 2024 + +## What's Changed + +* make userscheduler run on general node group by @Adam-D-Lewis in +* Upgrade to Pydantic V2 by @Adam-D-Lewis in +* Pydantic2 PR fix by @Adam-D-Lewis in +* remove redundant pydantic class, fix bug by @Adam-D-Lewis in +* Update `python-keycloak` version pins constraints by @viniciusdc in +* add HERA_TOKEN env var to user pods by @Adam-D-Lewis in +* fix docs link by @Adam-D-Lewis in +* Update allowed admin groups by @aktech in + +**Full Changelog**: + ## Release 2024.4.1 - April 20, 2024 ### What's Changed @@ -231,7 +318,7 @@ command and follow the instructions * paginator for boto3 ec2 instance types by @sblair-metrostar in https://github.com/nebari-dev/nebari/pull/1923 * Update README.md -- fix typo. by @teoliphant in https://github.com/nebari-dev/nebari/pull/1925 * Add more unit tests, add cleanup step for Digital Ocean integration test by @iameskild in https://github.com/nebari-dev/nebari/pull/1910 -* Add cleanup step for AWS integration test, ensure diable_prompt is passed through by @iameskild in https://github.com/nebari-dev/nebari/pull/1921 +* Add cleanup step for AWS integration test, ensure disable_prompt is passed through by @iameskild in https://github.com/nebari-dev/nebari/pull/1921 * K8s 1.25 + More Improvements by @Adam-D-Lewis in https://github.com/nebari-dev/nebari/pull/1856 * adding lifecycle ignore to eks node group by @sblair-metrostar in https://github.com/nebari-dev/nebari/pull/1905 * nebari init unit tests by @sblair-metrostar in https://github.com/nebari-dev/nebari/pull/1931 diff --git a/pyproject.toml b/pyproject.toml index aeba24460b..a560d64c35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ dependencies = [ "ruamel.yaml==0.18.6", "typer==0.9.0", "packaging==23.2", + "typing-extensions==4.11.0", ] [project.optional-dependencies] @@ -101,7 +102,7 @@ docs = [ ] [project.urls] -Documentation = "https://www.nebari.dev/docs" +Documentation = "https://www.nebari.dev/docs/welcome" Source = "https://github.com/nebari-dev/nebari" [project.scripts] @@ -136,21 +137,23 @@ module = [ ignore_missing_imports = true [tool.ruff] +extend-exclude = [ + "src/_nebari/template", + "home", + "__pycache__" +] + +[tool.ruff.lint] select = [ - "E", - "F", - "PTH", + "E", # E: pycodestyle rules + "F", # F: pyflakes rules + "PTH", # PTH: flake8-use-pathlib rules ] ignore = [ "E501", # Line too long "F821", # Undefined name "PTH123", # open() should be replaced by Path.open() ] -extend-exclude = [ - "src/_nebari/template", - "home", - "__pycache__" -] [tool.coverage.run] branch = true @@ -174,3 +177,16 @@ exclude_also = [ "@(abc\\.)?abstractmethod", ] ignore_errors = false + +[tool.typos] +files.extend-exclude = ["_build", "*/build/*", "*/node_modules/*", "nebari.egg-info", "*.git", "*.js", "*.json", "*.yaml", "*.yml", "pre-commit-config.yaml"] +default.extend-ignore-re = ["(?Rm)^.*(#|//)\\s*typos: ignore$"] +default.extend-ignore-words-re = ["aks", "AKS"] +default.check-filename = false # Turn off initially, enable once https://github.com/nebari-dev/nebari/issues/2598 is addressed + +[tool.codespell] +# Ref: https://github.com/codespell-project/codespell#using-a-config-file +skip = '_build,*/build/*,*/node_modules/*,nebari.egg-info,*.git,package-lock.json,*.lock' +check-hidden = true +ignore-regex = '^\s*"image/\S+": ".*' +ignore-words-list = 'aks' diff --git a/pytest.ini b/pytest.ini index 0555ec6b2d..d299f154a8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] addopts = - # show tests that (f)ailed, (E)rror, or (X)passed in the summary + # show tests that (f)ailed, (E)rror, or (X)passed in the summary # typos: ignore -rfEX # Make tracebacks shorter --tb=native diff --git a/src/_nebari/config.py b/src/_nebari/config.py index 7c27274f36..9d7dec4bd4 100644 --- a/src/_nebari/config.py +++ b/src/_nebari/config.py @@ -103,7 +103,8 @@ def write_configuration( """Write the nebari configuration file to disk""" with config_filename.open(mode) as f: if isinstance(config, pydantic.BaseModel): - yaml.dump(config.model_dump(), f) + config_dict = config.model_dump() + yaml.dump(config_dict, f) else: config = dump_nested_model(config) yaml.dump(config, f) diff --git a/src/_nebari/constants.py b/src/_nebari/constants.py index 7ca8df28b4..a0ad80d3a4 100644 --- a/src/_nebari/constants.py +++ b/src/_nebari/constants.py @@ -1,4 +1,4 @@ -CURRENT_RELEASE = "2024.4.1" +CURRENT_RELEASE = "2024.7.1" # NOTE: Terraform cannot be upgraded further due to Hashicorp licensing changes # implemented in August 2023. diff --git a/src/_nebari/deploy.py b/src/_nebari/deploy.py index 46cc20179b..4478e65f75 100644 --- a/src/_nebari/deploy.py +++ b/src/_nebari/deploy.py @@ -49,7 +49,9 @@ def deploy_configuration( stage_outputs = {} with contextlib.ExitStack() as stack: for stage in stages: - s = stage(output_directory=pathlib.Path.cwd(), config=config) + s: hookspecs.NebariStage = stage( + output_directory=pathlib.Path.cwd(), config=config + ) stack.enter_context(s.deploy(stage_outputs, disable_prompt)) if not disable_checks: diff --git a/src/_nebari/destroy.py b/src/_nebari/destroy.py index 900ad8acf8..cf17bd733f 100644 --- a/src/_nebari/destroy.py +++ b/src/_nebari/destroy.py @@ -22,7 +22,9 @@ def destroy_configuration(config: schema.Main, stages: List[hookspecs.NebariStag with contextlib.ExitStack() as stack: for stage in stages: try: - s = stage(output_directory=pathlib.Path.cwd(), config=config) + s: hookspecs.NebariStage = stage( + output_directory=pathlib.Path.cwd(), config=config + ) stack.enter_context(s.destroy(stage_outputs, status)) except Exception as e: status[s.name] = False diff --git a/src/_nebari/stages/infrastructure/__init__.py b/src/_nebari/stages/infrastructure/__init__.py index f430c49126..cda515b786 100644 --- a/src/_nebari/stages/infrastructure/__init__.py +++ b/src/_nebari/stages/infrastructure/__init__.py @@ -1,4 +1,5 @@ import contextlib +import enum import inspect import os import pathlib @@ -18,6 +19,7 @@ google_cloud, ) from _nebari.stages.base import NebariTerraformStage +from _nebari.stages.kubernetes_services import SharedFsEnum from _nebari.stages.tf_objects import NebariTerraformState from _nebari.utils import ( AZURE_NODE_RESOURCE_GROUP_SUFFIX, @@ -73,6 +75,16 @@ class GCPPrivateClusterConfig(schema.Base): master_ipv4_cidr_block: str +@schema.yaml_object(schema.yaml) +class GCPNodeGroupImageTypeEnum(str, enum.Enum): + UBUNTU_CONTAINERD = "UBUNTU_CONTAINERD" + COS_CONTAINERD = "COS_CONTAINERD" + + @classmethod + def to_yaml(cls, representer, node): + return representer.represent_str(node.value) + + class GCPInputVars(schema.Base): name: str environment: str @@ -90,6 +102,7 @@ class GCPInputVars(schema.Base): ip_allocation_policy: Optional[Dict[str, str]] = None master_authorized_networks_config: Optional[Dict[str, str]] = None private_cluster_config: Optional[GCPPrivateClusterConfig] = None + node_group_image_type: GCPNodeGroupImageTypeEnum = None class AzureNodeGroupInputVars(schema.Base): @@ -112,6 +125,7 @@ class AzureInputVars(schema.Base): tags: Dict[str, str] = {} max_pods: Optional[int] = None network_profile: Optional[Dict[str, str]] = None + workload_identity_enabled: bool = False class AWSNodeGroupInputVars(schema.Base): @@ -138,6 +152,7 @@ class AWSInputVars(schema.Base): permissions_boundary: Optional[str] = None kubeconfig_filename: str = get_kubeconfig_filename() tags: Dict[str, str] = {} + efs_enabled: bool def _calculate_asg_node_group_map(config: schema.Main): @@ -173,7 +188,7 @@ def _calculate_node_groups(config: schema.Main): for group in ["general", "user", "worker"] } elif config.provider == schema.ProviderEnum.existing: - return config.existing.node_selectors + return config.existing.model_dump()["node_selectors"] else: return config.local.model_dump()["node_selectors"] @@ -314,9 +329,9 @@ class GCPNodeGroup(schema.Base): DEFAULT_GCP_NODE_GROUPS = { - "general": GCPNodeGroup(instance="n1-standard-8", min_nodes=1, max_nodes=1), - "user": GCPNodeGroup(instance="n1-standard-4", min_nodes=0, max_nodes=5), - "worker": GCPNodeGroup(instance="n1-standard-4", min_nodes=0, max_nodes=5), + "general": GCPNodeGroup(instance="e2-standard-8", min_nodes=1, max_nodes=1), + "user": GCPNodeGroup(instance="e2-standard-4", min_nodes=0, max_nodes=5), + "worker": GCPNodeGroup(instance="e2-standard-4", min_nodes=0, max_nodes=5), } @@ -339,10 +354,10 @@ class GoogleCloudPlatformProvider(schema.Base): @classmethod def _check_input(cls, data: Any) -> Any: google_cloud.check_credentials() - avaliable_regions = google_cloud.regions() - if data["region"] not in avaliable_regions: + available_regions = google_cloud.regions() + if data["region"] not in available_regions: raise ValueError( - f"Google Cloud region={data['region']} is not one of {avaliable_regions}" + f"Google Cloud region={data['region']} is not one of {available_regions}" ) available_kubernetes_versions = google_cloud.kubernetes_versions(data["region"]) @@ -380,6 +395,7 @@ class AzureProvider(schema.Base): tags: Optional[Dict[str, str]] = {} network_profile: Optional[Dict[str, str]] = None max_pods: Optional[int] = None + workload_identity_enabled: bool = False @model_validator(mode="before") @classmethod @@ -582,16 +598,16 @@ def check_provider(cls, data: Any) -> Any: f"'{provider}' is not a valid enumeration member; permitted: local, existing, do, aws, gcp, azure" ) else: - setted_providers = [ + set_providers = [ provider for provider in provider_name_abbreviation_map.keys() if provider in data ] - num_providers = len(setted_providers) + num_providers = len(set_providers) if num_providers > 1: - raise ValueError(f"Multiple providers set: {setted_providers}") + raise ValueError(f"Multiple providers set: {set_providers}") elif num_providers == 1: - data["provider"] = provider_name_abbreviation_map[setted_providers[0]] + data["provider"] = provider_name_abbreviation_map[set_providers[0]] elif num_providers == 0: data["provider"] = schema.ProviderEnum.local.value return data @@ -604,7 +620,7 @@ class NodeSelectorKeyValue(schema.Base): class KubernetesCredentials(schema.Base): host: str - cluster_ca_certifiate: str + cluster_ca_certifiate: str # ignored for now. More info in https://github.com/nebari-dev/nebari/issues/2597. # typos: ignore token: Optional[str] = None username: Optional[str] = None password: Optional[str] = None @@ -750,6 +766,11 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): ip_allocation_policy=self.config.google_cloud_platform.ip_allocation_policy, master_authorized_networks_config=self.config.google_cloud_platform.master_authorized_networks_config, private_cluster_config=self.config.google_cloud_platform.private_cluster_config, + node_group_image_type=( + GCPNodeGroupImageTypeEnum.UBUNTU_CONTAINERD + if self.config.storage.type == SharedFsEnum.cephfs + else GCPNodeGroupImageTypeEnum.COS_CONTAINERD + ), ).model_dump() elif self.config.provider == schema.ProviderEnum.azure: return AzureInputVars( @@ -781,6 +802,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): tags=self.config.azure.tags, network_profile=self.config.azure.network_profile, max_pods=self.config.azure.max_pods, + workload_identity_enabled=self.config.azure.workload_identity_enabled, ).model_dump() elif self.config.provider == schema.ProviderEnum.aws: return AWSInputVars( @@ -807,6 +829,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): vpc_cidr_block=self.config.amazon_web_services.vpc_cidr_block, permissions_boundary=self.config.amazon_web_services.permissions_boundary, tags=self.config.amazon_web_services.tags, + efs_enabled=self.config.storage.type == SharedFsEnum.efs, ).model_dump() else: raise ValueError(f"Unknown provider: {self.config.provider}") diff --git a/src/_nebari/stages/infrastructure/template/aws/main.tf b/src/_nebari/stages/infrastructure/template/aws/main.tf index 2c78018f0b..356ce8f957 100644 --- a/src/_nebari/stages/infrastructure/template/aws/main.tf +++ b/src/_nebari/stages/infrastructure/template/aws/main.tf @@ -64,6 +64,7 @@ module "registry-jupyterlab" { # ====================== EFS ========================= module "efs" { + count = var.efs_enabled ? 1 : 0 source = "./modules/efs" name = "${local.cluster_name}-jupyterhub-shared" diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/kafka/main.tf b/src/_nebari/stages/infrastructure/template/aws/modules/kafka/main.tf deleted file mode 100644 index 877a5955be..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/kafka/main.tf +++ /dev/null @@ -1,29 +0,0 @@ -resource "aws_kms_key" "main" { - description = var.name - - tags = merge({ Name = var.name }, var.tags) -} - -resource "aws_msk_cluster" "main" { - cluster_name = var.name - kafka_version = var.kafka_version - number_of_broker_nodes = var.kafka_instance_count - - broker_node_group_info { - instance_type = var.kafka_instance_type - ebs_volume_size = var.kafka_ebs_volume_size - client_subnets = var.kafka_vpc_subnets - security_groups = var.kafka_security_groups - } - - encryption_info { - encryption_at_rest_kms_key_arn = aws_kms_key.main.arn - - encryption_in_transit { - client_broker = "TLS" - in_cluster = true - } - } - - tags = merge({ Name = var.name }, var.tags) -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/kafka/outputs.tf b/src/_nebari/stages/infrastructure/template/aws/modules/kafka/outputs.tf deleted file mode 100644 index 0b2189a934..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/kafka/outputs.tf +++ /dev/null @@ -1,8 +0,0 @@ -output "credentials" { - description = "Important credentials for connecting to MSK cluster" - value = { - zookeeper_host = aws_msk_cluster.main.zookeeper_connect_string - bootstrap_brokers = aws_msk_cluster.main.bootstrap_brokers - bootstrap_brokers_tls = aws_msk_cluster.main.bootstrap_brokers_tls - } -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/kafka/variables.tf b/src/_nebari/stages/infrastructure/template/aws/modules/kafka/variables.tf deleted file mode 100644 index 14d3f8b4a3..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/kafka/variables.tf +++ /dev/null @@ -1,44 +0,0 @@ -variable "name" { - description = "Name to give kafka cluster" - type = string -} - -variable "tags" { - description = "Tags for kafka cluster" - type = map(string) - default = {} -} - -variable "kafka_version" { - description = "Kafka server version" - type = string - default = "2.3.1" -} - -variable "kafka_instance_count" { - description = "Number of nodes to run Kafka cluster on" - type = number - default = 2 -} - -variable "kafka_instance_type" { - description = "AWS Instance type to run Kafka cluster on" - type = string - default = "kafka.m5.large" -} - -variable "kafka_ebs_volume_size" { - description = "AWS EBS volume size (GB) to use for Kafka broker storage" - type = number - default = 100 -} - -variable "kafka_vpc_subnets" { - description = "Kafka VPC subnets to run cluster on" - type = list(string) -} - -variable "kafka_security_groups" { - description = "Kafka security groups to run cluster on" - type = list(string) -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf index 43e5538507..521096cae0 100644 --- a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf +++ b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf @@ -89,8 +89,11 @@ resource "aws_eks_addon" "aws-ebs-csi-driver" { } resource "aws_eks_addon" "coredns" { - addon_name = "coredns" - cluster_name = aws_eks_cluster.main.name + addon_name = "coredns" + cluster_name = aws_eks_cluster.main.name + resolve_conflicts_on_create = "OVERWRITE" + resolve_conflicts_on_update = "OVERWRITE" + configuration_values = jsonencode({ nodeSelector = { diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/permissions/main.tf b/src/_nebari/stages/infrastructure/template/aws/modules/permissions/main.tf deleted file mode 100644 index ac88990c40..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/permissions/main.tf +++ /dev/null @@ -1,36 +0,0 @@ -resource "aws_iam_user" "main" { - name = var.name - - tags = merge({ Name = var.name }, var.tags) -} - -resource "aws_iam_access_key" "main" { - user = aws_iam_user.main.name -} - -data "aws_iam_policy_document" "main" { - depends_on = [ - aws_iam_user.main, - aws_iam_access_key.main - ] - - statement { - sid = "1" - - effect = "Allow" - - actions = var.allowed_policy_actions - resources = var.allowed_policy_resources - } -} - -resource "aws_iam_policy" "main" { - name = var.name - path = "/" - policy = data.aws_iam_policy_document.main.json -} - -resource "aws_iam_user_policy_attachment" "main" { - user = aws_iam_user.main.name - policy_arn = aws_iam_policy.main.arn -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/permissions/outputs.tf b/src/_nebari/stages/infrastructure/template/aws/modules/permissions/outputs.tf deleted file mode 100644 index 9153e6223f..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/permissions/outputs.tf +++ /dev/null @@ -1,11 +0,0 @@ -output "credentials" { - description = "Information about specific AWS IAM user" - value = { - user_arn = aws_iam_user.main.arn, - username = aws_iam_user.main.name, - access_key = aws_iam_access_key.main.id, - secret_key = aws_iam_access_key.main.secret - allowed_policies = var.allowed_policy_actions, - allowed_resources = var.allowed_policy_resources - } -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/permissions/variables.tf b/src/_nebari/stages/infrastructure/template/aws/modules/permissions/variables.tf deleted file mode 100644 index 16e3d65bf8..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/permissions/variables.tf +++ /dev/null @@ -1,22 +0,0 @@ -variable "name" { - description = "Prefix name to use to annotate permission resources" - type = string -} - -variable "tags" { - description = "AWS iam additional tags" - type = map(string) - default = {} -} - -variable "allowed_policy_actions" { - description = "Actions to allow IAM user to perform" - type = list(string) - default = [] -} - -variable "allowed_policy_resources" { - description = "Allowed AWS arns for user to have access to" - type = list(string) - default = [] -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/rds/main.tf b/src/_nebari/stages/infrastructure/template/aws/modules/rds/main.tf deleted file mode 100644 index e50beb2b1b..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/rds/main.tf +++ /dev/null @@ -1,39 +0,0 @@ -resource "aws_rds_cluster" "main" { - cluster_identifier = var.name - - engine = var.rds_database_engine - - database_name = var.database_master.database - master_username = var.database_master.username - master_password = var.database_master.password - - backup_retention_period = 5 - preferred_backup_window = "07:00-09:00" - skip_final_snapshot = true - iam_database_authentication_enabled = true - - # NOTE - this should be removed when not in dev mode to reduce risk - # of downtime - apply_immediately = true - - tags = merge({ - Name = var.name - Description = "RDS database for ${var.name}-rds-cluster" - }, var.tags) -} - -resource "aws_rds_cluster_instance" "main" { - count = 1 - identifier = "${var.name}-cluster-instance-${count.index}" - - cluster_identifier = aws_rds_cluster.main.id - instance_class = var.rds_instance_type - publicly_accessible = true - - engine = var.rds_database_engine - - tags = merge({ - Name = "${var.name}-cluster-instance-${count.index}" - Description = "RDS database for ${var.name}-rds-cluster instances" - }, var.tags) -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/rds/outputs.tf b/src/_nebari/stages/infrastructure/template/aws/modules/rds/outputs.tf deleted file mode 100644 index fb8c1db95a..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/rds/outputs.tf +++ /dev/null @@ -1,15 +0,0 @@ -output "credentials" { - description = "connection string for master database connection" - value = { - arn = aws_rds_cluster.main.arn - username = aws_rds_cluster.main.master_username - password = aws_rds_cluster.main.master_password - database = aws_rds_cluster.main.database_name - host = aws_rds_cluster.main.endpoint - port = aws_rds_cluster.main.port - } -} - -# output "aws_postgresql_user_connections" { -# description = "Database connections and iam users for each database" -# } diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/rds/users.tf b/src/_nebari/stages/infrastructure/template/aws/modules/rds/users.tf deleted file mode 100644 index d93c4263e0..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/rds/users.tf +++ /dev/null @@ -1,51 +0,0 @@ -# # ======================================================= -# # AWS RDS + IAM Policy Setup -# # ======================================================= - -# resource "aws_iam_user" "psql_user" { -# count = length(var.postgresql_additional_users) - -# name = "${var.name}-psql" -# } - -# resource "aws_iam_access_key" "psql_user" { -# user = aws_iam_user.psql_user.name -# } - -# output "psql_user_secret" { -# description = "PSQL User Access Keys" -# value = aws_iam_access_key.psql_user.encrypted_secret -# } - -# data "aws_iam_policy_document" "psql" { -# depends_on = [ -# aws_rds_cluster.postgresql -# ] - -# statement { -# sid = "1" - -# effect = "Allow" - -# actions = [ -# "rds-db:connect" -# ] - -# # should username be included with arn? var.postgresql_user? -# resources = concat( -# [ aws_rds_cluster.postgresql.arn ], -# aws_rds_cluster_instance.postgresql[*].arn -# ) -# } -# } - -# resource "aws_iam_policy" "psql" { -# name = "${var.name}-psql" -# path = "/" -# policy = data.aws_iam_policy_document.psql.json -# } - -# resource "aws_iam_user_policy_attachment" "psql_attach" { -# user = aws_iam_user.psql_user.name -# policy_arn = aws_iam_policy.psql.arn -# } diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/rds/variables.tf b/src/_nebari/stages/infrastructure/template/aws/modules/rds/variables.tf deleted file mode 100644 index e49b8ba822..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/rds/variables.tf +++ /dev/null @@ -1,37 +0,0 @@ -variable "name" { - description = "Prefix name to assign to AWS RDS postgresql database" - type = string -} - -variable "tags" { - description = "Additional tags to assign to AWS RDS postgresql database" - type = map(string) - default = {} -} - -variable "rds_instance_type" { - description = "AWS Instance type for postgresql instance" - type = string - default = "db.r4.large" -} - -variable "rds_number_instances" { - description = "AWS number of rds database instances" - type = number - default = 1 -} - -variable "rds_database_engine" { - description = "aurora-postgresql" - type = string - default = "aurora-postgresql" -} - -variable "database_master" { - description = "AWS RDS master" - type = object({ - username = string - password = string - database = string - }) -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/s3/main.tf b/src/_nebari/stages/infrastructure/template/aws/modules/s3/main.tf deleted file mode 100644 index 6f349fbf16..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/s3/main.tf +++ /dev/null @@ -1,13 +0,0 @@ -resource "aws_s3_bucket" "main" { - bucket = var.name - acl = var.public ? "public-read" : "private" - - versioning { - enabled = true - } - - tags = merge({ - Name = var.name - Description = "S3 bucket for ${var.name}" - }, var.tags) -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/s3/outputs.tf b/src/_nebari/stages/infrastructure/template/aws/modules/s3/outputs.tf deleted file mode 100644 index 11f9f58930..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/s3/outputs.tf +++ /dev/null @@ -1,8 +0,0 @@ -output "credentials" { - description = "Important credentials for connecting to S3 bucket" - value = { - bucket = aws_s3_bucket.main.bucket - bucket_domain_name = aws_s3_bucket.main.bucket_domain_name - arn = aws_s3_bucket.main.arn - } -} diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/s3/variables.tf b/src/_nebari/stages/infrastructure/template/aws/modules/s3/variables.tf deleted file mode 100644 index df14c5b830..0000000000 --- a/src/_nebari/stages/infrastructure/template/aws/modules/s3/variables.tf +++ /dev/null @@ -1,16 +0,0 @@ -variable "name" { - description = "Prefix name for bucket resource" - type = string -} - -variable "tags" { - description = "Additional tags to include with AWS S3 bucket" - type = map(string) - default = {} -} - -variable "public" { - description = "AWS s3 bucket is exposed publicly" - type = bool - default = false -} diff --git a/src/_nebari/stages/infrastructure/template/aws/outputs.tf b/src/_nebari/stages/infrastructure/template/aws/outputs.tf index 08581fe6e2..9c11139498 100644 --- a/src/_nebari/stages/infrastructure/template/aws/outputs.tf +++ b/src/_nebari/stages/infrastructure/template/aws/outputs.tf @@ -22,7 +22,7 @@ output "kubeconfig_filename" { output "nfs_endpoint" { description = "Endpoint for nfs server" - value = module.efs.credentials.dns_name + value = length(module.efs) == 1 ? module.efs[0].credentials.dns_name : null } output "cluster_oidc_issuer_url" { diff --git a/src/_nebari/stages/infrastructure/template/aws/variables.tf b/src/_nebari/stages/infrastructure/template/aws/variables.tf index c07c8f60f2..b0455d42ed 100644 --- a/src/_nebari/stages/infrastructure/template/aws/variables.tf +++ b/src/_nebari/stages/infrastructure/template/aws/variables.tf @@ -47,7 +47,7 @@ variable "availability_zones" { } variable "vpc_cidr_block" { - description = "VPC cidr block for infastructure" + description = "VPC cidr block for infrastructure" type = string } @@ -77,3 +77,8 @@ variable "tags" { type = map(string) default = {} } + +variable "efs_enabled" { + description = "Enable EFS" + type = bool +} diff --git a/src/_nebari/stages/infrastructure/template/azure/main.tf b/src/_nebari/stages/infrastructure/template/azure/main.tf index 2ee687cc0f..2d6e2e2afa 100644 --- a/src/_nebari/stages/infrastructure/template/azure/main.tf +++ b/src/_nebari/stages/infrastructure/template/azure/main.tf @@ -40,6 +40,7 @@ module "kubernetes" { max_size = config.max_nodes } ] - vnet_subnet_id = var.vnet_subnet_id - private_cluster_enabled = var.private_cluster_enabled + vnet_subnet_id = var.vnet_subnet_id + private_cluster_enabled = var.private_cluster_enabled + workload_identity_enabled = var.workload_identity_enabled } diff --git a/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/main.tf b/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/main.tf index 5f2bad6561..f093f048c6 100644 --- a/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/main.tf +++ b/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/main.tf @@ -5,6 +5,10 @@ resource "azurerm_kubernetes_cluster" "main" { resource_group_name = var.resource_group_name tags = var.tags + # To enable Azure AD Workload Identity oidc_issuer_enabled must be set to true. + oidc_issuer_enabled = var.workload_identity_enabled + workload_identity_enabled = var.workload_identity_enabled + # DNS prefix specified when creating the managed cluster. Changing this forces a new resource to be created. dns_prefix = "Nebari" # required @@ -27,18 +31,20 @@ resource "azurerm_kubernetes_cluster" "main" { default_node_pool { vnet_subnet_id = var.vnet_subnet_id name = var.node_groups[0].name - node_count = 1 vm_size = var.node_groups[0].instance_type enable_auto_scaling = "true" - min_count = 1 - max_count = 1 + min_count = var.node_groups[0].min_size + max_count = var.node_groups[0].max_size max_pods = var.max_pods - # node_labels = var.node_labels + orchestrator_version = var.kubernetes_version node_labels = { "azure-node-pool" = var.node_groups[0].name } tags = var.tags + + # temparory_name_for_rotation must be <= 12 characters + temporary_name_for_rotation = "${substr(var.node_groups[0].name, 0, 9)}tmp" } sku_tier = "Free" # "Free" [Default] or "Paid" @@ -47,39 +53,30 @@ resource "azurerm_kubernetes_cluster" "main" { type = "SystemAssigned" # "UserAssigned" or "SystemAssigned". SystemAssigned identity lifecycles are tied to the AKS Cluster. } + lifecycle { + ignore_changes = [ + # We ignore changes since otherwise, the AKS cluster unsets this default value every time you deploy. + # https://github.com/hashicorp/terraform-provider-azurerm/issues/24020#issuecomment-1887670287 + default_node_pool[0].upgrade_settings, + ] + } + } # https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster_node_pool -resource "azurerm_kubernetes_cluster_node_pool" "user_node_group" { - name = var.node_groups[1].name - kubernetes_cluster_id = azurerm_kubernetes_cluster.main.id - vm_size = var.node_groups[1].instance_type - node_count = 0 - enable_auto_scaling = "true" - mode = "User" # "System" or "User", only "User" nodes can scale down to 0 - min_count = var.node_groups[1].min_size - max_count = var.node_groups[1].max_size - max_pods = var.max_pods - node_labels = { - "azure-node-pool" = var.node_groups[1].name - } - orchestrator_version = var.kubernetes_version - tags = var.tags - vnet_subnet_id = var.vnet_subnet_id -} +resource "azurerm_kubernetes_cluster_node_pool" "node_group" { + for_each = { for i, group in var.node_groups : i => group if i != 0 } -resource "azurerm_kubernetes_cluster_node_pool" "worker_node_group" { - name = var.node_groups[2].name + name = each.value.name kubernetes_cluster_id = azurerm_kubernetes_cluster.main.id - vm_size = var.node_groups[2].instance_type - node_count = 0 + vm_size = each.value.instance_type enable_auto_scaling = "true" mode = "User" # "System" or "User", only "User" nodes can scale down to 0 - min_count = var.node_groups[2].min_size - max_count = var.node_groups[2].max_size + min_count = each.value.min_size + max_count = each.value.max_size max_pods = var.max_pods node_labels = { - "azure-node-pool" = var.node_groups[2].name + "azure-node-pool" = each.value.name } orchestrator_version = var.kubernetes_version tags = var.tags diff --git a/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/outputs.tf b/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/outputs.tf index 35d7b048b9..e96187bcd6 100644 --- a/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/outputs.tf +++ b/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/outputs.tf @@ -17,3 +17,13 @@ output "kubeconfig" { sensitive = true value = azurerm_kubernetes_cluster.main.kube_config_raw } + +output "cluster_oidc_issuer_url" { + description = "The OpenID Connect issuer URL that is associated with the AKS cluster" + value = azurerm_kubernetes_cluster.main.oidc_issuer_url +} + +output "resource_group_name" { + description = "The name of the resource group in which the AKS cluster is created" + value = azurerm_kubernetes_cluster.main.resource_group_name +} diff --git a/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/variables.tf b/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/variables.tf index b7159dad9b..b93a9fae2d 100644 --- a/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/variables.tf +++ b/src/_nebari/stages/infrastructure/template/azure/modules/kubernetes/variables.tf @@ -70,3 +70,9 @@ variable "max_pods" { type = number default = 60 } + +variable "workload_identity_enabled" { + description = "Enable Workload Identity" + type = bool + default = false +} diff --git a/src/_nebari/stages/infrastructure/template/azure/outputs.tf b/src/_nebari/stages/infrastructure/template/azure/outputs.tf index 352e52e3c5..d904e3ec1e 100644 --- a/src/_nebari/stages/infrastructure/template/azure/outputs.tf +++ b/src/_nebari/stages/infrastructure/template/azure/outputs.tf @@ -22,3 +22,13 @@ output "kubeconfig_filename" { description = "filename for nebari kubeconfig" value = var.kubeconfig_filename } + +output "cluster_oidc_issuer_url" { + description = "The OpenID Connect issuer URL that is associated with the AKS cluster" + value = module.kubernetes.cluster_oidc_issuer_url +} + +output "resource_group_name" { + description = "The name of the resource group in which the AKS cluster is created" + value = module.kubernetes.resource_group_name +} diff --git a/src/_nebari/stages/infrastructure/template/azure/variables.tf b/src/_nebari/stages/infrastructure/template/azure/variables.tf index 4d9e6440eb..dcef2c97cb 100644 --- a/src/_nebari/stages/infrastructure/template/azure/variables.tf +++ b/src/_nebari/stages/infrastructure/template/azure/variables.tf @@ -76,3 +76,9 @@ variable "max_pods" { type = number default = 60 } + +variable "workload_identity_enabled" { + description = "Enable Workload Identity" + type = bool + default = false +} diff --git a/src/_nebari/stages/infrastructure/template/gcp/main.tf b/src/_nebari/stages/infrastructure/template/gcp/main.tf index 4411cac406..3d23af5571 100644 --- a/src/_nebari/stages/infrastructure/template/gcp/main.tf +++ b/src/_nebari/stages/infrastructure/template/gcp/main.tf @@ -36,4 +36,5 @@ module "kubernetes" { release_channel = var.release_channel tags = var.tags labels = var.labels + node_group_image_type = var.node_group_image_type } diff --git a/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/main.tf b/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/main.tf index c4b18f32ad..57e8d9fc88 100644 --- a/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/main.tf +++ b/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/main.tf @@ -87,6 +87,7 @@ resource "google_container_node_pool" "main" { node_config { preemptible = local.merged_node_groups[count.index].preemptible machine_type = local.merged_node_groups[count.index].instance_type + image_type = var.node_group_image_type service_account = google_service_account.main.email diff --git a/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/outputs.tf b/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/outputs.tf index bfb5463295..513294aac7 100644 --- a/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/outputs.tf +++ b/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/outputs.tf @@ -1,5 +1,5 @@ output "credentials" { - description = "Credentials required for connecting to kubernets cluster" + description = "Credentials required for connecting to kubernetes cluster" sensitive = true value = { endpoint = "https://${google_container_cluster.main.endpoint}" diff --git a/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/variables.tf b/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/variables.tf index cef5363030..2ee2d78ed5 100644 --- a/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/variables.tf +++ b/src/_nebari/stages/infrastructure/template/gcp/modules/kubernetes/variables.tf @@ -156,3 +156,16 @@ variable "labels" { type = map(string) default = {} } + +variable "node_group_image_type" { + description = "The image type to use for the node groups" + type = string + default = null + + validation { + # Only 2 values are valid according to docs + # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#image_type + condition = var.node_group_image_type == null || contains(["COS_CONTAINERD", "UBUNTU_CONTAINERD"], var.node_group_image_type) + error_message = "Allowed values for input_parameter are \"COS_CONTAINERD\" or \"UBUNTU_CONTAINERD\"." + } +} diff --git a/src/_nebari/stages/infrastructure/template/gcp/variables.tf b/src/_nebari/stages/infrastructure/template/gcp/variables.tf index a0de29d0b9..5a280e59c9 100644 --- a/src/_nebari/stages/infrastructure/template/gcp/variables.tf +++ b/src/_nebari/stages/infrastructure/template/gcp/variables.tf @@ -99,3 +99,16 @@ variable "private_cluster_config" { master_ipv4_cidr_block = string })) } + +variable "node_group_image_type" { + description = "The image type to use for the node groups" + type = string + default = null + + validation { + # Only 2 values are valid according to docs + # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#image_type + condition = var.node_group_image_type == null || contains(["COS_CONTAINERD", "UBUNTU_CONTAINERD"], var.node_group_image_type) + error_message = "Allowed values for input_parameter are \"COS_CONTAINERD\" or \"UBUNTU_CONTAINERD\"." + } +} diff --git a/src/_nebari/stages/kubernetes_ingress/__init__.py b/src/_nebari/stages/kubernetes_ingress/__init__.py index 628d383830..ea5f8fa335 100644 --- a/src/_nebari/stages/kubernetes_ingress/__init__.py +++ b/src/_nebari/stages/kubernetes_ingress/__init__.py @@ -93,17 +93,18 @@ def _attempt_dns_lookup( attempt = 0 while not _attempt_dns_lookup(domain_name, ip): - sleeptime = 60 * (2**attempt) - if not disable_prompt: + if disable_prompt: + sleeptime = 60 * (2**attempt) + print(f"Will attempt to poll DNS again in {sleeptime} seconds...") + time.sleep(sleeptime) + else: input( f"After attempting to poll the DNS, the record for domain={domain_name} appears not to exist, " f"has recently been updated, or has yet to fully propagate. This non-deterministic behavior is likely due to " - f"DNS caching and will likely resolve itself in a few minutes.\n\n\tTo poll the DNS again in {sleeptime} seconds " - f"[Press Enter].\n\n...otherwise kill the process and run the deployment again later..." + f"DNS caching and will likely resolve itself in a few minutes.\n\n\tTo poll the DNS again [Press Enter].\n\n" + f"...otherwise kill the process and run the deployment again later..." ) - print(f"Will attempt to poll DNS again in {sleeptime} seconds...") - time.sleep(sleeptime) attempt += 1 if attempt == 5: print( diff --git a/src/_nebari/stages/kubernetes_keycloak/template/modules/kubernetes/keycloak-helm/values.yaml b/src/_nebari/stages/kubernetes_keycloak/template/modules/kubernetes/keycloak-helm/values.yaml index abe7d4d3e3..bf356145f0 100644 --- a/src/_nebari/stages/kubernetes_keycloak/template/modules/kubernetes/keycloak-helm/values.yaml +++ b/src/_nebari/stages/kubernetes_keycloak/template/modules/kubernetes/keycloak-helm/values.yaml @@ -27,14 +27,18 @@ extraInitContainers: | - sh - -c - | - wget https://github.com/aerogear/keycloak-metrics-spi/releases/download/2.5.3/keycloak-metrics-spi-2.5.3.jar -P /data/ && - export SHA256SUM=9b3f52f842a66dadf5ff3cc3a729b8e49042d32f84510a5d73d41a2e39f29a96 && - if ! (echo "$SHA256SUM /data/keycloak-metrics-spi-2.5.3.jar" | sha256sum -c) - then - echo "Error: Checksum not verified" && exit 1 - else - chown 1000:1000 /data/keycloak-metrics-spi-2.5.3.jar && - chmod 777 /data/keycloak-metrics-spi-2.5.3.jar + if [ ! -f /data/keycloak-metrics-spi-2.5.3.jar ]; then + wget https://github.com/aerogear/keycloak-metrics-spi/releases/download/2.5.3/keycloak-metrics-spi-2.5.3.jar -P /data/ && + export SHA256SUM=9b3f52f842a66dadf5ff3cc3a729b8e49042d32f84510a5d73d41a2e39f29a96 && + if ! (echo "$SHA256SUM /data/keycloak-metrics-spi-2.5.3.jar" | sha256sum -c) + then + echo "Error: Checksum not verified" && exit 1 + else + chown 1000:1000 /data/keycloak-metrics-spi-2.5.3.jar && + chmod 777 /data/keycloak-metrics-spi-2.5.3.jar + fi + else + echo "File /data/keycloak-metrics-spi-2.5.3.jar already exists. Skipping download." fi image: busybox:1.36 name: initialize-spi-metrics-jar diff --git a/src/_nebari/stages/kubernetes_services/__init__.py b/src/_nebari/stages/kubernetes_services/__init__.py index cdc1ae9151..ba32ca6186 100644 --- a/src/_nebari/stages/kubernetes_services/__init__.py +++ b/src/_nebari/stages/kubernetes_services/__init__.py @@ -6,6 +6,7 @@ from urllib.parse import urlencode from pydantic import ConfigDict, Field, field_validator, model_validator +from typing_extensions import Self from _nebari import constants from _nebari.stages.base import NebariTerraformStage @@ -14,7 +15,11 @@ NebariKubernetesProvider, NebariTerraformState, ) -from _nebari.utils import set_docker_image_tag, set_nebari_dask_version +from _nebari.utils import ( + byte_unit_conversion, + set_docker_image_tag, + set_nebari_dask_version, +) from _nebari.version import __version__ from nebari import schema from nebari.hookspecs import NebariStage, hookimpl @@ -24,6 +29,9 @@ TIMEOUT = 10 # seconds +_forwardauth_middleware_name = "traefik-forward-auth" + + @schema.yaml_object(schema.yaml) class AccessEnum(str, enum.Enum): all = "all" @@ -35,6 +43,17 @@ def to_yaml(cls, representer, node): return representer.represent_str(node.value) +@schema.yaml_object(schema.yaml) +class SharedFsEnum(str, enum.Enum): + nfs = "nfs" + cephfs = "cephfs" + efs = "efs" + + @classmethod + def to_yaml(cls, representer, node): + return representer.represent_str(node.value) + + class DefaultImages(schema.Base): jupyterhub: str = f"quay.io/nebari/nebari-jupyterhub:{set_docker_image_tag()}" jupyterlab: str = f"quay.io/nebari/nebari-jupyterlab:{set_docker_image_tag()}" @@ -42,6 +61,7 @@ class DefaultImages(schema.Base): class Storage(schema.Base): + type: SharedFsEnum = None conda_store: str = "200Gi" shared_filesystem: str = "200Gi" @@ -232,13 +252,37 @@ class IdleCuller(schema.Base): server_shutdown_no_activity_timeout: int = 15 +class JupyterLabGalleryExhibit(schema.Base): + git: str + title: str + homepage: Optional[str] = None + description: Optional[str] = None + icon: Optional[str] = None + account: Optional[str] = None + token: Optional[str] = None + branch: Optional[str] = None + depth: Optional[int] = None + + +class JupyterLabGallerySettings(schema.Base): + title: str = "Examples" + destination: str = "examples" + exhibits: List[JupyterLabGalleryExhibit] = [] + hide_gallery_without_exhibits: bool = True + + class JupyterLab(schema.Base): default_settings: Dict[str, Any] = {} + gallery_settings: JupyterLabGallerySettings = JupyterLabGallerySettings() idle_culler: IdleCuller = IdleCuller() initial_repositories: List[Dict[str, str]] = [] preferred_dir: Optional[str] = None +class RookCeph(schema.Base): + storage_class_name: None | str = None + + class InputSchema(schema.Base): default_images: DefaultImages = DefaultImages() storage: Storage = Storage() @@ -313,6 +357,35 @@ class InputSchema(schema.Base): jupyterhub: JupyterHub = JupyterHub() jupyterlab: JupyterLab = JupyterLab() jhub_apps: JHubApps = JHubApps() + ceph: RookCeph = RookCeph() + + def _set_storage_type_default_value(self): + if self.storage.type is None: + if self.provider == schema.ProviderEnum.aws: + self.storage.type = SharedFsEnum.efs + else: + self.storage.type = SharedFsEnum.nfs + + @model_validator(mode="after") + def custom_validation(self) -> Self: + self._set_storage_type_default_value() + + if ( + self.storage.type == SharedFsEnum.cephfs + and self.provider == schema.ProviderEnum.local + ): + raise ValueError( + f'storage.type: "{self.storage.type.value}" is not supported for provider: "{self.provider.value}"' + ) + + if ( + self.storage.type == SharedFsEnum.efs + and self.provider != schema.ProviderEnum.aws + ): + raise ValueError( + f'storage.type: "{self.storage.type.value}" is only supported for provider: "{schema.ProviderEnum.aws.value}"' + ) + return self class OutputSchema(schema.Base): @@ -327,6 +400,8 @@ class KubernetesServicesInputVars(schema.Base): realm_id: str node_groups: Dict[str, Dict[str, str]] jupyterhub_logout_redirect_url: str = Field(alias="jupyterhub-logout-redirect-url") + forwardauth_middleware_name: str = _forwardauth_middleware_name + cert_secret_name: Optional[str] = None def _split_docker_image_name(image_name): @@ -339,12 +414,18 @@ class ImageNameTag(schema.Base): tag: str +class RookCephInputVars(schema.Base): + rook_ceph_storage_class_name: None | str = None + + class CondaStoreInputVars(schema.Base): conda_store_environments: Dict[str, CondaEnvironment] = Field( alias="conda-store-environments" ) conda_store_default_namespace: str = Field(alias="conda-store-default-namespace") - conda_store_filesystem_storage: str = Field(alias="conda-store-filesystem-storage") + conda_store_filesystem_storage: float = Field( + alias="conda-store-filesystem-storage" + ) conda_store_object_storage: str = Field(alias="conda-store-object-storage") conda_store_extra_settings: Dict[str, Any] = Field( alias="conda-store-extra-settings" @@ -356,6 +437,11 @@ class CondaStoreInputVars(schema.Base): alias="conda-store-service-token-scopes" ) + @field_validator("conda_store_filesystem_storage", mode="before") + @classmethod + def handle_units(cls, value: Optional[str]) -> float: + return byte_unit_conversion(value, "GiB") + class JupyterhubInputVars(schema.Base): jupyterhub_theme: Dict[str, Any] = Field(alias="jupyterhub-theme") @@ -363,9 +449,12 @@ class JupyterhubInputVars(schema.Base): jupyterlab_default_settings: Dict[str, Any] = Field( alias="jupyterlab-default-settings" ) + jupyterlab_gallery_settings: JupyterLabGallerySettings = Field( + alias="jupyterlab-gallery-settings" + ) initial_repositories: str = Field(alias="initial-repositories") jupyterhub_overrides: List[str] = Field(alias="jupyterhub-overrides") - jupyterhub_stared_storage: str = Field(alias="jupyterhub-shared-storage") + jupyterhub_shared_storage: float = Field(alias="jupyterhub-shared-storage") jupyterhub_shared_endpoint: Optional[str] = Field( alias="jupyterhub-shared-endpoint", default=None ) @@ -377,12 +466,19 @@ class JupyterhubInputVars(schema.Base): jhub_apps_enabled: bool = Field(alias="jhub-apps-enabled") cloud_provider: str = Field(alias="cloud-provider") jupyterlab_preferred_dir: Optional[str] = Field(alias="jupyterlab-preferred-dir") + shared_fs_type: SharedFsEnum + + @field_validator("jupyterhub_shared_storage", mode="before") + @classmethod + def handle_units(cls, value: Optional[str]) -> float: + return byte_unit_conversion(value, "GiB") class DaskGatewayInputVars(schema.Base): dask_worker_image: ImageNameTag = Field(alias="dask-worker-image") dask_gateway_profiles: Dict[str, Any] = Field(alias="dask-gateway-profiles") cloud_provider: str = Field(alias="cloud-provider") + forwardauth_middleware_name: str = _forwardauth_middleware_name class MonitoringInputVars(schema.Base): @@ -462,6 +558,12 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): "*/*": ["viewer"], }, }, + "conda-store-service-account": { + "primary_namespace": "", + "role_bindings": { + "*/*": ["admin"], + }, + }, } # Compound any logout URLs from extensions so they are are logged out in succession @@ -486,8 +588,15 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): realm_id=realm_id, node_groups=stage_outputs["stages/02-infrastructure"]["node_selectors"], jupyterhub_logout_redirect_url=final_logout_uri, + cert_secret_name=( + self.config.certificate.secret_name + if self.config.certificate.type == "existing" + else None + ), ) + rook_ceph_vars = RookCephInputVars() + conda_store_vars = CondaStoreInputVars( conda_store_environments={ k: v.model_dump() for k, v in self.config.environments.items() @@ -507,7 +616,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): jupyterlab_image=_split_docker_image_name( self.config.default_images.jupyterlab ), - jupyterhub_stared_storage=self.config.storage.shared_filesystem, + jupyterhub_shared_storage=self.config.storage.shared_filesystem, jupyterhub_shared_endpoint=jupyterhub_shared_endpoint, cloud_provider=cloud_provider, jupyterhub_profiles=self.config.profiles.model_dump()["jupyterlab"], @@ -523,7 +632,14 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): jhub_apps_enabled=self.config.jhub_apps.enabled, initial_repositories=str(self.config.jupyterlab.initial_repositories), jupyterlab_default_settings=self.config.jupyterlab.default_settings, + jupyterlab_gallery_settings=self.config.jupyterlab.gallery_settings, jupyterlab_preferred_dir=self.config.jupyterlab.preferred_dir, + shared_fs_type=( + # efs is equivalent to nfs in these modules + SharedFsEnum.nfs + if self.config.storage.type == SharedFsEnum.efs + else self.config.storage.type + ), ) dask_gateway_vars = DaskGatewayInputVars( @@ -561,6 +677,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): return { **kubernetes_services_vars.model_dump(by_alias=True), + **rook_ceph_vars.model_dump(by_alias=True), **conda_store_vars.model_dump(by_alias=True), **jupyterhub_vars.model_dump(by_alias=True), **dask_gateway_vars.model_dump(by_alias=True), diff --git a/src/_nebari/stages/kubernetes_services/template/conda-store.tf b/src/_nebari/stages/kubernetes_services/template/conda-store.tf index 904a17e8df..c800f5b1e6 100644 --- a/src/_nebari/stages/kubernetes_services/template/conda-store.tf +++ b/src/_nebari/stages/kubernetes_services/template/conda-store.tf @@ -61,17 +61,19 @@ module "kubernetes-conda-store-server" { services = var.conda-store-service-token-scopes extra-settings = var.conda-store-extra-settings extra-config = var.conda-store-extra-config + conda-store-fs = var.shared_fs_type + + depends_on = [ + module.rook-ceph + ] } -module "conda-store-nfs-mount" { - source = "./modules/kubernetes/nfs-mount" +moved { + from = module.conda-store-nfs-mount + to = module.kubernetes-conda-store-server.module.conda-store-nfs-mount[0] +} - name = "conda-store" - namespace = var.environment - nfs_capacity = var.conda-store-filesystem-storage - nfs_endpoint = module.kubernetes-conda-store-server.endpoint_ip - depends_on = [ - module.kubernetes-conda-store-server - ] +locals { + conda-store-fs = var.shared_fs_type } diff --git a/src/_nebari/stages/kubernetes_services/template/dask_gateway.tf b/src/_nebari/stages/kubernetes_services/template/dask_gateway.tf index b9b0a9c6c3..a47acee8fa 100644 --- a/src/_nebari/stages/kubernetes_services/template/dask_gateway.tf +++ b/src/_nebari/stages/kubernetes_services/template/dask_gateway.tf @@ -30,7 +30,7 @@ module "dask-gateway" { dask-etc-configmap-name = "dask-etc" # environments - conda-store-pvc = module.conda-store-nfs-mount.persistent_volume_claim.name + conda-store-pvc = module.kubernetes-conda-store-server.pvc conda-store-mount = "/home/conda" default-conda-store-namespace = var.conda-store-default-namespace conda-store-api-token = module.kubernetes-conda-store-server.service-tokens.dask-gateway @@ -40,4 +40,11 @@ module "dask-gateway" { profiles = var.dask-gateway-profiles cloud-provider = var.cloud-provider + + forwardauth_middleware_name = var.forwardauth_middleware_name + + depends_on = [ + module.kubernetes-nfs-server, + module.rook-ceph + ] } diff --git a/src/_nebari/stages/kubernetes_services/template/forward-auth.tf b/src/_nebari/stages/kubernetes_services/template/forward-auth.tf index 3cb4e827e2..2d98bf3e6a 100644 --- a/src/_nebari/stages/kubernetes_services/template/forward-auth.tf +++ b/src/_nebari/stages/kubernetes_services/template/forward-auth.tf @@ -5,5 +5,27 @@ module "forwardauth" { external-url = var.endpoint realm_id = var.realm_id - node-group = var.node_groups.general + node-group = var.node_groups.general + forwardauth_middleware_name = var.forwardauth_middleware_name + cert_secret_name = var.cert_secret_name +} + +variable "forwardauth_middleware_name" { + description = "Name of the traefik forward auth middleware" + type = string +} + +variable "cert_secret_name" { + description = "Name of the secret containing the certificate" + type = string +} + +output "forward-auth-middleware" { + description = "middleware name for use with forward auth" + value = module.forwardauth.forward-auth-middleware +} + +output "forward-auth-service" { + description = "middleware name for use with forward auth" + value = module.forwardauth.forward-auth-service } diff --git a/src/_nebari/stages/kubernetes_services/template/jupyterhub.tf b/src/_nebari/stages/kubernetes_services/template/jupyterhub.tf index 4f8bebb9e4..17b6f12411 100644 --- a/src/_nebari/stages/kubernetes_services/template/jupyterhub.tf +++ b/src/_nebari/stages/kubernetes_services/template/jupyterhub.tf @@ -19,7 +19,7 @@ variable "jupyterhub-overrides" { variable "jupyterhub-shared-storage" { description = "JupyterHub shared storage size [GB]" - type = string + type = number } variable "jupyterhub-shared-endpoint" { @@ -54,6 +54,26 @@ variable "jupyterlab-default-settings" { type = map(any) } +variable "jupyterlab-gallery-settings" { + description = "Server-side settings for jupyterlab-gallery extension" + type = object({ + title = optional(string) + destination = optional(string) + hide_gallery_without_exhibits = optional(bool) + exhibits = list(object({ + git = string + title = string + homepage = optional(string) + description = optional(string) + icon = optional(string) + account = optional(string) + token = optional(string) + branch = optional(string) + depth = optional(number) + })) + }) +} + variable "jupyterhub-hub-extraEnv" { description = "Extracted overrides to merge with jupyterhub.hub.extraEnv" type = string @@ -65,8 +85,28 @@ variable "idle-culler-settings" { type = any } +variable "shared_fs_type" { + type = string + description = "Use NFS or Ceph" + + validation { + condition = contains(["cephfs", "nfs"], var.shared_fs_type) + error_message = "Allowed values for input_parameter are \"cephfs\" or \"nfs\"." + } + +} + +locals { + jupyterhub-fs = var.shared_fs_type + jupyterhub-pvc-name = "jupyterhub-${var.environment}-share" + jupyterhub-pvc = local.jupyterhub-fs == "nfs" ? module.jupyterhub-nfs-mount[0].persistent_volume_claim.pvc : module.jupyterhub-cephfs-mount[0].persistent_volume_claim.pvc + enable-nfs-server = var.jupyterhub-shared-endpoint == null && (local.jupyterhub-fs == "nfs" || local.conda-store-fs == "nfs") +} + + + module "kubernetes-nfs-server" { - count = var.jupyterhub-shared-endpoint == null ? 1 : 0 + count = local.enable-nfs-server ? 1 : 0 source = "./modules/kubernetes/nfs-server" @@ -76,20 +116,43 @@ module "kubernetes-nfs-server" { node-group = var.node_groups.general } +moved { + from = module.jupyterhub-nfs-mount + to = module.jupyterhub-nfs-mount[0] +} module "jupyterhub-nfs-mount" { + count = local.jupyterhub-fs == "nfs" ? 1 : 0 source = "./modules/kubernetes/nfs-mount" name = "jupyterhub" namespace = var.environment nfs_capacity = var.jupyterhub-shared-storage nfs_endpoint = var.jupyterhub-shared-endpoint == null ? module.kubernetes-nfs-server.0.endpoint_ip : var.jupyterhub-shared-endpoint + nfs-pvc-name = local.jupyterhub-pvc-name depends_on = [ - module.kubernetes-nfs-server + module.kubernetes-nfs-server, + module.rook-ceph ] } +module "jupyterhub-cephfs-mount" { + count = local.jupyterhub-fs == "cephfs" ? 1 : 0 + source = "./modules/kubernetes/cephfs-mount" + + name = "jupyterhub" + namespace = var.environment + fs_capacity = var.jupyterhub-shared-storage + ceph-pvc-name = local.jupyterhub-pvc-name + + depends_on = [ + module.kubernetes-nfs-server, + module.rook-ceph + ] +} + + module "jupyterhub" { source = "./modules/kubernetes/services/jupyterhub" @@ -104,11 +167,11 @@ module "jupyterhub" { overrides = var.jupyterhub-overrides - home-pvc = module.jupyterhub-nfs-mount.persistent_volume_claim.name + home-pvc = local.jupyterhub-pvc - shared-pvc = module.jupyterhub-nfs-mount.persistent_volume_claim.name + shared-pvc = local.jupyterhub-pvc - conda-store-pvc = module.conda-store-nfs-mount.persistent_volume_claim.name + conda-store-pvc = module.kubernetes-conda-store-server.pvc.name conda-store-mount = "/home/conda" conda-store-environments = var.conda-store-environments default-conda-store-namespace = var.conda-store-default-namespace @@ -149,8 +212,15 @@ module "jupyterhub" { jupyterlab-default-settings = var.jupyterlab-default-settings + jupyterlab-gallery-settings = var.jupyterlab-gallery-settings + jupyterlab-pioneer-enabled = var.jupyterlab-pioneer-enabled jupyterlab-pioneer-log-format = var.jupyterlab-pioneer-log-format jupyterlab-preferred-dir = var.jupyterlab-preferred-dir + + depends_on = [ + module.kubernetes-nfs-server, + module.rook-ceph, + ] } diff --git a/src/_nebari/stages/kubernetes_services/template/jupyterhub_ssh.tf b/src/_nebari/stages/kubernetes_services/template/jupyterhub_ssh.tf index ec49fc6749..dd1726c560 100644 --- a/src/_nebari/stages/kubernetes_services/template/jupyterhub_ssh.tf +++ b/src/_nebari/stages/kubernetes_services/template/jupyterhub_ssh.tf @@ -5,5 +5,10 @@ module "kubernetes-jupyterhub-ssh" { jupyterhub_api_url = module.jupyterhub.internal_jupyterhub_url node-group = var.node_groups.general - persistent_volume_claim = module.jupyterhub-nfs-mount.persistent_volume_claim.name + persistent_volume_claim = local.jupyterhub-pvc + + depends_on = [ + module.kubernetes-nfs-server, + module.rook-ceph + ] } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/main.tf new file mode 100644 index 0000000000..36471ed443 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/main.tf @@ -0,0 +1,21 @@ +resource "kubernetes_persistent_volume_claim" "main" { + metadata { + name = var.ceph-pvc-name + namespace = var.namespace + } + + spec { + access_modes = ["ReadWriteMany"] + storage_class_name = "ceph-filesystem-retain" # kubernetes_storage_class.main.metadata.0.name # Get this from a terraform output + resources { + requests = { + storage = "${var.fs_capacity}Gi" + } + } + } + + # Hack to avoid timeout while CephCluster is being created + timeouts { + create = "10m" + } +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/outputs.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/outputs.tf new file mode 100644 index 0000000000..a0e02b23d1 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/outputs.tf @@ -0,0 +1,11 @@ +output "persistent_volume_claim" { + description = "Name of persistent volume claim" + value = { + pvc = { + name = kubernetes_persistent_volume_claim.main.metadata.0.name + id = kubernetes_persistent_volume_claim.main.metadata.0.uid + } + namespace = var.namespace + kind = "persistentvolumeclaim" + } +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/variables.tf new file mode 100644 index 0000000000..f593c803aa --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/cephfs-mount/variables.tf @@ -0,0 +1,20 @@ +variable "name" { + description = "Prefix name form nfs mount kubernetes resource" + type = string +} + +variable "namespace" { + description = "Namespace to deploy nfs storage mount" + type = string +} + +variable "fs_capacity" { + description = "Capacity of NFS server mount in Gi" + type = number + default = 10 +} + +variable "ceph-pvc-name" { + description = "Name of the persistent volume claim" + type = string +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/main.tf index 6d9eb126ea..564d397d1a 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/main.tf @@ -59,7 +59,19 @@ resource "kubernetes_deployment" "forwardauth-deployment" { node_selector = { "${var.node-group.key}" = var.node-group.value } - + dynamic "volume" { + for_each = var.cert_secret_name == null ? [] : [1] + content { + name = "cert-volume" + secret { + secret_name = var.cert_secret_name + items { + key = "tls.crt" + path = "tls.crt" + } + } + } + } container { # image = "thomseddon/traefik-forward-auth:2.2.0" # Use PR #159 https://github.com/thomseddon/traefik-forward-auth/pull/159 @@ -125,10 +137,26 @@ resource "kubernetes_deployment" "forwardauth-deployment" { value = var.external-url } + dynamic "env" { + for_each = var.cert_secret_name == null ? [] : [1] + content { + name = "SSL_CERT_FILE" + value = "/config/tls.crt" + } + } + port { container_port = 4181 } + dynamic "volume_mount" { + for_each = var.cert_secret_name == null ? [] : [1] + content { + name = "cert-volume" + mount_path = "/config" + read_only = true + } + } } } @@ -144,12 +172,12 @@ resource "kubernetes_manifest" "forwardauth-middleware" { apiVersion = "traefik.containo.us/v1alpha1" kind = "Middleware" metadata = { - name = "traefik-forward-auth" + name = var.forwardauth_middleware_name namespace = var.namespace } spec = { forwardAuth = { - address = "http://forwardauth-service:4181" + address = "http://${kubernetes_service.forwardauth-service.metadata.0.name}:4181" authResponseHeaders = [ "X-Forwarded-User" ] @@ -175,7 +203,7 @@ resource "kubernetes_manifest" "forwardauth-ingressroute" { middlewares = [ { - name = "traefik-forward-auth" + name = kubernetes_manifest.forwardauth-middleware.manifest.metadata.name namespace = var.namespace } ] diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/outputs.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/outputs.tf new file mode 100644 index 0000000000..9280da29e9 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/outputs.tf @@ -0,0 +1,13 @@ +output "forward-auth-middleware" { + description = "middleware name for use with forward auth" + value = { + name = kubernetes_manifest.forwardauth-middleware.manifest.metadata.name + } +} + +output "forward-auth-service" { + description = "middleware name for use with forward auth" + value = { + name = kubernetes_service.forwardauth-service.metadata.0.name + } +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/variables.tf index 3674b1db75..ae53c5b3a1 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/forwardauth/variables.tf @@ -26,3 +26,13 @@ variable "node-group" { value = string }) } + +variable "forwardauth_middleware_name" { + description = "Name of the traefik forward auth middleware" + type = string +} + +variable "cert_secret_name" { + description = "Name of the secret containing the certificate" + type = string +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/main.tf index 4534be7b21..81ad1797e6 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/main.tf @@ -12,7 +12,7 @@ resource "kubernetes_persistent_volume" "main" { } spec { capacity = { - storage = var.nfs_capacity + storage = "${var.nfs_capacity}Gi" } storage_class_name = kubernetes_storage_class.main.metadata.0.name access_modes = ["ReadWriteMany"] @@ -28,7 +28,7 @@ resource "kubernetes_persistent_volume" "main" { resource "kubernetes_persistent_volume_claim" "main" { metadata { - name = "${var.name}-${var.namespace}-share" + name = var.nfs-pvc-name namespace = var.namespace } @@ -37,7 +37,7 @@ resource "kubernetes_persistent_volume_claim" "main" { storage_class_name = kubernetes_storage_class.main.metadata.0.name resources { requests = { - storage = var.nfs_capacity + storage = "${var.nfs_capacity}Gi" } } } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/outputs.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/outputs.tf index d5318cf5be..a0e02b23d1 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/outputs.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/outputs.tf @@ -1,7 +1,10 @@ output "persistent_volume_claim" { description = "Name of persistent volume claim" value = { - name = kubernetes_persistent_volume_claim.main.metadata.0.name + pvc = { + name = kubernetes_persistent_volume_claim.main.metadata.0.name + id = kubernetes_persistent_volume_claim.main.metadata.0.uid + } namespace = var.namespace kind = "persistentvolumeclaim" } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/variables.tf index 88ddf6f32f..fe7294b53b 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-mount/variables.tf @@ -9,12 +9,17 @@ variable "namespace" { } variable "nfs_capacity" { - description = "Capacity of NFS server mount" - type = string - default = "10Gi" + description = "Capacity of NFS server mount in Gi" + type = number + default = 10 } variable "nfs_endpoint" { description = "Endpoint of nfs server" type = string } + +variable "nfs-pvc-name" { + description = "Name of the persistent volume claim" + type = string +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/main.tf index 1032d15ad7..95120343e2 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/main.tf @@ -8,7 +8,7 @@ resource "kubernetes_persistent_volume_claim" "main" { access_modes = ["ReadWriteOnce"] resources { requests = { - storage = var.nfs_capacity + storage = "${var.nfs_capacity}Gi" } } } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/variables.tf index 21e41a7e90..63025465b3 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/nfs-server/variables.tf @@ -9,9 +9,9 @@ variable "namespace" { } variable "nfs_capacity" { - description = "Capacity of NFS server deployment" - type = string - default = "10Gi" + description = "Capacity of NFS server deployment in Gi" + type = number + default = 10 } variable "node-group" { diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/config/conda_store_config.py b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/config/conda_store_config.py index 6ed6232ba8..ad9b79843a 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/config/conda_store_config.py +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/config/conda_store_config.py @@ -1,6 +1,12 @@ +import dataclasses import json import logging +import re import tempfile +import typing +import urllib +import urllib.parse +import urllib.request from pathlib import Path import requests @@ -17,7 +23,6 @@ def conda_store_config(path="/var/lib/conda-store/config.json"): config = conda_store_config() - # ================================== # conda-store settings # ================================== @@ -49,11 +54,15 @@ def conda_store_config(path="/var/lib/conda-store/config.json"): "conda-forge", "https://repo.anaconda.com/pkgs/main", ] +c.RBACAuthorizationBackend.role_mappings_version = 2 # ================================== # server settings # ================================== c.CondaStoreServer.log_level = logging.INFO +c.CondaStoreServer.log_format = ( + "%(asctime)s %(levelname)9s %(name)s:%(lineno)4s: %(message)s" +) c.CondaStoreServer.enable_ui = True c.CondaStoreServer.enable_api = True c.CondaStoreServer.enable_registry = True @@ -64,7 +73,6 @@ def conda_store_config(path="/var/lib/conda-store/config.json"): # This MUST start with `/` c.CondaStoreServer.url_prefix = "/conda-store" - # ================================== # auth settings # ================================== @@ -83,10 +91,280 @@ def conda_store_config(path="/var/lib/conda-store/config.json"): c.GenericOAuthAuthentication.user_data_key = "preferred_username" c.GenericOAuthAuthentication.tls_verify = False +CONDA_STORE_ROLE_PERMISSIONS_ORDER = ["viewer", "developer", "admin"] + + +@dataclasses.dataclass +class CondaStoreNamespaceRole: + namespace: str + role: str + + +@dataclasses.dataclass +class KeyCloakCondaStoreRoleScopes: + scopes: str + log: logging.Logger + + def _validate_role(self, role): + valid = role in CONDA_STORE_ROLE_PERMISSIONS_ORDER + self.log.info(f"role: {role} is {'valid' if valid else 'invalid'}") + return valid + + def parse_role_and_namespace( + self, text + ) -> typing.Optional[CondaStoreNamespaceRole]: + # The regex pattern + pattern = r"^(\w+)!namespace=([^!]+)$" + + # Perform the regex search + match = re.search(pattern, text) + + # Extract the permission and namespace if there is a match + if match and self._validate_role(match.group(1)): + return CondaStoreNamespaceRole( + namespace=match.group(2), role=match.group(1) + ) + else: + return None + + def parse_scope(self) -> typing.List[CondaStoreNamespaceRole]: + """Parsed scopes from keycloak role's attribute and returns a list of role/namespace + if scopes' syntax is valid otherwise return [] + + Example: + Given scopes as "viewer!namespace=scipy,admin!namespace=pycon", the function will + return [{"role": "viewer", "namespace": "scipy"}, {"role": "admin", "namespace": "pycon"}] + """ + if not self.scopes: + self.log.info(f"No scope found: {self.scopes}, skipping role") + return [] + scope_list = self.scopes.split(",") + parsed_scopes = [] + self.log.info(f"Scopes to parse: {scope_list}") + for scope_text in scope_list: + parsed_scope = self.parse_role_and_namespace(scope_text) + parsed_scopes.append(parsed_scope) + if not parsed_scope: + self.log.info(f"Unable to parse: {scope_text}, skipping keycloak role") + return [] + return parsed_scopes + class KeyCloakAuthentication(GenericOAuthAuthentication): + conda_store_api_url = f"https://{config['external-url']}/conda-store/api/v1" + access_token_url = config["token_url_internal"] + realm_api_url = config["realm_api_url_internal"] + service_account_token = config["service-tokens-mapping"][ + "conda-store-service-account" + ] + + def _get_conda_store_client_id(self, token: str) -> str: + # Get the clients list to find the "id" of "conda-store" client. + self.log.info("Getting conda store client id") + clients_data = self._fetch_api(endpoint="clients/", token=token) + conda_store_clients = [ + client for client in clients_data if client["clientId"] == "conda_store" + ] + self.log.info(f"conda store clients: {conda_store_clients}") + assert len(conda_store_clients) == 1 + conda_store_client_id = conda_store_clients[0]["id"] + return conda_store_client_id + + async def _delete_conda_store_roles(self, request, namespace: str, username: str): + self.log.info( + f"Delete all conda-store roles on namespace: {namespace} for user: {username}" + ) + conda_store = await get_conda_store(request) + with conda_store.session_factory() as db: + api.delete_namespace_role(db, namespace, other=username) + db.commit() + + async def _create_conda_store_role( + self, request, namespace_role: CondaStoreNamespaceRole, username: str + ): + self.log.info( + f"Creating conda-store roles on namespace: {namespace_role.namespace} for user: {username}" + ) + conda_store = await get_conda_store(request) + with conda_store.session_factory() as db: + api.create_namespace_role( + db, namespace_role.namespace, username, namespace_role.role + ) + db.commit() + + def _get_keycloak_token(self) -> str: + body = urllib.parse.urlencode( + { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "client_credentials", + } + ) + self.log.info(f"Getting token from access token url: {self.access_token_url}") + req = urllib.request.Request(self.access_token_url, data=body.encode()) + response = urllib.request.urlopen(req) + data = json.loads(response.read()) + return data["access_token"] # type: ignore[no-any-return] + + def _fetch_api(self, endpoint: str, token: str): + request_url = f"{self.realm_api_url}/{endpoint}" + req = urllib.request.Request( + request_url, + method="GET", + headers={"Authorization": f"Bearer {token}"}, + ) + self.log.info(f"Making request to: {request_url}") + with urllib.request.urlopen(req) as response: + data = json.loads(response.read()) + return data + + async def _remove_current_bindings(self, request, username): + """Remove current roles for the user to make sure only the roles defined in + keycloak are applied: + - to avoid inconsistency in user roles + - single source of truth + - roles that are added in keycloak and then later removed from keycloak are actually removed from conda-store. + """ + entity_bindings = self._get_current_entity_bindings(username) + self.log.info("Remove current role bindings for the user") + for entity, role in entity_bindings.items(): + if entity not in {"default/*", "filesystem/*"}: + namespace = entity.split("/")[0] + self.log.info( + f"Removing current role {role} on namespace {namespace} " + f"for user {username}" + ) + await self._delete_conda_store_roles(request, namespace, username) + + async def _apply_roles_from_keycloak(self, request, user_data): + token = self._get_keycloak_token() + conda_store_client_id = self._get_conda_store_client_id(token) + conda_store_client_roles = self._get_conda_store_client_roles_for_user( + user_data["sub"], conda_store_client_id, token + ) + await self._remove_current_bindings(request, user_data["preferred_username"]) + await self._apply_conda_store_roles_from_keycloak( + request, conda_store_client_roles, user_data["preferred_username"] + ) + + def _filter_duplicate_namespace_roles_with_max_permissions( + self, namespace_roles: typing.List[CondaStoreNamespaceRole] + ): + """Filter duplicate roles in keycloak such that to apply only the one with the highest + permissions. + + Example: + role 1: namespace: foo, role: viewer + role 2: namespace: foo, role: admin + We need to apply only the role 2 as that one has higher permissions. + """ + self.log.info("Filtering duplicate roles for same namespace") + namespace_role_mapping: typing.Dict[str:CondaStoreNamespaceRole] = {} + for namespace_role in namespace_roles: + namespace = namespace_role.namespace + new_role = namespace_role.role + + existing_role: CondaStoreNamespaceRole = namespace_role_mapping.get( + namespace + ) + if not existing_role: + # Add if not already added + namespace_role_mapping[namespace] = namespace_role + else: + # Only add if the permissions of this role is higher than existing + new_role_priority = CONDA_STORE_ROLE_PERMISSIONS_ORDER.index(new_role) + existing_role_priority = CONDA_STORE_ROLE_PERMISSIONS_ORDER.index( + existing_role.role + ) + if new_role_priority > existing_role_priority: + namespace_role_mapping[namespace] = new_role + return list(namespace_role_mapping.values()) + + def _get_permissions_from_keycloak_role( + self, keycloak_role + ) -> typing.List[CondaStoreNamespaceRole]: + self.log.info(f"Getting permissions from keycloak role: {keycloak_role}") + role_attributes = keycloak_role["attributes"] + # scopes returns a list with a value say ["viewer!namespace=pycon,developer!namespace=scipy"] + scopes = role_attributes.get("scopes", [""])[0] + k_cstore_scopes = KeyCloakCondaStoreRoleScopes(scopes=scopes, log=self.log) + return k_cstore_scopes.parse_scope() + + async def _apply_conda_store_roles_from_keycloak( + self, request, conda_store_client_roles, username + ): + self.log.info( + f"Apply conda store roles from keycloak roles: {conda_store_client_roles}, user: {username}" + ) + role_permissions: typing.List[CondaStoreNamespaceRole] = [] + for conda_store_client_role in conda_store_client_roles: + role_permissions += self._get_permissions_from_keycloak_role( + conda_store_client_role + ) + + self.log.info("Filtering duplicate namespace role for max permissions") + filtered_namespace_role: typing.List[CondaStoreNamespaceRole] = ( + self._filter_duplicate_namespace_roles_with_max_permissions( + role_permissions + ) + ) + self.log.info(f"Final role permissions to apply: {filtered_namespace_role}") + for namespace_role in filtered_namespace_role: + if namespace_role.namespace.lower() == username.lower(): + self.log.info("Role for given user's namespace, skipping") + continue + try: + await self._delete_conda_store_roles( + request, namespace_role.namespace, username + ) + await self._create_conda_store_role(request, namespace_role, username) + except ValueError as e: + self.log.error( + f"Failed to add permissions for namespace: {namespace_role.namespace} to user: {username}" + ) + self.log.exception(e) + + def _get_keycloak_conda_store_roles_with_attributes( + self, roles: dict, client_id: str, token: str + ): + """This fetches all roles by id to fetch their attributes.""" + roles_rich = [] + for role in roles: + # If this takes too much time, which isn't the case right now, we can + # also do multi-threaded requests + role_rich = self._fetch_api( + endpoint=f"roles-by-id/{role['id']}?client={client_id}", token=token + ) + roles_rich.append(role_rich) + return roles_rich + + def _get_conda_store_client_roles_for_user( + self, user_id, conda_store_client_id, token + ): + """Get roles for the client named 'conda-store' for the given user_id.""" + self.log.info( + f"Get conda store client roles for user: {user_id}, conda_store_client_id: {conda_store_client_id}" + ) + user_roles = self._fetch_api( + endpoint=f"users/{user_id}/role-mappings/clients/{conda_store_client_id}/composite", + token=token, + ) + client_roles_rich = self._get_keycloak_conda_store_roles_with_attributes( + user_roles, client_id=conda_store_client_id, token=token + ) + self.log.info(f"conda store client roles: {client_roles_rich}") + return client_roles_rich + + def _get_current_entity_bindings(self, username): + entity = schema.AuthenticationToken( + primary_namespace=username, role_bindings={} + ) + self.log.info(f"entity: {entity}") + entity_bindings = self.authorization.get_entity_bindings(entity) + self.log.info(f"current entity_bindings: {entity_bindings}") + return entity_bindings + async def authenticate(self, request): - # 1. using the callback_url code and state in request oauth_access_token = self._get_oauth_token(request) if oauth_access_token is None: return None # authentication failed @@ -98,9 +376,14 @@ async def authenticate(self, request): ) response.raise_for_status() user_data = response.json() - username = user_data["preferred_username"] + try: + await self._apply_roles_from_keycloak(request, user_data=user_data) + except Exception as e: + self.log.error("Adding roles from keycloak failed") + self.log.exception(e) + # superadmin gets access to everything if "conda_store_superadmin" in user_data.get("roles", []): return schema.AuthenticationToken( @@ -119,7 +402,9 @@ async def authenticate(self, request): if role in role_mappings } default_namespace = config["default-namespace"] + self.log.info(f"default_namespace: {default_namespace}") namespaces = {username, "global", default_namespace} + self.log.info(f"namespaces: {namespaces}") role_bindings = { f"{username}/*": {"admin"}, f"{default_namespace}/*": {"viewer"}, diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/output.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/output.tf index 571e75ef07..a00e0d2c80 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/output.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/output.tf @@ -17,3 +17,8 @@ output "service-tokens" { description = "Service tokens for conda-store" value = { for k, _ in var.services : k => base64encode(random_password.conda_store_service_token[k].result) } } + +output "pvc" { + description = "Shared PVC name for conda-store" + value = local.shared-pvc +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/server.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/server.tf index ab9edd87e8..8a29bc2d41 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/server.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/server.tf @@ -13,22 +13,28 @@ resource "kubernetes_secret" "conda-store-secret" { data = { "config.json" = jsonencode({ - external-url = var.external-url - minio-username = module.minio.root_username - minio-password = module.minio.root_password - minio-service = module.minio.service - redis-password = module.redis.root_password - redis-service = module.redis.service - postgres-username = module.postgresql.root_username - postgres-password = module.postgresql.root_password - postgres-service = module.postgresql.service - openid-config = module.conda-store-openid-client.config - extra-settings = var.extra-settings - extra-config = var.extra-config - default-namespace = var.default-namespace-name + external-url = var.external-url + minio-username = module.minio.root_username + minio-password = module.minio.root_password + minio-service = module.minio.service + redis-password = module.redis.root_password + redis-service = module.redis.service + postgres-username = module.postgresql.root_username + postgres-password = module.postgresql.root_password + postgres-service = module.postgresql.service + openid-config = module.conda-store-openid-client.config + extra-settings = var.extra-settings + extra-config = var.extra-config + default-namespace = var.default-namespace-name + token_url_internal = "http://keycloak-http.${var.namespace}.svc/auth/realms/${var.realm_id}/protocol/openid-connect/token" + realm_api_url_internal = "http://keycloak-http.${var.namespace}.svc/auth/admin/realms/${var.realm_id}" service-tokens = { for service, value in var.services : base64encode(random_password.conda_store_service_token[service].result) => value } + # So that the mapping can be used in conda-store config itself + service-tokens-mapping = { + for service, _ in var.services : service => base64encode(random_password.conda_store_service_token[service].result) + } extra-settings = var.extra-settings extra-config = var.extra-config }) @@ -63,6 +69,10 @@ module "conda-store-openid-client" { callback-url-paths = [ "https://${var.external-url}/conda-store/oauth_callback" ] + service-accounts-enabled = true + service-account-roles = [ + "view-realm", "view-users", "view-clients" + ] } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/shared-pvc.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/shared-pvc.tf new file mode 100644 index 0000000000..cb6809d265 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/shared-pvc.tf @@ -0,0 +1,36 @@ +module "conda-store-nfs-mount" { + count = var.conda-store-fs == "nfs" ? 1 : 0 + source = "../../../../modules/kubernetes/nfs-mount" + + name = "conda-store" + namespace = var.namespace + nfs_capacity = var.nfs_capacity + nfs_endpoint = kubernetes_service.nfs.spec.0.cluster_ip + nfs-pvc-name = local.conda-store-pvc-name + + depends_on = [ + kubernetes_deployment.worker, + ] +} + + +locals { + conda-store-pvc-name = "conda-store-${var.namespace}-share" + new-pvc-name = "nebari-conda-store-storage" + create-pvc = var.conda-store-fs == "nfs" + enable-nfs-server-worker = var.conda-store-fs == "nfs" + pvc-name = var.conda-store-fs == "nfs" ? local.new-pvc-name : local.conda-store-pvc-name + shared-pvc = var.conda-store-fs == "nfs" ? module.conda-store-nfs-mount[0].persistent_volume_claim.pvc : module.conda-store-cephfs-mount[0].persistent_volume_claim.pvc +} + + + +module "conda-store-cephfs-mount" { + count = var.conda-store-fs == "cephfs" ? 1 : 0 + source = "../../../../modules/kubernetes/cephfs-mount" + + name = "conda-store" + namespace = var.namespace + fs_capacity = var.nfs_capacity # conda-store-filesystem-storage + ceph-pvc-name = local.conda-store-pvc-name +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/variables.tf index fd5ff0fa2f..d90e1650de 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/variables.tf @@ -76,3 +76,13 @@ variable "services" { description = "Map of services tokens and scopes for conda-store" type = map(any) } + +variable "conda-store-fs" { + type = string + description = "Use NFS or Ceph" + + validation { + condition = contains(["cephfs", "nfs"], var.conda-store-fs) + error_message = "Allowed values for input_parameter are \"cephfs\", or \"nfs\"." + } +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/worker.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/worker.tf index c3e725dbea..9410a4cc65 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/worker.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/conda-store/worker.tf @@ -28,6 +28,8 @@ resource "kubernetes_service" "nfs" { resource "kubernetes_persistent_volume_claim" "main" { + count = local.create-pvc ? 1 : 0 + metadata { name = "${var.name}-conda-store-storage" namespace = var.namespace @@ -37,7 +39,7 @@ resource "kubernetes_persistent_volume_claim" "main" { access_modes = ["ReadWriteOnce"] resources { requests = { - storage = var.nfs_capacity + storage = "${var.nfs_capacity}Gi" } } } @@ -134,32 +136,35 @@ resource "kubernetes_deployment" "worker" { } } - container { - name = "nfs-server" - image = "gcr.io/google_containers/volume-nfs:0.8" + dynamic "container" { + for_each = local.enable-nfs-server-worker ? [1] : [] + content { + name = "nfs-server" + image = "gcr.io/google_containers/volume-nfs:0.8" - port { - name = "nfs" - container_port = 2049 - } + port { + name = "nfs" + container_port = 2049 + } - port { - name = "mountd" - container_port = 20048 - } + port { + name = "mountd" + container_port = 20048 + } - port { - name = "rpcbind" - container_port = 111 - } + port { + name = "rpcbind" + container_port = 111 + } - security_context { - privileged = true - } + security_context { + privileged = true + } - volume_mount { - mount_path = "/exports" - name = "storage" + volume_mount { + mount_path = "/exports" + name = "storage" + } } } @@ -191,7 +196,7 @@ resource "kubernetes_deployment" "worker" { # directly reference the pvc may no longer be issue in # future # claim_name = kubernetes_persistent_volume_claim.main.metadata.0.name - claim_name = "${var.name}-conda-store-storage" + claim_name = local.pvc-name } } security_context { @@ -201,4 +206,19 @@ resource "kubernetes_deployment" "worker" { } } } + depends_on = [ + module.conda-store-cephfs-mount + ] + + lifecycle { + replace_triggered_by = [ + null_resource.pvc + ] + } +} + +resource "null_resource" "pvc" { + triggers = { + pvc = var.conda-store-fs + } } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py index 2219d14e56..c58e3aa90d 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py @@ -129,7 +129,7 @@ def base_node_group(options): default_node_group if worker_node_group is None else worker_node_group ) - # check `schduler_extra_pod_config` first + # check `scheduler_extra_pod_config` first scheduler_node_group = ( config["profiles"][options.profile] .get("scheduler_extra_pod_config", {}) diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/gateway.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/gateway.tf index 62265b350b..4c1d638a4c 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/gateway.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/gateway.tf @@ -18,7 +18,7 @@ resource "kubernetes_secret" "gateway" { cluster-image = var.cluster-image profiles = var.profiles default-conda-store-namespace = var.default-conda-store-namespace - conda-store-pvc = var.conda-store-pvc + conda-store-pvc = var.conda-store-pvc.name conda-store-mount = var.conda-store-mount worker-node-group = var.worker-node-group conda-store-api-token = var.conda-store-api-token @@ -170,7 +170,7 @@ resource "kubernetes_deployment" "gateway" { volume { name = "conda-store" persistent_volume_claim { - claim_name = var.conda-store-pvc + claim_name = var.conda-store-pvc.name } } @@ -245,4 +245,14 @@ resource "kubernetes_deployment" "gateway" { } } } + + lifecycle { + replace_triggered_by = [null_resource.conda-store-pvc] + } +} + +resource "null_resource" "conda-store-pvc" { + triggers = { + pvc = var.conda-store-pvc.id + } } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/middleware.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/middleware.tf index 01680129b8..389127d06e 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/middleware.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/middleware.tf @@ -32,7 +32,7 @@ resource "kubernetes_manifest" "chain-middleware" { chain = { middlewares = [ { - name = "traefik-forward-auth" + name = var.forwardauth_middleware_name namespace = var.namespace }, { diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/variables.tf index 7f8a4aa978..121405a322 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/variables.tf @@ -177,7 +177,10 @@ variable "profiles" { variable "conda-store-pvc" { description = "Name for persistent volume claim to use for conda-store directory" - type = string + type = object({ + name = string + id = string + }) } variable "conda-store-mount" { @@ -204,3 +207,7 @@ variable "cloud-provider" { description = "Name of the cloud provider to deploy to." type = string } + +variable "forwardauth_middleware_name" { + type = string +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/sftp.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/sftp.tf index 87f70f9a58..4f53567e4c 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/sftp.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/sftp.tf @@ -76,7 +76,7 @@ resource "kubernetes_deployment" "jupyterhub-sftp" { volume { name = "home" persistent_volume_claim { - claim_name = var.persistent_volume_claim + claim_name = var.persistent_volume_claim.name } } @@ -131,4 +131,16 @@ resource "kubernetes_deployment" "jupyterhub-sftp" { } } } + lifecycle { + replace_triggered_by = [ + null_resource.pvc, + ] + } +} + +# hack to force the deployment to update when the pvc changes +resource "null_resource" "pvc" { + triggers = { + pvc = var.persistent_volume_claim.id + } } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/variables.tf index 430eb14888..d90b1ddad9 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub-ssh/variables.tf @@ -30,7 +30,7 @@ variable "jupyterhub-ssh-image" { }) default = { name = "quay.io/jupyterhub-ssh/ssh" - tag = "0.0.1-0.dev.git.136.ha610981" + tag = "0.0.1-0.dev.git.149.he5107a4" } } @@ -48,5 +48,8 @@ variable "jupyterhub-sftp-image" { variable "persistent_volume_claim" { description = "name of persistent volume claim to mount" - type = string + type = object({ + name = string + id = string + }) } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/configmaps.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/configmaps.tf index 4b8f9145b9..bfee219e9e 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/configmaps.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/configmaps.tf @@ -47,6 +47,13 @@ resource "local_file" "jupyter_jupyterlab_pioneer_config_py" { } } +resource "local_sensitive_file" "jupyter_gallery_config_json" { + content = jsonencode({ + "GalleryManager" = var.jupyterlab-gallery-settings + }) + filename = "${path.module}/files/jupyter/jupyter_gallery_config.json" +} + resource "local_file" "overrides_json" { content = jsonencode(local.jupyterlab-overrides-json-object) @@ -70,7 +77,8 @@ resource "kubernetes_config_map" "etc-ipython" { locals { etc-jupyter-config-data = merge( { - "jupyter_server_config.py" = local_file.jupyter_server_config_py.content, + "jupyter_server_config.py" = local_file.jupyter_server_config_py.content, + "jupyter_gallery_config.json" = local_sensitive_file.jupyter_gallery_config_json.content, }, var.jupyterlab-pioneer-enabled ? { # quotes are must here, as terraform would otherwise think py is a property of @@ -89,7 +97,8 @@ locals { resource "kubernetes_config_map" "etc-jupyter" { depends_on = [ local_file.jupyter_server_config_py, - local_file.jupyter_jupyterlab_pioneer_config_py + local_file.jupyter_jupyterlab_pioneer_config_py, + local_sensitive_file.jupyter_gallery_config_json ] metadata { diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyter/jupyter_server_config.py.tpl b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyter/jupyter_server_config.py.tpl index d5e089dfa3..f8206a3ec9 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyter/jupyter_server_config.py.tpl +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyter/jupyter_server_config.py.tpl @@ -4,6 +4,13 @@ # Extra config available at: # https://zero-to-jupyterhub.readthedocs.io/en/1.x/jupyterhub/customizing/user-management.html#culling-user-pods + +# Refuse to serve content from handlers missing authentication guards, unless +# the handler is explicitly allow-listed with `@allow_unauthenticated`; this +# prevents accidental exposure of information by extensions installed in the +# single-user server when their handlers are missing authentication guards. +c.ServerApp.allow_unauthenticated_access = False + # Enable Show Hidden Files menu option in View menu c.ContentsManager.allow_hidden = True c.FileContentsManager.allow_hidden = True diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/02-spawner.py b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/02-spawner.py index c3934aad05..aa2153dc29 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/02-spawner.py +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/02-spawner.py @@ -1,3 +1,5 @@ +import inspect + import kubernetes.client.models from tornado import gen @@ -72,7 +74,6 @@ def service_for_jhub_apps(name, url): "url": url, "external": True, }, - "oauth_no_confirm": True, } c.JupyterHub.services.extend( @@ -81,9 +82,14 @@ def service_for_jhub_apps(name, url): service_for_jhub_apps(name="Users", url="/auth/admin/nebari/console/"), service_for_jhub_apps(name="Environments", url="/conda-store"), service_for_jhub_apps(name="Monitoring", url="/monitoring"), - service_for_jhub_apps(name="VSCode", url="/user/[USER]/vscode"), ] ) c.JupyterHub.template_paths = theme_template_paths - c = install_jhub_apps(c, spawner_to_subclass=KubeSpawner) + + kwargs = {} + jhub_apps_signature = inspect.signature(install_jhub_apps) + if "oauth_no_confirm" in jhub_apps_signature.parameters: + kwargs["oauth_no_confirm"] = True + + c = install_jhub_apps(c, spawner_to_subclass=KubeSpawner, **kwargs) diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/03-profiles.py b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/03-profiles.py index ddbc3b6be2..22193e79dc 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/03-profiles.py +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/03-profiles.py @@ -48,10 +48,16 @@ def base_profile_home_mounts(username): } MKDIR_OWN_DIRECTORY = ( - "mkdir -p /mnt/{path} && chmod 777 /mnt/{path} && cp -r /etc/skel/. /mnt/{path}" + "mkdir -p /mnt/{path} && chmod 777 /mnt/{path} && " + # Copy skel files/folders not starting with '..' to user home directory. + # Filtering out ..* removes some unneeded folders (k8s configmap mount implementation details). + "find /etc/skel/. -maxdepth 1 -not -name '.' -not -name '..*' -exec " + "cp -rL {escaped_brackets} /mnt/{path} \;" ) command = MKDIR_OWN_DIRECTORY.format( - path=pvc_home_mount_path.format(username=username) + # have to escape the brackets since this string will be formatted later by KubeSpawner + escaped_brackets="{{}}", + path=pvc_home_mount_path.format(username=username), ) init_containers = [ { @@ -426,7 +432,10 @@ def profile_argo_token(groups): "ARGO_BASE_HREF": "/argo", "ARGO_SERVER": f"{domain}:443", "ARGO_NAMESPACE": namespace, - "ARGO_TOKEN": { + "ARGO_TOKEN": "Bearer $(HERA_TOKEN)", + "ARGO_HTTP1": "true", # Maybe due to traefik config, but `argo list` returns 404 without this set. Try removing after upgrading argo past v3.4.4. + # Hera token is needed for versions of hera released before https://github.com/argoproj-labs/hera/pull/1053 is merged + "HERA_TOKEN": { "valueFrom": { "secretKeyRef": { "name": f"{argo_sa}.service-account-token", diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/04-auth.py b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/04-auth.py new file mode 100644 index 0000000000..cbd20a4418 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/jupyterhub/04-auth.py @@ -0,0 +1,238 @@ +import json +import os +import time +import urllib +from functools import reduce + +from jupyterhub import scopes +from jupyterhub.traitlets import Callable +from oauthenticator.generic import GenericOAuthenticator +from traitlets import Bool, Unicode, Union + + +class KeyCloakOAuthenticator(GenericOAuthenticator): + """ + Since `oauthenticator` 16.3 `GenericOAuthenticator` supports group management. + This subclass adds role management on top of it, building on the new `manage_roles` + feature added in JupyterHub 5.0 (https://github.com/jupyterhub/jupyterhub/pull/4748). + """ + + claim_roles_key = Union( + [Unicode(os.environ.get("OAUTH2_ROLES_KEY", "groups")), Callable()], + config=True, + help="""As `claim_groups_key` but for roles.""", + ) + + realm_api_url = Unicode( + config=True, help="""The keycloak REST API URL for the realm.""" + ) + + reset_managed_roles_on_startup = Bool(True) + + async def update_auth_model(self, auth_model): + """Updates and returns the auth_model dict. + This function is called every time a user authenticates with JupyterHub, as in + every time a user login to Nebari. + + It will fetch the roles and their corresponding scopes from keycloak + and return updated auth model which will updates roles/scopes for the + user. When a user's roles/scopes are updated, they take in-affect only + after they log in to Nebari. + """ + start = time.time() + self.log.info("Updating user auth model") + auth_model = await super().update_auth_model(auth_model) + user_id = auth_model["auth_state"]["oauth_user"]["sub"] + token = await self._get_token() + + jupyterhub_client_id = await self._get_jupyterhub_client_id(token=token) + user_info = auth_model["auth_state"][self.user_auth_state_key] + user_roles_from_claims = self._get_user_roles(user_info=user_info) + keycloak_api_call_start = time.time() + user_roles = await self._get_client_roles_for_user( + user_id=user_id, client_id=jupyterhub_client_id, token=token + ) + user_roles_rich = await self._get_roles_with_attributes( + roles=user_roles, client_id=jupyterhub_client_id, token=token + ) + keycloak_api_call_time_taken = time.time() - keycloak_api_call_start + user_roles_rich_names = {role["name"] for role in user_roles_rich} + user_roles_non_jhub_client = [ + {"name": role} + for role in user_roles_from_claims + if role in (user_roles_from_claims - user_roles_rich_names) + ] + auth_model["roles"] = [ + { + "name": role["name"], + "description": role.get("description"), + "scopes": self._get_scope_from_role(role), + } + for role in [*user_roles_rich, *user_roles_non_jhub_client] + ] + # note: because the roles check is comprehensive, we need to re-add the admin and user roles + if auth_model["admin"]: + auth_model["roles"].append({"name": "admin"}) + if await self.check_allowed(auth_model["name"], auth_model): + auth_model["roles"].append({"name": "user"}) + execution_time = time.time() - start + self.log.info( + f"Auth model update complete, time taken: {execution_time}s " + f"time taken for keycloak api call: {keycloak_api_call_time_taken}s " + f"delta between full execution and keycloak call: {execution_time - keycloak_api_call_time_taken}s" + ) + return auth_model + + async def _get_jupyterhub_client_roles(self, jupyterhub_client_id, token): + """Get roles for the client named 'jupyterhub'.""" + # Includes roles like "jupyterhub_admin", "jupyterhub_developer", "dask_gateway_developer" + + client_roles = await self._fetch_api( + endpoint=f"clients/{jupyterhub_client_id}/roles", token=token + ) + client_roles_rich = await self._get_roles_with_attributes( + client_roles, client_id=jupyterhub_client_id, token=token + ) + return client_roles_rich + + async def _get_jupyterhub_client_id(self, token): + # Get the clients list to find the "id" of "jupyterhub" client. + clients_data = await self._fetch_api(endpoint="clients/", token=token) + jupyterhub_clients = [ + client for client in clients_data if client["clientId"] == "jupyterhub" + ] + assert len(jupyterhub_clients) == 1 + jupyterhub_client_id = jupyterhub_clients[0]["id"] + return jupyterhub_client_id + + async def load_managed_roles(self): + self.log.info("Loading managed roles") + if not self.manage_roles: + raise ValueError( + "Managed roles can only be loaded when `manage_roles` is True" + ) + token = await self._get_token() + jupyterhub_client_id = await self._get_jupyterhub_client_id(token=token) + client_roles_rich = await self._get_jupyterhub_client_roles( + jupyterhub_client_id=jupyterhub_client_id, token=token + ) + # Includes roles like "default-roles-nebari", "offline_access", "uma_authorization" + realm_roles = await self._fetch_api(endpoint="roles", token=token) + roles = { + role["name"]: { + "name": role["name"], + "description": role["description"], + "scopes": self._get_scope_from_role(role), + } + for role in [*realm_roles, *client_roles_rich] + } + # we could use either `name` (e.g. "developer") or `path` ("/developer"); + # since the default claim key returns `path`, it seems preferable. + group_name_key = "path" + for realm_role in realm_roles: + role_name = realm_role["name"] + role = roles[role_name] + # fetch role assignments to groups + groups = await self._fetch_api(f"roles/{role_name}/groups", token=token) + role["groups"] = [group[group_name_key] for group in groups] + # fetch role assignments to users + users = await self._fetch_api(f"roles/{role_name}/users", token=token) + role["users"] = [user["username"] for user in users] + for client_role in client_roles_rich: + role_name = client_role["name"] + role = roles[role_name] + # fetch role assignments to groups + groups = await self._fetch_api( + f"clients/{jupyterhub_client_id}/roles/{role_name}/groups", token=token + ) + role["groups"] = [group[group_name_key] for group in groups] + # fetch role assignments to users + users = await self._fetch_api( + f"clients/{jupyterhub_client_id}/roles/{role_name}/users", token=token + ) + role["users"] = [user["username"] for user in users] + + return list(roles.values()) + + def _get_scope_from_role(self, role): + """Return scopes from role if the component is jupyterhub""" + role_scopes = role.get("attributes", {}).get("scopes", []) + component = role.get("attributes", {}).get("component") + # Attributes are returned as a single-element array, unless `##` delimiter is used in Keycloak + # See this: https://stackoverflow.com/questions/68954733/keycloak-client-role-attribute-array + if component == ["jupyterhub"] and role_scopes: + return self.validate_scopes(role_scopes[0].split(",")) + else: + return [] + + def validate_scopes(self, role_scopes): + """Validate role scopes to sanity check user provided scopes from keycloak""" + self.log.info(f"Validating role scopes: {role_scopes}") + try: + # This is not a public function, but there isn't any alternative + # method to verify scopes, and we do need to do this sanity check + # as a invalid scopes could cause hub pod to fail + scopes._check_scopes_exist(role_scopes) + return role_scopes + except scopes.ScopeNotFound as e: + self.log.error(f"Invalid scopes, skipping: {role_scopes} ({e})") + return [] + + async def _get_roles_with_attributes(self, roles: dict, client_id: str, token: str): + """This fetches all roles by id to fetch their attributes.""" + roles_rich = [] + for role in roles: + # If this takes too much time, which isn't the case right now, we can + # also do multithreaded requests + role_rich = await self._fetch_api( + endpoint=f"roles-by-id/{role['id']}?client={client_id}", token=token + ) + roles_rich.append(role_rich) + return roles_rich + + async def _get_client_roles_for_user(self, user_id, client_id, token): + user_roles = await self._fetch_api( + endpoint=f"users/{user_id}/role-mappings/clients/{client_id}/composite", + token=token, + ) + return user_roles + + def _get_user_roles(self, user_info): + if callable(self.claim_roles_key): + return set(self.claim_roles_key(user_info)) + try: + return set(reduce(dict.get, self.claim_roles_key.split("."), user_info)) + except TypeError: + self.log.error( + f"The claim_roles_key {self.claim_roles_key} does not exist in the user token" + ) + return set() + + async def _get_token(self) -> str: + http = self.http_client + + body = urllib.parse.urlencode( + { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "client_credentials", + } + ) + response = await http.fetch( + self.token_url, + method="POST", + body=body, + ) + data = json.loads(response.body) + return data["access_token"] # type: ignore[no-any-return] + + async def _fetch_api(self, endpoint: str, token: str): + response = await self.http_client.fetch( + f"{self.realm_api_url}/{endpoint}", + method="GET", + headers={"Authorization": f"Bearer {token}"}, + ) + return json.loads(response.body) + + +c.JupyterHub.authenticator_class = KeyCloakOAuthenticator diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/skel/.bashrc b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/skel/.bashrc index a427972228..58a612cc46 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/skel/.bashrc +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/files/skel/.bashrc @@ -4,6 +4,10 @@ # ~/.bashrc: executed by bash(1) for non-login shells. # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) # for examples +# +# Regular Nebari users do not have write permissions to modify the ~/.bashrc file. +# Users can add their own customizations to the ~/.bash_profile file. +# More details can be found in the docs https://www.nebari.dev/docs/faq#can-i-modify-the-bashrc-file-on-nebari. # If not running interactively, don't do anything case $- in diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf index cf86d5a03e..570fda80c0 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf @@ -57,7 +57,7 @@ resource "helm_release" "jupyterhub" { repository = "https://jupyterhub.github.io/helm-chart/" chart = "jupyterhub" - version = "3.2.1" + version = "4.0.0-0.dev.git.6707.h109668fd" values = concat([ file("${path.module}/values.yaml"), @@ -69,8 +69,8 @@ resource "helm_release" "jupyterhub" { theme = var.theme profiles = var.profiles argo-workflows-enabled = var.argo-workflows-enabled - home-pvc = var.home-pvc - shared-pvc = var.shared-pvc + home-pvc = var.home-pvc.name + shared-pvc = var.shared-pvc.name conda-store-pvc = var.conda-store-pvc conda-store-mount = var.conda-store-mount default-conda-store-namespace = var.default-conda-store-namespace @@ -130,6 +130,7 @@ resource "helm_release" "jupyterhub" { "01-theme.py" = file("${path.module}/files/jupyterhub/01-theme.py") "02-spawner.py" = file("${path.module}/files/jupyterhub/02-spawner.py") "03-profiles.py" = file("${path.module}/files/jupyterhub/03-profiles.py") + "04-auth.py" = file("${path.module}/files/jupyterhub/04-auth.py") } services = { @@ -143,25 +144,25 @@ resource "helm_release" "jupyterhub" { # for simple key value configuration with jupyterhub traitlets # this hub.config property should be used config = { - JupyterHub = { - authenticator_class = "generic-oauth" - } Authenticator = { enable_auth_state = true } - GenericOAuthenticator = { + KeyCloakOAuthenticator = { client_id = module.jupyterhub-openid-client.config.client_id client_secret = module.jupyterhub-openid-client.config.client_secret oauth_callback_url = "https://${var.external-url}/hub/oauth_callback" authorize_url = module.jupyterhub-openid-client.config.authentication_url token_url = module.jupyterhub-openid-client.config.token_url userdata_url = module.jupyterhub-openid-client.config.userinfo_url + realm_api_url = module.jupyterhub-openid-client.config.realm_api_url login_service = "Keycloak" username_claim = "preferred_username" claim_groups_key = "groups" - allowed_groups = ["/analyst", "/developer", "/admin"] - admin_groups = ["/admin"] + claim_roles_key = "roles" + allowed_groups = ["/analyst", "/developer", "/admin", "jupyterhub_admin", "jupyterhub_developer"] + admin_groups = ["/admin", "jupyterhub_admin"] manage_groups = true + manage_roles = true refresh_pre_spawn = true validate_server_cert = false @@ -215,8 +216,25 @@ resource "helm_release" "jupyterhub" { name = "proxy.secretToken" value = random_password.proxy_secret_token.result } + + depends_on = [ + var.home-pvc, + var.shared-pvc, + ] + + lifecycle { + replace_triggered_by = [ + null_resource.home-pvc, + ] + } + } +resource "null_resource" "home-pvc" { + triggers = { + home-pvc = var.home-pvc.id + } +} resource "kubernetes_manifest" "jupyterhub" { manifest = { @@ -278,11 +296,41 @@ module "jupyterhub-openid-client" { "developer" = ["jupyterhub_developer", "dask_gateway_developer"] "analyst" = ["jupyterhub_developer"] } + client_roles = [ + { + "name" : "allow-app-sharing-role", + "description" : "Allow app sharing for apps created via JupyterHub App Launcher (jhub-apps)", + "groups" : [], + "attributes" : { + # grants permissions to share server + # grants permissions to read other user's names + # grants permissions to read other groups' names + # The later two are required for sharing with a group or user + "scopes" : "shares,read:users:name,read:groups:name" + "component" : "jupyterhub" + } + }, + { + "name" : "allow-read-access-to-services-role", + "description" : "Allow read access to services, such that they are visible on the home page e.g. conda-store", + # Adding it to analyst group such that it's applied to every user. + "groups" : ["analyst"], + "attributes" : { + # grants permissions to read services + "scopes" : "read:services", + "component" : "jupyterhub" + } + }, + ] callback-url-paths = [ "https://${var.external-url}/hub/oauth_callback", var.jupyterhub-logout-redirect-url ] jupyterlab_profiles_mapper = true + service-accounts-enabled = true + service-account-roles = [ + "view-realm", "view-users", "view-clients" + ] } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/variables.tf index 577dedc8ef..41089d391f 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/variables.tf @@ -48,12 +48,18 @@ variable "user-node-group" { variable "home-pvc" { description = "Name for persistent volume claim to use for home directory uses /home/{username}" - type = string + type = object({ + name = string + id = string + }) } variable "shared-pvc" { description = "Name for persistent volume claim to use for shared directory uses /share/{group}" - type = string + type = object({ + name = string + id = string + }) } variable "conda-store-pvc" { @@ -163,6 +169,26 @@ variable "jupyterlab-default-settings" { type = map(any) } +variable "jupyterlab-gallery-settings" { + description = "Server-side settings for jupyterlab-gallery extension" + type = object({ + title = optional(string) + destination = optional(string) + hide_gallery_without_exhibits = optional(bool) + exhibits = list(object({ + git = string + title = string + homepage = optional(string) + description = optional(string) + icon = optional(string) + account = optional(string) + token = optional(string) + branch = optional(string) + depth = optional(number) + })) + }) +} + variable "jupyterlab-pioneer-enabled" { description = "Enable JupyterLab Pioneer for telemetry" type = bool diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf index fd85eeb7a0..e23aeb13c8 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf @@ -15,7 +15,8 @@ resource "keycloak_openid_client" "main" { access_type = "CONFIDENTIAL" standard_flow_enabled = true - valid_redirect_uris = var.callback-url-paths + valid_redirect_uris = var.callback-url-paths + service_accounts_enabled = var.service-accounts-enabled } @@ -62,6 +63,33 @@ resource "keycloak_openid_user_attribute_protocol_mapper" "jupyterlab_profiles" aggregate_attributes = true } +data "keycloak_realm" "master" { + realm = "nebari" +} + +data "keycloak_openid_client" "realm_management" { + realm_id = var.realm_id + client_id = "realm-management" +} + +data "keycloak_role" "main-service" { + for_each = toset(var.service-account-roles) + + realm_id = data.keycloak_realm.master.id + client_id = data.keycloak_openid_client.realm_management.id + name = each.key +} + +resource "keycloak_openid_client_service_account_role" "main" { + for_each = toset(var.service-account-roles) + + realm_id = var.realm_id + service_account_user_id = keycloak_openid_client.main.service_account_user_id + client_id = data.keycloak_openid_client.realm_management.id + role = data.keycloak_role.main-service[each.key].name +} + + resource "keycloak_role" "main" { for_each = toset(flatten(values(var.role_mapping))) @@ -71,7 +99,6 @@ resource "keycloak_role" "main" { description = each.key } - data "keycloak_group" "main" { for_each = var.role_mapping @@ -89,3 +116,41 @@ resource "keycloak_group_roles" "group_roles" { exhaustive = false } + +resource "keycloak_role" "default_client_roles" { + for_each = { for role in var.client_roles : role.name => role } + realm_id = var.realm_id + client_id = keycloak_openid_client.main.id + name = each.value.name + description = each.value.description + attributes = each.value.attributes +} + +locals { + group_role_mapping = flatten([ + for role_object in var.client_roles : [ + for group_name in role_object.groups : { + group : group_name + role_name : role_object.name + } + ] + ]) + + client_roles_groups = toset([ + for index, value in local.group_role_mapping : value.group + ]) +} + +data "keycloak_group" "client_role_groups" { + for_each = local.client_roles_groups + realm_id = var.realm_id + name = each.value +} + +resource "keycloak_group_roles" "assign_roles" { + for_each = { for idx, value in local.group_role_mapping : idx => value } + realm_id = var.realm_id + group_id = data.keycloak_group.client_role_groups[each.value.group].id + role_ids = [keycloak_role.default_client_roles[each.value.role_name].id] + exhaustive = false +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/outputs.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/outputs.tf index bd1978bd4b..6077c22b0e 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/outputs.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/outputs.tf @@ -1,12 +1,14 @@ output "config" { description = "configuration credentials for connecting to openid client" value = { - client_id = keycloak_openid_client.main.client_id - client_secret = keycloak_openid_client.main.client_secret + client_id = keycloak_openid_client.main.client_id + client_secret = keycloak_openid_client.main.client_secret + service_account_user_id = keycloak_openid_client.main.service_account_user_id authentication_url = "https://${var.external-url}/auth/realms/${var.realm_id}/protocol/openid-connect/auth" token_url = "https://${var.external-url}/auth/realms/${var.realm_id}/protocol/openid-connect/token" userinfo_url = "https://${var.external-url}/auth/realms/${var.realm_id}/protocol/openid-connect/userinfo" + realm_api_url = "https://${var.external-url}/auth/admin/realms/${var.realm_id}" callback_urls = var.callback-url-paths } } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf index d20ecca48a..7626cc2b93 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf @@ -16,6 +16,19 @@ variable "external-url" { } +variable "service-accounts-enabled" { + description = "Whether the client should have a service account created" + type = bool + default = false +} + +variable "service-account-roles" { + description = "Roles to be granted to the service account. Requires setting service-accounts-enabled to true." + type = list(string) + default = [] +} + + variable "role_mapping" { description = "Group to role mapping to establish for client" type = map(list(string)) @@ -33,3 +46,14 @@ variable "jupyterlab_profiles_mapper" { type = bool default = false } + +variable "client_roles" { + description = "Create roles for the client and assign it to groups" + default = [] + type = list(object({ + name = string + description = string + groups = optional(list(string)) + attributes = map(any) + })) +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/conda_store.json b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/conda_store.json index 724b130bc0..87d38ade21 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/conda_store.json +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/conda_store.json @@ -112,7 +112,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_queued", @@ -123,7 +123,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_building", @@ -136,7 +136,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_completed", @@ -149,7 +149,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_failed", @@ -165,7 +165,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -216,7 +216,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_environments", @@ -231,7 +231,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -282,7 +282,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_queued", @@ -297,7 +297,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -348,7 +348,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_building", @@ -357,13 +357,13 @@ "refId": "A" } ], - "title": "Buliding", + "title": "Building", "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -414,7 +414,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_completed", @@ -429,7 +429,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -480,7 +480,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_build_failed", @@ -508,7 +508,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -559,7 +559,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_disk_usage / conda_store_disk_total", @@ -574,7 +574,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -621,7 +621,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_disk_total / (2.0^30)", @@ -637,7 +637,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -688,7 +688,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "conda_store_disk_usage / (2^30)", diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/keycloak.json b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/keycloak.json index 41fea2ad23..8dbbd2d4f8 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/keycloak.json +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/keycloak.json @@ -403,7 +403,7 @@ "format": "time_series", "interval": "", "intervalFactor": 1, - "legendFormat": "Comitted", + "legendFormat": "Committed", "refId": "C" }, { diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/traefik.json b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/traefik.json index 503b48af7c..188491c73c 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/traefik.json +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/dashboards/Main/traefik.json @@ -31,7 +31,7 @@ "collapsed": false, "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "gridPos": { "h": 1, @@ -108,7 +108,7 @@ { "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "exemplar": true, "expr": "count(kube_pod_status_ready{namespace=\"$namespace\",condition=\"true\",pod=~\"nebari-traefik-ingress-.*\", job=\"kube-state-metrics\"})", @@ -211,7 +211,7 @@ "collapsed": false, "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "gridPos": { "h": 1, @@ -506,7 +506,7 @@ "collapsed": false, "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "gridPos": { "h": 1, @@ -806,7 +806,7 @@ "collapsed": false, "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "gridPos": { "h": 1, @@ -917,7 +917,7 @@ "collapsed": false, "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "gridPos": { "h": 1, @@ -1168,7 +1168,7 @@ }, "datasource": { "type": "prometheus", - "uid": "PBFA97CFB590B2093" + "uid": "prometheus" }, "definition": "label_values(kube_pod_container_info{pod=~\".*traefik.*\"}, namespace)", "hide": 0, diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/main.tf index 869f616c71..f969937124 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/monitoring/main.tf @@ -3,16 +3,28 @@ resource "random_password" "grafana_admin_password" { special = false } +resource "kubernetes_secret" "grafana_oauth_secret" { + metadata { + name = "grafana-oauth-secret" + namespace = var.namespace + } + + data = { + "grafana-oauth-client-id" = module.grafana-client-id.config.client_id + "grafana-oauth-client-secret" = module.grafana-client-id.config.client_secret + } +} + resource "helm_release" "prometheus-grafana" { name = "nebari" namespace = var.namespace repository = "https://prometheus-community.github.io/helm-charts" chart = "kube-prometheus-stack" - version = "30.1.0" + version = "58.4.0" values = concat([ file("${path.module}/values.yaml"), - # https://github.com/prometheus-community/helm-charts/blob/kube-prometheus-stack-30.1.0/charts/kube-prometheus-stack/values.yaml + # https://github.com/prometheus-community/helm-charts/blob/kube-prometheus-stack-58.4.0/charts/kube-prometheus-stack/values.yaml jsonencode({ alertmanager = { alertmanagerSpec = { @@ -206,6 +218,8 @@ resource "helm_release" "prometheus-grafana" { } } + envFromSecret = kubernetes_secret.grafana_oauth_secret.metadata[0].name + "grafana.ini" : { server = { protocol = "http" @@ -222,8 +236,8 @@ resource "helm_release" "prometheus-grafana" { enabled = "true" name = "Login Keycloak" allow_sign_up = "true" - client_id = module.grafana-client-id.config.client_id - client_secret = module.grafana-client-id.config.client_secret + client_id = "$__env{grafana-oauth-client-id}" + client_secret = "$__env{grafana-oauth-client-secret}" scopes = "profile" auth_url = module.grafana-client-id.config.authentication_url token_url = module.grafana-client-id.config.token_url diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/cluster-values.yaml.tftpl b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/cluster-values.yaml.tftpl new file mode 100644 index 0000000000..2c1253b6e5 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/cluster-values.yaml.tftpl @@ -0,0 +1,163 @@ +# https://github.com/rook/rook/blob/v1.14.7/deploy/charts/rook-ceph-cluster/values.yaml +monitoring: + enabled: false # TODO: Enable monitoring when nebari-config.yaml has it enabled +toolbox: + enabled: false # for debugging purposes +cephBlockPools: [] +cephObjectStores: [] +cephClusterSpec: + cephConfig: + global: + osd_pool_default_size: "1" + mon_warn_on_pool_no_redundancy: "false" + bdev_flock_retry: "20" + bluefs_buffered_io: "false" + mon_data_avail_warn: "10" + placement: + additionalProperties: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: ${node_group.key} + operator: In + values: + - ${node_group.value} + # values from https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/cluster-on-pvc.yaml + dataDirHostPath: /var/lib/rook + mon: + # Set the number of mons to be started. Generally recommended to be 3. + # For highest availability, an odd number of mons should be specified. + count: 1 + allowMultiplePerNode: true + # A volume claim template can be specified in which case new monitors (and + # monitors created during fail over) will construct a PVC based on the + # template for the monitor's primary storage. Changes to the template do not + # affect existing monitors. Log data is stored on the HostPath under + # dataDirHostPath. If no storage requirement is specified, a default storage + # size appropriate for monitor data will be used. + volumeClaimTemplate: + spec: + %{ if storageClassName != null }storageClassName: ${storageClassName}%{ endif } + resources: + requests: + storage: 10Gi + cephVersion: + image: quay.io/ceph/ceph:v18.2.2 + allowUnsupported: false + mgr: + count: 1 + allowMultiplePerNode: true + modules: + - name: rook + enabled: true + dashboard: + enabled: true + ssl: false + crashCollector: + disable: true # false + logCollector: + enabled: true + periodicity: daily # one of: hourly, daily, weekly, monthly + maxLogSize: 500M # SUFFIX may be 'M' or 'G'. Must be at least 1M. + storage: + storageClassDeviceSets: + - name: set1 + # The number of OSDs to create from this device set + count: 1 + portable: true + tuneDeviceClass: true + tuneFastDeviceClass: true + # whether to encrypt the deviceSet or not + encrypted: false + # Since the OSDs could end up on any node, an effort needs to be made to spread the OSDs + # across nodes as much as possible. Unfortunately the pod anti-affinity breaks down + # as soon as you have more than one OSD per node. The topology spread constraints will + # give us an even spread on K8s 1.18 or newer. + placement: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: ${node_group.key} + operator: In + values: + - ${node_group.value} + resources: + volumeClaimTemplates: + - metadata: + name: data + # if you are looking at giving your OSD a different CRUSH device class than the one detected by Ceph + # annotations: + # crushDeviceClass: hybrid + spec: + resources: + requests: + storage: ${storage_capacity_Gi}Gi # TODO: Look into auto resizing these as needed + # IMPORTANT: Change the storage class depending on your environment + %{ if storageClassName != null }storageClassName: ${storageClassName}%{ endif } + volumeMode: Block + accessModes: + - ReadWriteOnce + # when onlyApplyOSDPlacement is false, will merge both placement.All() and storageClassDeviceSets.Placement. + onlyApplyOSDPlacement: false + resources: + priorityClassNames: + # If there are multiple nodes available in a failure domain (e.g. zones), the + # mons and osds can be portable and set the system-cluster-critical priority class. + mon: system-node-critical + osd: system-node-critical + mgr: system-cluster-critical + disruptionManagement: + managePodBudgets: true + osdMaintenanceTimeout: 30 + pgHealthCheckTimeout: 0 + +cephFileSystems: + - name: ceph-filesystem + # see https://github.com/rook/rook/blob/master/Documentation/CRDs/Shared-Filesystem/ceph-filesystem-crd.md#filesystem-settings for available configuration + spec: + metadataPool: + replicated: + size: 1 + dataPools: + - failureDomain: host + replicated: + size: 1 + # Optional and highly recommended, 'data0' by default, see https://github.com/rook/rook/blob/master/Documentation/CRDs/Shared-Filesystem/ceph-filesystem-crd.md#pools + name: data0 + metadataServer: + activeCount: 1 + activeStandby: true + resources: + limits: + memory: "4Gi" + requests: + cpu: "1000m" + memory: "4Gi" + priorityClassName: system-cluster-critical + storageClass: + enabled: true + isDefault: false + name: ceph-filesystem + # (Optional) specify a data pool to use, must be the name of one of the data pools above, 'data0' by default + pool: data0 + reclaimPolicy: Delete + allowVolumeExpansion: true + volumeBindingMode: "Immediate" + annotations: { } + labels: { } + mountOptions: [] + # see https://github.com/rook/rook/blob/master/Documentation/Storage-Configuration/Shared-Filesystem-CephFS/filesystem-storage.md#provision-storage for available configuration + parameters: + # The secrets contain Ceph admin credentials. + csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner + csi.storage.k8s.io/provisioner-secret-namespace: "{{ .Release.Namespace }}" + csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner + csi.storage.k8s.io/controller-expand-secret-namespace: "{{ .Release.Namespace }}" + csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node + csi.storage.k8s.io/node-stage-secret-namespace: "{{ .Release.Namespace }}" + # Specify the filesystem type of the volume. If not specified, csi-provisioner + # will set default as `ext4`. Note that `xfs` is not recommended due to potential deadlock + # in hyperconverged settings where the volume is mounted on the same node as the osds. + csi.storage.k8s.io/fstype: ext4 diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/main.tf new file mode 100644 index 0000000000..32be674561 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/main.tf @@ -0,0 +1,72 @@ +resource "helm_release" "rook-ceph-cluster" { + name = "rook-ceph-cluster" + namespace = var.namespace + repository = "https://charts.rook.io/release" + chart = "rook-ceph-cluster" + version = "v1.14.7" + wait = true + wait_for_jobs = true + + values = concat([ + templatefile("${path.module}/cluster-values.yaml.tftpl", + { + "storageClassName" = var.storage_class_name, + "node_group" = var.node_group, + "storage_capacity_Gi" = var.ceph_storage_capacity, + }), + jsonencode({ + operatorNamespace = var.operator_namespace, + }) + ], var.overrides) +} + +locals { + storage-class = data.kubernetes_storage_class.rook-ceph-fs-delete-sc + storage-class-base-name = "ceph-filesystem" +} + +data "kubernetes_storage_class" "rook-ceph-fs-delete-sc" { + metadata { + name = local.storage-class-base-name # TODO: Make sure we get this right + } + depends_on = [helm_release.rook-ceph-cluster] +} + +resource "kubernetes_storage_class" "ceph-retain-sc" { + metadata { + name = "${local.storage-class-base-name}-retain" # "ceph-filesystem-retain" # TODO: Make sure we get this right + } + storage_provisioner = local.storage-class.storage_provisioner # "rook-ceph.cephfs.csi.ceph.com" + reclaim_policy = "Retain" + volume_binding_mode = local.storage-class.volume_binding_mode + allow_volume_expansion = local.storage-class.allow_volume_expansion + parameters = local.storage-class.parameters + + depends_on = [data.kubernetes_storage_class.rook-ceph-fs-delete-sc] +} + +# This is necessary on GKE to completely create a ceph cluster +resource "kubernetes_resource_quota" "rook_critical_pods" { + metadata { + name = "rook-critical-pods" + namespace = var.namespace + labels = { + "addonmanager.kubernetes.io/mode" = "Reconcile" + } + } + + spec { + hard = { + "pods" = "1G" + } + + scope_selector { + match_expression { + operator = "In" + scope_name = "PriorityClass" + values = ["system-node-critical", "system-cluster-critical"] + } + } + } + # depends_on = [helm_release.rook-ceph] +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/operator-values.yaml b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/operator-values.yaml new file mode 100644 index 0000000000..d9155da1ef --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/operator-values.yaml @@ -0,0 +1 @@ +# https://github.com/rook/rook/blob/v1.14.7/deploy/charts/rook-ceph/values.yaml diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/variables.tf new file mode 100644 index 0000000000..67969dc083 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/variables.tf @@ -0,0 +1,35 @@ +variable "namespace" { + description = "deploy rook-ceph operator in this namespace" + type = string +} + +variable "operator_namespace" { + description = "namespace where the rook-ceph operator is deployed" + type = string +} + + +variable "overrides" { + description = "Rook Ceph helm chart overrides" + type = list(string) + default = [] +} + +variable "storage_class_name" { + description = "Name of the storage class to create" + type = string + default = null +} + +variable "node_group" { + description = "Node key value pair for bound resources" + type = object({ + key = string + value = string + }) +} + +variable "ceph_storage_capacity" { + description = "Ceph storage capacity in Gi" + type = number +} diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/versions.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/versions.tf new file mode 100644 index 0000000000..341def1365 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/rook-ceph/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + helm = { + source = "hashicorp/helm" + version = "2.1.2" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.20.0" + } + } + required_version = ">= 1.0" +} diff --git a/src/_nebari/stages/kubernetes_services/template/rook-ceph.tf b/src/_nebari/stages/kubernetes_services/template/rook-ceph.tf new file mode 100644 index 0000000000..1895d50d41 --- /dev/null +++ b/src/_nebari/stages/kubernetes_services/template/rook-ceph.tf @@ -0,0 +1,55 @@ +# ======================= VARIABLES ====================== +variable "rook_ceph_storage_class_name" { + description = "Name of the storage class to create" + type = string +} + +locals { + enable-ceph-cluster = local.jupyterhub-fs == "cephfs" || local.conda-store-fs == "cephfs" +} +# ====================== RESOURCES ======================= +module "rook-ceph" { + count = local.enable-ceph-cluster ? 1 : 0 + source = "./modules/kubernetes/services/rook-ceph" + namespace = var.environment + operator_namespace = var.environment + + storage_class_name = var.rook_ceph_storage_class_name + node_group = var.node_groups.general + ceph_storage_capacity = var.jupyterhub-shared-storage + var.conda-store-filesystem-storage + + depends_on = [helm_release.rook-ceph] +} + +# data "kubernetes_namespace" "existing" { +# metadata { +# name = var.environment +# } +# } + +resource "helm_release" "rook-ceph" { + name = "rook-ceph" + namespace = var.environment + repository = "https://charts.rook.io/release" + chart = "rook-ceph" + version = "v1.14.7" + + values = concat([ + file("./modules/kubernetes/services/rook-ceph/operator-values.yaml"), + jsonencode({ + nodeSelector = { + "${var.node_groups.general.key}" = var.node_groups.general.value + }, + monitoring = { + enabled = false # TODO: Enable monitoring when nebari-config.yaml has it enabled + }, + csi = { + enableRbdDriver = false, # necessary to provision block storage, but saves some cpu and memory if not needed + }, + }) + ], + # var.overrides + ) + + # depends_on = [kubernetes_namespace.rook-ceph] +} diff --git a/src/_nebari/stages/nebari_tf_extensions/__init__.py b/src/_nebari/stages/nebari_tf_extensions/__init__.py index eaaf131117..b589f5fb8f 100644 --- a/src/_nebari/stages/nebari_tf_extensions/__init__.py +++ b/src/_nebari/stages/nebari_tf_extensions/__init__.py @@ -72,6 +72,9 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): "stages/05-kubernetes-keycloak" ]["keycloak_nebari_bot_password"]["value"], "helm_extensions": [_.model_dump() for _ in self.config.helm_extensions], + "forwardauth_middleware_name": stage_outputs[ + "stages/07-kubernetes-services" + ]["forward-auth-middleware"]["value"]["name"], } diff --git a/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/locals.tf b/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/locals.tf index 4c5f0de3e7..b3616d4d29 100644 --- a/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/locals.tf +++ b/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/locals.tf @@ -1,6 +1,6 @@ locals { middlewares = (var.private) ? ([{ - name = "traefik-forward-auth" + name = var.forwardauth_middleware_name namespace = var.namespace }]) : ([]) diff --git a/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/variables.tf b/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/variables.tf index 071c11ffbd..9a255ff5e1 100644 --- a/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/variables.tf +++ b/src/_nebari/stages/nebari_tf_extensions/template/modules/nebariextension/variables.tf @@ -70,3 +70,8 @@ variable "keycloak_nebari_bot_password" { type = string default = "" } + +variable "forwardauth_middleware_name" { + description = "Name of the traefik forward auth middleware" + type = string +} diff --git a/src/_nebari/stages/nebari_tf_extensions/template/tf-extensions.tf b/src/_nebari/stages/nebari_tf_extensions/template/tf-extensions.tf index dd87639393..915b78879e 100644 --- a/src/_nebari/stages/nebari_tf_extensions/template/tf-extensions.tf +++ b/src/_nebari/stages/nebari_tf_extensions/template/tf-extensions.tf @@ -16,6 +16,7 @@ module "extension" { nebari-realm-id = var.realm_id keycloak_nebari_bot_password = each.value.keycloakadmin ? var.keycloak_nebari_bot_password : "" + forwardauth_middleware_name = var.forwardauth_middleware_name envs = lookup(each.value, "envs", []) } diff --git a/src/_nebari/stages/nebari_tf_extensions/template/variables.tf b/src/_nebari/stages/nebari_tf_extensions/template/variables.tf index 144a6049cb..e17d86ffca 100644 --- a/src/_nebari/stages/nebari_tf_extensions/template/variables.tf +++ b/src/_nebari/stages/nebari_tf_extensions/template/variables.tf @@ -31,3 +31,8 @@ variable "helm_extensions" { variable "keycloak_nebari_bot_password" { description = "Keycloak password for nebari-bot" } + +variable "forwardauth_middleware_name" { + description = "Name of the traefik forward auth middleware" + type = string +} diff --git a/src/_nebari/stages/terraform_state/template/aws/modules/terraform-state/main.tf b/src/_nebari/stages/terraform_state/template/aws/modules/terraform-state/main.tf index 2b0561dd73..2931f153bf 100644 --- a/src/_nebari/stages/terraform_state/template/aws/modules/terraform-state/main.tf +++ b/src/_nebari/stages/terraform_state/template/aws/modules/terraform-state/main.tf @@ -1,3 +1,7 @@ +resource "aws_kms_key" "tf-state-key" { + enable_key_rotation = true +} + resource "aws_s3_bucket" "terraform-state" { bucket = "${var.name}-terraform-state" @@ -16,6 +20,28 @@ resource "aws_s3_bucket" "terraform-state" { } } +resource "aws_s3_bucket_public_access_block" "terraform-state" { + bucket = aws_s3_bucket.terraform-state.id + ignore_public_acls = true + block_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "terraform-state" { + bucket = aws_s3_bucket.terraform-state.id + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.tf-state-key.arn + sse_algorithm = "aws:kms" + } + } + # // AWS may return HTTP 409 if PutBucketEncryption is called immediately after S3 + # bucket creation. Adding dependency avoids concurrent requests. + depends_on = [aws_s3_bucket_public_access_block.terraform-state] +} + resource "aws_dynamodb_table" "terraform-state-lock" { name = "${var.name}-terraform-state-lock" diff --git a/src/_nebari/stages/terraform_state/template/azure/modules/terraform-state/main.tf b/src/_nebari/stages/terraform_state/template/azure/modules/terraform-state/main.tf index a13f613cf9..544aa578c7 100644 --- a/src/_nebari/stages/terraform_state/template/azure/modules/terraform-state/main.tf +++ b/src/_nebari/stages/terraform_state/template/azure/modules/terraform-state/main.tf @@ -12,6 +12,7 @@ resource "azurerm_storage_account" "terraform-state-storage-account" { account_tier = "Standard" account_replication_type = "GRS" tags = var.tags + min_tls_version = "TLS1_2" identity { type = "SystemAssigned" diff --git a/src/_nebari/stages/tf_objects.py b/src/_nebari/stages/tf_objects.py index f5911a72cb..76f05e5f9c 100644 --- a/src/_nebari/stages/tf_objects.py +++ b/src/_nebari/stages/tf_objects.py @@ -17,7 +17,6 @@ def NebariKubernetesProvider(nebari_config: schema.Main): Provider("aws", region=nebari_config.amazon_web_services.region), Provider( "kubernetes", - experiments={"manifest_resource": True}, host="${data.aws_eks_cluster.default.endpoint}", cluster_ca_certificate="${base64decode(data.aws_eks_cluster.default.certificate_authority[0].data)}", token="${data.aws_eks_cluster_auth.default.token}", @@ -25,7 +24,6 @@ def NebariKubernetesProvider(nebari_config: schema.Main): ) return Provider( "kubernetes", - experiments={"manifest_resource": True}, ) diff --git a/src/_nebari/subcommands/deploy.py b/src/_nebari/subcommands/deploy.py index 0aa861027f..fe4cddf1df 100644 --- a/src/_nebari/subcommands/deploy.py +++ b/src/_nebari/subcommands/deploy.py @@ -84,6 +84,11 @@ def deploy( stages.remove(stage) rich.print("Skipping remote state provision") + # Digital Ocean support deprecation warning -- Nebari 2024.7.1 + if config.provider == "do" and not disable_prompt: + msg = "Digital Ocean support is currently being deprecated and will be removed in a future release. Would you like to continue?" + typer.confirm(msg) + deploy_configuration( config, stages, diff --git a/src/_nebari/subcommands/init.py b/src/_nebari/subcommands/init.py index 9040f3d201..a43c285132 100644 --- a/src/_nebari/subcommands/init.py +++ b/src/_nebari/subcommands/init.py @@ -106,6 +106,7 @@ class InitInputs(schema.Base): ssl_cert_email: Optional[schema.email_pydantic] = None disable_prompt: bool = False output: pathlib.Path = pathlib.Path("nebari-config.yaml") + explicit: int = 0 def enum_to_list(enum_cls): @@ -152,7 +153,7 @@ def handle_init(inputs: InitInputs, config_schema: BaseModel): try: write_configuration( inputs.output, - config, + config if not inputs.explicit else config_schema(**config), mode="x", ) except FileExistsError: @@ -565,6 +566,13 @@ def init( "-o", help="Output file path for the rendered config file.", ), + explicit: int = typer.Option( + 0, + "--explicit", + "-e", + count=True, + help="Write explicit nebari config file (advanced users only).", + ), ): """ Create and initialize your [purple]nebari-config.yaml[/purple] file. @@ -587,6 +595,13 @@ def init( inputs.cloud_provider = check_cloud_provider_creds( cloud_provider, disable_prompt ) + + # Digital Ocean deprecation warning -- Nebari 2024.7.1 + if inputs.cloud_provider == ProviderEnum.do.value.lower(): + rich.print( + ":warning: Digital Ocean support is being deprecated and support will be removed in the future. :warning:\n" + ) + inputs.region = check_cloud_provider_region(region, inputs.cloud_provider) inputs.kubernetes_version = check_cloud_provider_kubernetes_version( kubernetes_version, inputs.cloud_provider, inputs.region @@ -604,6 +619,7 @@ def init( inputs.ssl_cert_email = ssl_cert_email inputs.disable_prompt = disable_prompt inputs.output = output + inputs.explicit = explicit from nebari.plugins import nebari_plugin_manager @@ -653,6 +669,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): "\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❗️ [red]Digital Ocean[/red] is currently being deprecated and support will be removed in the future.\n" ) ) # try: @@ -726,7 +743,7 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): # DOMAIN NAME rich.print( ( - "\n\n 🪴 Great! Now you can provide a valid domain name (i.e. the URL) to access your Nebri instance. " + "\n\n 🪴 Great! Now you can provide a valid domain name (i.e. the URL) to access your Nebari instance. " "This should be a domain that you own. Default if unspecified is the IP of the load balancer.\n\n" ) ) @@ -894,6 +911,14 @@ def guided_init_wizard(ctx: typer.Context, guided_init: str): ) inputs.kubernetes_version = kubernetes_version + # EXPLICIT CONFIG + inputs.explicit = questionary.confirm( + "Would you like the nebari config to show all available options? (recommended for advanced users only)", + default=False, + qmark=qmark, + auto_enter=False, + ).unsafe_ask() + from nebari.plugins import nebari_plugin_manager config_schema = nebari_plugin_manager.config_schema diff --git a/src/_nebari/upgrade.py b/src/_nebari/upgrade.py index 64e593be66..5fc18b1af5 100644 --- a/src/_nebari/upgrade.py +++ b/src/_nebari/upgrade.py @@ -1,16 +1,27 @@ +""" +This file contains the upgrade logic for Nebari. +Each release of Nebari requires an upgrade step class (which is a child class of UpgradeStep) to be created. +When a user runs `nebari upgrade -c nebari-config.yaml`, then the do_upgrade function will then run through all required upgrade steps to bring the config file up to date with the current version of Nebari. +""" + import json import logging import re import secrets import string +import textwrap from abc import ABC from pathlib import Path from typing import Any, ClassVar, Dict +import kubernetes.client +import kubernetes.config +import requests import rich from packaging.version import Version from pydantic import ValidationError from rich.prompt import Prompt +from typing_extensions import override from _nebari.config import backup_configuration from _nebari.stages.infrastructure import ( @@ -38,6 +49,20 @@ def do_upgrade(config_filename, attempt_fixes=False): + """ + Perform an upgrade of the Nebari configuration file. + + This function loads the YAML configuration file, checks for deprecated keys, + validates the current version, and if necessary, upgrades the configuration + to the latest version of Nebari. + + Args: + config_filename (str): The path to the configuration file. + attempt_fixes (bool): Whether to attempt automatic fixes for validation errors. + + Returns: + None + """ config = load_yaml(config_filename) if config.get("qhub_version"): rich.print( @@ -86,10 +111,24 @@ def do_upgrade(config_filename, attempt_fixes=False): class UpgradeStep(ABC): + """ + Abstract base class representing an upgrade step. + + Attributes: + _steps (ClassVar[Dict[str, Any]]): Class variable holding registered upgrade steps. + version (ClassVar[str]): The version of the upgrade step. + """ + _steps: ClassVar[Dict[str, Any]] = {} version: ClassVar[str] = "" def __init_subclass__(cls): + """ + Initializes a subclass of UpgradeStep. + + This method validates the version string and registers the subclass + in the _steps dictionary. + """ try: parsed_version = Version(cls.version) except ValueError as exc: @@ -111,6 +150,15 @@ def clear_steps_registry(cls): @classmethod def has_step(cls, version): + """ + Checks if there is an upgrade step for a given version. + + Args: + version (str): The version to check. + + Returns: + bool: True if the step exists, False otherwise. + """ return version in cls._steps @classmethod @@ -120,6 +168,16 @@ def upgrade( """ Runs through all required upgrade steps (i.e. relevant subclasses of UpgradeStep). Calls UpgradeStep.upgrade_step for each. + + Args: + config (dict): The current configuration dictionary. + start_version (str): The starting version of the configuration. + finish_version (str): The target version for the configuration. + config_filename (str): The path to the configuration file. + attempt_fixes (bool): Whether to attempt automatic fixes for validation errors. + + Returns: + dict: The updated configuration dictionary. """ starting_ver = rounded_ver_parse(start_version or "0.0.0") finish_ver = rounded_ver_parse(finish_version) @@ -155,9 +213,19 @@ def upgrade( return config def get_version(self): + """ + Returns: + str: The version of the upgrade step. + """ return self.version def requires_nebari_version_field(self): + """ + Checks if the nebari_version field is required for this upgrade step. + + Returns: + bool: True if the nebari_version field is required, False otherwise. + """ return rounded_ver_parse(self.version) > rounded_ver_parse("0.3.13") def upgrade_step(self, config, start_version, config_filename, *args, **kwargs): @@ -173,6 +241,14 @@ def upgrade_step(self, config, start_version, config_filename, *args, **kwargs): It should normally be left as-is for all upgrades. Use _version_specific_upgrade below for any actions that are only required for the particular upgrade you are creating. + + Args: + config (dict): The current configuration dictionary. + start_version (str): The starting version of the configuration. + config_filename (str): The path to the configuration file. + + Returns: + dict: The updated configuration dictionary. """ finish_version = self.get_version() __rounded_finish_version__ = str(rounded_ver_parse(finish_version)) @@ -190,11 +266,32 @@ def upgrade_step(self, config, start_version, config_filename, *args, **kwargs): config["nebari_version"] = self.version def contains_image_and_tag(s: str) -> bool: - # match on `quay.io/nebari/nebari-<...>:YYYY.MM.XX`` + """ + Check if the string matches the Nebari image pattern. + + Args: + s (str): The string to check. + + Returns: + bool: True if the string matches the pattern, False otherwise. + """ pattern = r"^quay\.io\/nebari\/nebari-(jupyterhub|jupyterlab|dask-worker)(-gpu)?:\d{4}\.\d+\.\d+$" return bool(re.match(pattern, s)) - def replace_image_tag_legacy(image, start_version, new_version): + def replace_image_tag_legacy( + image: str, start_version: str, new_version: str + ) -> str: + """ + Replace legacy image tags with the new version. + + Args: + image (str): The current image string. + start_version (str): The starting version of the image. + new_version (str): The new version to replace with. + + Returns: + str: The updated image string with the new version, or None if no match. + """ start_version_regex = start_version.replace(".", "\\.") if not start_version: start_version_regex = "0\\.[0-3]\\.[0-9]{1,2}" @@ -209,6 +306,17 @@ def replace_image_tag_legacy(image, start_version, new_version): return None def replace_image_tag(s: str, new_version: str, config_path: str) -> str: + """ + Replace the image tag with the new version. + + Args: + s (str): The current image string. + new_version (str): The new version to replace with. + config_path (str): The path to the configuration file. + + Returns: + str: The updated image string with the new version, or the original string if no changes. + """ legacy_replacement = replace_image_tag_legacy(s, start_version, new_version) if legacy_replacement: return legacy_replacement @@ -229,6 +337,17 @@ def replace_image_tag(s: str, new_version: str, config_path: str) -> str: return s def set_nested_item(config: dict, config_path: list, value: str): + """ + Set a nested item in the configuration dictionary. + + Args: + config (dict): The configuration dictionary. + config_path (list): The path to the item to set. + value (str): The value to set. + + Returns: + None + """ config_path = config_path.split(".") for k in config_path[:-1]: try: @@ -242,7 +361,21 @@ def set_nested_item(config: dict, config_path: list, value: str): pass config[config_path[-1]] = value - def update_image_tag(config, config_path, current_image, new_version): + def update_image_tag( + config: dict, config_path: str, current_image: str, new_version: str + ) -> dict: + """ + Update the image tag in the configuration. + + Args: + config (dict): The configuration dictionary. + config_path (str): The path to the item to update. + current_image (str): The current image string. + new_version (str): The new version to replace with. + + Returns: + dict: The updated configuration dictionary. + """ new_image = replace_image_tag(current_image, new_version, config_path) if new_image != current_image: set_nested_item(config, config_path, new_image) @@ -287,7 +420,17 @@ def _version_specific_upgrade( self, config, start_version, config_filename, *args, **kwargs ): """ + Perform version-specific upgrade tasks. + Override this method in subclasses if you need to do anything specific to your version. + + Args: + config (dict): The current configuration dictionary. + start_version (str): The starting version of the configuration. + config_filename (str): The path to the configuration file. + + Returns: + dict: The updated configuration dictionary. """ return config @@ -295,6 +438,7 @@ def _version_specific_upgrade( class Upgrade_0_3_12(UpgradeStep): version = "0.3.12" + @override def _version_specific_upgrade( self, config, start_version, config_filename, *args, **kwargs ): @@ -315,11 +459,13 @@ def _version_specific_upgrade( class Upgrade_0_4_0(UpgradeStep): version = "0.4.0" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): """ - Upgrade to Keycloak. + This version of Nebari introduces Keycloak for authentication, removes deprecated fields, + and generates a default password for the Keycloak root user. """ security = config.get("security", {}) users = security.get("users", {}) @@ -447,6 +593,7 @@ def _version_specific_upgrade( class Upgrade_0_4_1(UpgradeStep): version = "0.4.1" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -473,6 +620,7 @@ def _version_specific_upgrade( class Upgrade_2023_4_2(UpgradeStep): version = "2023.4.2" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -507,6 +655,7 @@ def _version_specific_upgrade( class Upgrade_2023_7_1(UpgradeStep): version = "2023.7.1" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -525,6 +674,7 @@ def _version_specific_upgrade( class Upgrade_2023_7_2(UpgradeStep): version = "2023.7.2" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -546,11 +696,22 @@ def _version_specific_upgrade( class Upgrade_2023_10_1(UpgradeStep): + """ + Upgrade step for Nebari version 2023.10.1 + + Note: + Upgrading to 2023.10.1 is considered high-risk because it includes a major refactor + to introduce the extension mechanism system. This version introduces significant + changes, including the support for third-party plugins, upgrades JupyterHub to version 3.1, + and deprecates certain components such as CDS Dashboards, ClearML, Prefect, and kbatch. + """ + version = "2023.10.1" # JupyterHub Helm chart 2.0.0 (app version 3.0.0) requires K8S Version >=1.23. (reference: https://z2jh.jupyter.org/en/stable/) # This released has been tested against 1.26 min_k8s_version = 1.26 + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -653,8 +814,16 @@ def _version_specific_upgrade( class Upgrade_2023_11_1(UpgradeStep): + """ + Upgrade step for Nebari version 2023.11.1 + + Note: + - ClearML, Prefect, and kbatch are no longer supported in this version. + """ + version = "2023.11.1" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -671,8 +840,16 @@ def _version_specific_upgrade( class Upgrade_2023_12_1(UpgradeStep): + """ + Upgrade step for Nebari version 2023.12.1 + + Note: + - This is the last version that supports the jupyterlab-videochat extension. + """ + version = "2023.12.1" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -690,8 +867,16 @@ def _version_specific_upgrade( class Upgrade_2024_1_1(UpgradeStep): + """ + Upgrade step for Nebari version 2024.1.1 + + Note: + - jupyterlab-videochat, retrolab, jupyter-tensorboard, jupyterlab-conda-store, and jupyter-nvdashboard are no longer supported. + """ + version = "2024.1.1" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -712,6 +897,7 @@ def _version_specific_upgrade( class Upgrade_2024_3_1(UpgradeStep): version = "2024.3.1" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -723,6 +909,7 @@ def _version_specific_upgrade( class Upgrade_2024_3_2(UpgradeStep): version = "2024.3.2" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -734,6 +921,7 @@ def _version_specific_upgrade( class Upgrade_2024_3_3(UpgradeStep): version = "2024.3.3" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -743,8 +931,16 @@ def _version_specific_upgrade( class Upgrade_2024_4_1(UpgradeStep): + """ + Upgrade step for Nebari version 2024.4.1 + + Note: + - Adds default configuration for node groups if not already defined. + """ + version = "2024.4.1" + @override def _version_specific_upgrade( self, config, start_version, config_filename: Path, *args, **kwargs ): @@ -773,6 +969,242 @@ def _version_specific_upgrade( return config +class Upgrade_2024_5_1(UpgradeStep): + version = "2024.5.1" + + @override + def _version_specific_upgrade( + self, config, start_version, config_filename: Path, *args, **kwargs + ): + rich.print("Ready to upgrade to Nebari version [green]2024.5.1[/green].") + + return config + + +class Upgrade_2024_6_1(UpgradeStep): + """ + Upgrade step for version 2024.6.1 + + This upgrade includes: + - Manual updates for kube-prometheus-stack CRDs if monitoring is enabled. + - Prompts to upgrade GCP node groups to more cost-efficient instances. + """ + + version = "2024.6.1" + + @override + def _version_specific_upgrade( + self, config, start_version, config_filename: Path, *args, **kwargs + ): + # Prompt users to manually update kube-prometheus-stack CRDs if monitoring is enabled + if config.get("monitoring", {}).get("enabled", True): + + crd_urls = [ + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagerconfigs.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagers.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_podmonitors.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_probes.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_prometheusagents.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_prometheuses.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_prometheusrules.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_scrapeconfigs.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml", + "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.73.0/example/prometheus-operator-crd/monitoring.coreos.com_thanosrulers.yaml", + ] + daemonset_name = "prometheus-node-exporter" + namespace = config.get("namespace", "default") + + # We're upgrading from version 30.1.0 to 58.4.0. This is a major upgrade and requires manual intervention. + # See https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/README.md#upgrading-chart + # for more information on why the following commands are necessary. + commands = "[cyan bold]" + for url in crd_urls: + commands += f"kubectl apply --server-side --force-conflicts -f {url}\n" + commands += f"kubectl delete daemonset -l app={daemonset_name} --namespace {namespace}\n" + commands += "[/cyan bold]" + + rich.print( + "\n ⚠️ Warning ⚠️" + "\n-> [red bold]Nebari version 2024.6.1 comes with a new version of Grafana. Any custom dashboards that you created will be deleted after upgrading Nebari. Make sure to [link=https://grafana.com/docs/grafana/latest/dashboards/share-dashboards-panels/#export-a-dashboard-as-json]export them as JSON[/link] so you can [link=https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/import-dashboards/#import-a-dashboard]import them[/link] again afterwards.[/red bold]" + f"\n-> [red bold]Before upgrading, kube-prometheus-stack CRDs need to be updated and the {daemonset_name} daemonset needs to be deleted.[/red bold]" + ) + run_commands = Prompt.ask( + "\nDo you want Nebari to update the kube-prometheus-stack CRDs and delete the prometheus-node-exporter for you? If not, you'll have to do it manually.", + choices=["y", "N"], + default="N", + ) + + # By default, rich wraps lines by splitting them into multiple lines. This is + # far from ideal, as users copy-pasting the commands will get errors when running them. + # To avoid this, we use a rich console with a larger width to print the entire commands + # and let the terminal wrap them if needed. + console = rich.console.Console(width=220) + if run_commands == "y": + try: + kubernetes.config.load_kube_config() + except kubernetes.config.config_exception.ConfigException: + rich.print( + "[red bold]No default kube configuration file was found. Make sure to [link=https://www.nebari.dev/docs/how-tos/debug-nebari#generating-the-kubeconfig]have one pointing to your Nebari cluster[/link] before upgrading.[/red bold]" + ) + exit() + current_kube_context = kubernetes.config.list_kube_config_contexts()[1] + cluster_name = current_kube_context["context"]["cluster"] + rich.print( + f"The following commands will be run for the [cyan bold]{cluster_name}[/cyan bold] cluster" + ) + Prompt.ask("Hit enter to show the commands") + console.print(commands) + + Prompt.ask("Hit enter to continue") + # We need to add a special constructor to the yaml loader to handle a specific + # tag as otherwise the kubernetes API will fail when updating the CRD. + yaml.constructor.add_constructor( + "tag:yaml.org,2002:value", lambda loader, node: node.value + ) + for url in crd_urls: + response = requests.get(url) + response.raise_for_status() + crd = yaml.load(response.text) + crd_name = crd["metadata"]["name"] + api_instance = kubernetes.client.ApiextensionsV1Api() + try: + api_response = api_instance.read_custom_resource_definition( + name=crd_name + ) + except kubernetes.client.exceptions.ApiException: + api_response = api_instance.create_custom_resource_definition( + body=crd + ) + else: + api_response = api_instance.patch_custom_resource_definition( + name=crd["metadata"]["name"], body=crd + ) + + api_instance = kubernetes.client.AppsV1Api() + api_response = api_instance.list_namespaced_daemon_set( + namespace=namespace, label_selector=f"app={daemonset_name}" + ) + if api_response.items: + api_instance.delete_namespaced_daemon_set( + name=api_response.items[0].metadata.name, + namespace=namespace, + ) + + rich.print( + f"The kube-prometheus-stack CRDs have been updated and the {daemonset_name} daemonset has been deleted." + ) + else: + rich.print( + "[red bold]Before upgrading, you need to manually delete the prometheus-node-exporter daemonset and update the kube-prometheus-stack CRDs. To do that, please run the following commands.[/red bold]" + ) + Prompt.ask("Hit enter to show the commands") + console.print(commands) + + Prompt.ask("Hit enter to continue") + continue_ = Prompt.ask( + f"Have you backed up your custom dashboards (if necessary), deleted the {daemonset_name} daemonset and updated the kube-prometheus-stack CRDs?", + choices=["y", "N"], + default="N", + ) + if not continue_ == "y": + rich.print( + f"[red bold]You must back up your custom dashboards (if necessary), delete the {daemonset_name} daemonset and update the kube-prometheus-stack CRDs before upgrading to [green]{self.version}[/green] (or later).[/bold red]" + ) + exit() + + # Prompt users to upgrade to the new default node groups for GCP + if (provider := config.get("provider", "")) == ProviderEnum.gcp.value: + provider_full_name = provider_enum_name_map[provider] + if not config.get(provider_full_name, {}).get("node_groups", {}): + try: + text = textwrap.dedent( + f""" + The default node groups for GCP have been changed to cost efficient e2 family nodes reducing the running cost of Nebari on GCP by ~50%. + This change will affect your current deployment, and will result in ~15 minutes of downtime during the upgrade step as the node groups are switched out, but shouldn't result in data loss. + + [red bold]Note: If upgrading to the new node types, the upgrade process will take longer than usual. For this upgrade only, you'll likely see a timeout \ + error and need to restart the deployment process afterwards in order to upgrade successfully.[/red bold] + + As always, make sure to backup data before upgrading. See https://www.nebari.dev/docs/how-tos/manual-backup for more information. + + Would you like to upgrade to the cost effective node groups [purple]{config_filename}[/purple]? + If not, select "N" and the old default node groups will be added to the nebari config file. + """ + ) + continue_ = Prompt.ask( + text, + choices=["y", "N"], + default="y", + ) + if continue_ == "N": + config[provider_full_name]["node_groups"] = { + "general": { + "instance": "n1-standard-8", + "min_nodes": 1, + "max_nodes": 1, + }, + "user": { + "instance": "n1-standard-4", + "min_nodes": 0, + "max_nodes": 5, + }, + "worker": { + "instance": "n1-standard-4", + "min_nodes": 0, + "max_nodes": 5, + }, + } + except KeyError: + pass + else: + text = textwrap.dedent( + """ + The default node groups for GCP have been changed to cost efficient e2 family nodes reducing the running cost of Nebari on GCP by ~50%. + Consider upgrading your node group instance types to the new default configuration. + + Upgrading your general node will result in ~15 minutes of downtime during the upgrade step as the node groups are switched out, but shouldn't result in data loss. + + As always, make sure to backup data before upgrading. See https://www.nebari.dev/docs/how-tos/manual-backup for more information. + + The new default node groups instances are: + """ + ) + text += json.dumps( + { + "general": {"instance": "e2-highmem-4"}, + "user": {"instance": "e2-standard-4"}, + "worker": {"instance": "e2-standard-4"}, + }, + indent=4, + ) + text += "\n\nHit enter to continue" + Prompt.ask(text) + return config + + +class Upgrade_2024_7_1(UpgradeStep): + """ + Upgrade step for Nebari version 2024.7.1 + + Note: + - Digital Ocean deprecation warning. + """ + + version = "2024.7.1" + + @override + def _version_specific_upgrade( + self, config, start_version, config_filename: Path, *args, **kwargs + ): + if config.get("provider", "") == ProviderEnum.do.value: + rich.print("\n ⚠️ Deprecation Warning ⚠️") + rich.print( + "-> Digital Ocean support is currently being deprecated and will be removed in a future release.", + ) + rich.print("") + return config + + __rounded_version__ = str(rounded_ver_parse(__version__)) # Manually-added upgrade steps must go above this line @@ -780,4 +1212,11 @@ def _version_specific_upgrade( # Always have a way to upgrade to the latest full version number, even if no customizations # Don't let dev/prerelease versions cloud things class UpgradeLatest(UpgradeStep): + """ + Upgrade step for the latest available version. + + This class ensures there is always an upgrade path to the latest version, + even if no specific upgrade steps are defined for the current version. + """ + version = __rounded_version__ diff --git a/src/_nebari/utils.py b/src/_nebari/utils.py index 3ae4ad4bd8..6b33b1efbb 100644 --- a/src/_nebari/utils.py +++ b/src/_nebari/utils.py @@ -353,3 +353,56 @@ def check_environment_variables(variables: Set[str], reference: str) -> None: f"""Missing the following required environment variables: {required_variables}\n Please see the documentation for more information: {reference}""" ) + + +def byte_unit_conversion(byte_size_str: str, output_unit: str = "B") -> float: + """Converts string representation of byte size to another unit and returns float output + + e.g. byte_unit_conversion("1 KB", "B") -> 1000.0 + e.g. byte_unit_conversion("1 KiB", "B") -> 1024.0 + """ + byte_size_str = byte_size_str.lower() + output_unit = output_unit.lower() + + units_multiplier = { + "b": 1, + "k": 1000, + "m": 1000**2, + "g": 1000**3, + "t": 1000**4, + "kb": 1000, + "mb": 1000**2, + "gb": 1000**3, + "tb": 1000**4, + "ki": 1024, + "mi": 1024**2, + "gi": 1024**3, + "ti": 1024**4, + "kib": 1024, + "mib": 1024**2, + "gib": 1024**3, + "tib": 1024**4, + } + + if output_unit not in units_multiplier: + raise ValueError( + f'Invalid input unit "{output_unit}". Valid units are {units_multiplier.keys()}' + ) + + str_pattern = r"\s*^(\d+(?:\.\d*){0,1})\s*([a-zA-Z]*)\s*$" + pattern = re.compile(str_pattern, re.IGNORECASE) + match = pattern.search(byte_size_str) + + if not match: + raise ValueError("Invalid byte size string") + value = float(match.group(1)) + input_unit = match.group(2) + if not input_unit: + input_unit = "b" + + if input_unit not in units_multiplier: + raise ValueError( + f'Invalid input unit "{input_unit}". Valid units are {list(units_multiplier.keys())}' + ) + + return value * units_multiplier[input_unit] / units_multiplier[output_unit] diff --git a/src/nebari/plugins.py b/src/nebari/plugins.py index c5148e9e1d..a523c0324f 100644 --- a/src/nebari/plugins.py +++ b/src/nebari/plugins.py @@ -128,7 +128,7 @@ def config_schema(self): classes = [schema.Main] + [ _.input_schema for _ in self.ordered_stages if _.input_schema is not None ] - return type("ConfigSchema", tuple(classes), {}) + return type("ConfigSchema", tuple(classes[::-1]), {}) nebari_plugin_manager = NebariPluginManager() diff --git a/src/nebari/schema.py b/src/nebari/schema.py index 70b9589e6f..2cc1c1ea3f 100644 --- a/src/nebari/schema.py +++ b/src/nebari/schema.py @@ -25,7 +25,9 @@ class Base(pydantic.BaseModel): model_config = ConfigDict( - extra="forbid", validate_assignment=True, populate_by_name=True + extra="forbid", + validate_assignment=True, + populate_by_name=True, ) diff --git a/tests/common/conda_store_utils.py b/tests/common/conda_store_utils.py new file mode 100644 index 0000000000..c150b4f6f0 --- /dev/null +++ b/tests/common/conda_store_utils.py @@ -0,0 +1,41 @@ +import re + +import requests + +from tests.tests_deployment import constants + + +def get_conda_store_session(): + """Log into conda-store using the test account and get session""" + session = requests.Session() + r = session.get( + f"https://{constants.NEBARI_HOSTNAME}/conda-store/login/?next=", verify=False + ) + auth_url = re.search('action="([^"]+)"', r.content.decode("utf8")).group(1) + response = session.post( + auth_url.replace("&", "&"), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "username": constants.KEYCLOAK_USERNAME, + "password": constants.KEYCLOAK_PASSWORD, + "credentialId": "", + }, + verify=False, + ) + assert response.status_code == 200 + return session + + +def get_conda_store_user_permissions(): + """Log into conda-store using the test account and get session and using the token in + session call conda-store API to get permissions. + """ + session = get_conda_store_session() + token = session.cookies.get("conda-store-auth") + response = requests.get( + f"https://{constants.NEBARI_HOSTNAME}/conda-store/api/v1/permission/", + headers={"Authorization": f"Bearer {token}"}, + verify=False, + ) + assert response.status_code == 200 + return response.json() diff --git a/tests/common/navigator.py b/tests/common/navigator.py index 12a1445bd5..f846d9a545 100644 --- a/tests/common/navigator.py +++ b/tests/common/navigator.py @@ -256,7 +256,7 @@ def reset_workspace(self): self._set_environment_via_popup(kernel=None) # go to Kernel menu - kernel_menuitem = self.page.get_by_text("Kernel", exact=True) + kernel_menuitem = self.page.get_by_role("menuitem", name="Kernel", exact=True) kernel_menuitem.click() # shut down multiple running kernels with contextlib.suppress(Exception): @@ -320,14 +320,23 @@ def _set_environment_via_popup(self, kernel=None): # failure here indicates that the environment doesn't exist either # because of incorrect naming syntax or because the env is still # being built - self.page.get_by_role("combobox").nth(1).select_option(kernel) - # click Select to close popup (deal with the two formats of this dialog) - try: - self.page.get_by_role("button", name="Select Kernel").click() - except Exception: - self.page.locator("div").filter(has_text="No KernelSelect").get_by_role( - "button", name="Select Kernel" - ).click() + + new_launcher_popup = self.page.locator( + ".jp-KernelSelector-Dialog .jp-NewLauncher-table table" + ).nth(0) + if new_launcher_popup.is_visible(): + # for when the jupyterlab-new-launcher extension is installed + new_launcher_popup.locator("td").nth(0).click() + else: + # for when only the native launcher is available + self.page.get_by_role("combobox").nth(1).select_option(kernel) + # click Select to close popup (deal with the two formats of this dialog) + try: + self.page.get_by_role("button", name="Select Kernel").click() + except Exception: + self.page.locator("div").filter( + has_text="No KernelSelect" + ).get_by_role("button", name="Select Kernel").click() def set_environment(self, kernel): """Set environment of a jupyter notebook. @@ -350,7 +359,7 @@ def set_environment(self, kernel): popup = self._check_for_kernel_popup() # if there is not a kernel popup, make it appear if not popup: - self.page.get_by_text("Kernel", exact=True).click() + self.page.get_by_role("menuitem", name="Kernel", exact=True).click() self.page.get_by_role("menuitem", name="Change Kernel…").get_by_text( "Change Kernel…" ).click() diff --git a/tests/common/run_notebook.py b/tests/common/run_notebook.py index 10d28d6637..0c39f4b5ac 100644 --- a/tests/common/run_notebook.py +++ b/tests/common/run_notebook.py @@ -20,7 +20,7 @@ def run( expected_outputs: List[str], conda_env: str, timeout: float = 1000, - complition_wait_time: float = 2, + completion_wait_time: float = 2, retry: int = 2, retry_wait_time: float = 5, exact_match: bool = True, @@ -47,7 +47,7 @@ def run( timeout: float Time in seconds to wait for the expected output text to appear. default: 1000 - complition_wait_time: float + completion_wait_time: float Time in seconds to wait between checking for expected output text. default: 2 retry: int @@ -77,7 +77,7 @@ def run( self._restart_run_all() # Wait for a couple of seconds to make sure it's re-started time.sleep(retry_wait_time) - self._wait_for_commands_completion(timeout, complition_wait_time) + self._wait_for_commands_completion(timeout, completion_wait_time) all_outputs = self._get_outputs() assert_match_all_outputs(expected_outputs, all_outputs, exact_match) @@ -126,7 +126,7 @@ def assert_code_output( code: str, expected_output: str, timeout: float = 1000, - complition_wait_time: float = 2, + completion_wait_time: float = 2, exact_match: bool = True, ): """ @@ -143,11 +143,11 @@ def assert_code_output( timeout: float Time in seconds to wait for the expected output text to appear. default: 1000 - complition_wait_time: float + completion_wait_time: float Time in seconds to wait between checking for expected output text. """ self.run_in_last_cell(code) - self._wait_for_commands_completion(timeout, complition_wait_time) + self._wait_for_commands_completion(timeout, completion_wait_time) outputs = self._get_outputs() actual_output = outputs[-1] if outputs else "" assert_match_output(expected_output, actual_output, exact_match) @@ -177,7 +177,7 @@ def _get_last_cell(self): raise ValueError("Unable to get last cell") def _wait_for_commands_completion( - self, timeout: float, complition_wait_time: float + self, timeout: float, completion_wait_time: float ): """ Wait for commands to finish running @@ -186,7 +186,7 @@ def _wait_for_commands_completion( ---------- timeout: float Time in seconds to wait for the expected output text to appear. - complition_wait_time: float + completion_wait_time: float Time in seconds to wait between checking for expected output text. """ elapsed_time = 0.0 @@ -198,7 +198,7 @@ def _wait_for_commands_completion( if not still_visible: break elapsed_time = time.time() - start_time - time.sleep(complition_wait_time) + time.sleep(completion_wait_time) if still_visible: raise ValueError( f"Timeout Waited for commands to finish, " @@ -212,7 +212,7 @@ def _get_outputs(self) -> List[str]: def _restart_run_all(self): # restart run all cells - self.nav.page.get_by_text("Kernel", exact=True).click() + self.nav.page.get_by_role("menuitem", name="Kernel", exact=True).click() self.nav.page.get_by_role( "menuitem", name="Restart Kernel and Run All Cells…" ).get_by_text("Restart Kernel and Run All Cells…").click() diff --git a/tests/tests_deployment/conftest.py b/tests/tests_deployment/conftest.py new file mode 100644 index 0000000000..7464f6bb46 --- /dev/null +++ b/tests/tests_deployment/conftest.py @@ -0,0 +1,12 @@ +import pytest + +from tests.tests_deployment.keycloak_utils import delete_client_keycloak_test_roles + + +@pytest.fixture() +def cleanup_keycloak_roles(): + # setup + yield + # teardown + delete_client_keycloak_test_roles(client_name="jupyterhub") + delete_client_keycloak_test_roles(client_name="conda_store") diff --git a/tests/tests_deployment/keycloak_utils.py b/tests/tests_deployment/keycloak_utils.py new file mode 100644 index 0000000000..b11c64b93f --- /dev/null +++ b/tests/tests_deployment/keycloak_utils.py @@ -0,0 +1,104 @@ +import os +import pathlib + +from _nebari.config import read_configuration +from _nebari.keycloak import get_keycloak_admin_from_config +from nebari.plugins import nebari_plugin_manager + + +def get_keycloak_client_details_by_name(client_name, keycloak_admin=None): + if not keycloak_admin: + keycloak_admin = get_keycloak_admin() + clients = keycloak_admin.get_clients() + for client in clients: + if client["clientId"] == client_name: + return client + + +def get_keycloak_user_details_by_name(username, keycloak_admin=None): + if not keycloak_admin: + keycloak_admin = get_keycloak_admin() + users = keycloak_admin.get_users() + for user in users: + if user["username"] == username: + return user + + +def get_keycloak_role_details_by_name(roles, role_name): + for role in roles: + if role["name"] == role_name: + return role + + +def get_keycloak_admin(): + config_schema = nebari_plugin_manager.config_schema + config_filepath = os.environ.get("NEBARI_CONFIG_PATH", "nebari-config.yaml") + assert pathlib.Path(config_filepath).exists() + config = read_configuration(config_filepath, config_schema) + return get_keycloak_admin_from_config(config) + + +def create_keycloak_client_role( + client_id: str, role_name: str, scopes: str, component: str +): + keycloak_admin = get_keycloak_admin() + keycloak_admin.create_client_role( + client_id, + payload={ + "name": role_name, + "description": f"{role_name} description", + "attributes": {"scopes": [scopes], "component": [component]}, + }, + ) + client_roles = keycloak_admin.get_client_roles(client_id=client_id) + return get_keycloak_role_details_by_name(client_roles, role_name) + + +def assign_keycloak_client_role_to_user(username: str, client_name: str, role: dict): + """Given a keycloak role and client name, assign that to the user""" + keycloak_admin = get_keycloak_admin() + user_details = get_keycloak_user_details_by_name( + username=username, keycloak_admin=keycloak_admin + ) + client_details = get_keycloak_client_details_by_name( + client_name=client_name, keycloak_admin=keycloak_admin + ) + keycloak_admin.assign_client_role( + user_id=user_details["id"], client_id=client_details["id"], roles=[role] + ) + + +def create_keycloak_role(client_name: str, role_name: str, scopes: str, component: str): + """Create a role keycloak role for the given client with scopes and + component set in attributes + """ + keycloak_admin = get_keycloak_admin() + client_details = get_keycloak_client_details_by_name( + client_name=client_name, keycloak_admin=keycloak_admin + ) + return create_keycloak_client_role( + client_details["id"], role_name=role_name, scopes=scopes, component=component + ) + + +def get_keycloak_client_roles(client_name): + keycloak_admin = get_keycloak_admin() + client_details = get_keycloak_client_details_by_name( + client_name=client_name, keycloak_admin=keycloak_admin + ) + return keycloak_admin.get_client_roles(client_id=client_details["id"]) + + +def delete_client_keycloak_test_roles(client_name): + keycloak_admin = get_keycloak_admin() + client_details = get_keycloak_client_details_by_name( + client_name=client_name, keycloak_admin=keycloak_admin + ) + client_roles = keycloak_admin.get_client_roles(client_id=client_details["id"]) + for role in client_roles: + if not role["name"].startswith("test"): + continue + keycloak_admin.delete_client_role( + client_role_id=client_details["id"], + role_name=role["name"], + ) diff --git a/tests/tests_deployment/test_conda_store_roles_loaded.py b/tests/tests_deployment/test_conda_store_roles_loaded.py new file mode 100644 index 0000000000..732b0b0154 --- /dev/null +++ b/tests/tests_deployment/test_conda_store_roles_loaded.py @@ -0,0 +1,66 @@ +import pytest + +from tests.common.conda_store_utils import get_conda_store_user_permissions +from tests.tests_deployment import constants +from tests.tests_deployment.keycloak_utils import ( + assign_keycloak_client_role_to_user, + create_keycloak_role, +) + + +@pytest.mark.parametrize( + "scopes,changed_scopes", + ( + [ + "admin!namespace=analyst,developer!namespace=nebari-git", + {"nebari-git/*": ["developer"], "analyst/*": ["admin"]}, + ], + [ + "admin!namespace=analyst,developer!namespace=invalid-namespace", + {"analyst/*": ["admin"]}, + ], + [ + # duplicate namespace role, chose highest permissions + "admin!namespace=analyst,developer!namespace=analyst", + {"analyst/*": ["admin"]}, + ], + ["invalid-role!namespace=analyst", {}], + ), +) +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +@pytest.mark.filterwarnings( + "ignore:.*auto_refresh_token is deprecated:DeprecationWarning" +) +@pytest.mark.filterwarnings("ignore::ResourceWarning") +def test_conda_store_roles_loaded_from_keycloak( + scopes: str, changed_scopes: dict, cleanup_keycloak_roles +): + + # Verify permissions/roles are different from what we're about to set + # So that this test is actually testing the change + permissions = get_conda_store_user_permissions() + entity_roles = permissions["data"]["entity_roles"] + for namespace, role in changed_scopes.items(): + assert entity_roles[namespace] != role + + role = create_keycloak_role( + client_name="conda_store", + # Note: we're clearing this role after every test case, and we're clearing + # it by name, so it must start with test- to be deleted afterwards + role_name="test-custom-role", + scopes=scopes, + component="conda-store", + ) + assert role + # assign created role to the user + assign_keycloak_client_role_to_user( + constants.KEYCLOAK_USERNAME, client_name="conda_store", role=role + ) + permissions = get_conda_store_user_permissions() + updated_entity_roles = permissions["data"]["entity_roles"] + + # Verify permissions/roles are set to expectation + assert updated_entity_roles == { + **entity_roles, + **changed_scopes, + } diff --git a/tests/tests_deployment/test_jupyterhub_api.py b/tests/tests_deployment/test_jupyterhub_api.py new file mode 100644 index 0000000000..4144fd4fe8 --- /dev/null +++ b/tests/tests_deployment/test_jupyterhub_api.py @@ -0,0 +1,115 @@ +import pytest + +from tests.tests_deployment import constants +from tests.tests_deployment.keycloak_utils import ( + assign_keycloak_client_role_to_user, + create_keycloak_role, + get_keycloak_client_roles, +) +from tests.tests_deployment.utils import create_jupyterhub_token, get_jupyterhub_session + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_jupyterhub_loads_roles_from_keycloak(): + session = get_jupyterhub_session() + xsrf_token = session.cookies.get("_xsrf") + response = session.get( + f"https://{constants.NEBARI_HOSTNAME}/hub/api/users/{constants.KEYCLOAK_USERNAME}", + headers={"X-XSRFToken": xsrf_token}, + verify=False, + ) + user = response.json() + assert set(user["roles"]) == { + "user", + "manage-account", + "jupyterhub_developer", + "argo-developer", + "dask_gateway_developer", + "grafana_viewer", + "conda_store_developer", + "argo-viewer", + "grafana_developer", + "manage-account-links", + "view-profile", + # default roles + "allow-read-access-to-services-role", + } + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_default_user_role_scopes(): + token_response = create_jupyterhub_token(note="get-default-scopes") + token_scopes = set(token_response.json()["scopes"]) + assert "read:services" in token_scopes + + +@pytest.mark.filterwarnings( + "ignore:.*auto_refresh_token is deprecated:DeprecationWarning" +) +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_check_default_roles_added_in_keycloak(): + client_roles = get_keycloak_client_roles(client_name="jupyterhub") + role_names = [role["name"] for role in client_roles] + assert "allow-app-sharing-role" in role_names + assert "allow-read-access-to-services-role" in role_names + + +@pytest.mark.parametrize( + "component,scopes,expected_scopes_difference", + ( + [ + "jupyterhub", + "read:users:shares,read:groups:shares,users:shares", + {"read:groups:shares", "users:shares", "read:users:shares"}, + ], + ["invalid-component", "read:users:shares,read:groups:shares,users:shares", {}], + ["invalid-component", "admin:invalid-scope", {}], + ), +) +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +@pytest.mark.filterwarnings( + "ignore:.*auto_refresh_token is deprecated:DeprecationWarning" +) +def test_keycloak_roles_attributes_parsed_as_jhub_scopes( + component, scopes, expected_scopes_difference, cleanup_keycloak_roles +): + # check token scopes before role creation and assignment + token_response_before = create_jupyterhub_token( + note="before-role-creation-and-assignment" + ) + token_scopes_before = set(token_response_before.json()["scopes"]) + # create keycloak role with jupyterhub scopes in attributes + role = create_keycloak_role( + client_name="jupyterhub", + # Note: we're clearing this role after every test case, and we're clearing + # it by name, so it must start with test- to be deleted afterward + role_name="test-custom-role", + scopes=scopes, + component=component, + ) + assert role + # assign created role to the user + assign_keycloak_client_role_to_user( + constants.KEYCLOAK_USERNAME, client_name="jupyterhub", role=role + ) + token_response_after = create_jupyterhub_token( + note="after-role-creation-and-assignment" + ) + token_scopes_after = set(token_response_after.json()["scopes"]) + # verify new scopes added/removed + expected_scopes_difference = token_scopes_after - token_scopes_before + # Comparing token scopes for the user before and after role assignment + assert expected_scopes_difference == expected_scopes_difference + + +@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") +def test_jupyterhub_loads_groups_from_keycloak(): + session = get_jupyterhub_session() + xsrf_token = session.cookies.get("_xsrf") + response = session.get( + f"https://{constants.NEBARI_HOSTNAME}/hub/api/users/{constants.KEYCLOAK_USERNAME}", + headers={"X-XSRFToken": xsrf_token}, + verify=False, + ) + user = response.json() + assert set(user["groups"]) == {"/analyst", "/developer", "/users"} diff --git a/tests/tests_deployment/test_jupyterhub_ssh.py b/tests/tests_deployment/test_jupyterhub_ssh.py index fd6b0799d5..1e28226436 100644 --- a/tests/tests_deployment/test_jupyterhub_ssh.py +++ b/tests/tests_deployment/test_jupyterhub_ssh.py @@ -125,6 +125,9 @@ def test_contains_jupyterhub_ssh(paramiko_object): ("cat ~/.bashrc", "Managed by Nebari"), ("cat ~/.profile", "Managed by Nebari"), ("cat ~/.bash_logout", "Managed by Nebari"), + # ensure we don't copy over extra files from /etc/skel in init container + ("ls -la ~/..202*", "No such file or directory"), + ("ls -la ~/..data", "No such file or directory"), ] for command, output in commands_contain: diff --git a/tests/tests_deployment/utils.py b/tests/tests_deployment/utils.py index d175a2dd05..b0965dd1ae 100644 --- a/tests/tests_deployment/utils.py +++ b/tests/tests_deployment/utils.py @@ -26,20 +26,24 @@ def get_jupyterhub_session(): return session -def get_jupyterhub_token(note="jupyterhub-tests-deployment"): +def create_jupyterhub_token(note): session = get_jupyterhub_session() xsrf_token = session.cookies.get("_xsrf") headers = {"Referer": f"https://{constants.NEBARI_HOSTNAME}/hub/token"} if xsrf_token: headers["X-XSRFToken"] = xsrf_token data = {"note": note, "expires_in": None} - r = session.post( + return session.post( f"https://{constants.NEBARI_HOSTNAME}/hub/api/users/{constants.KEYCLOAK_USERNAME}/tokens", headers=headers, json=data, + verify=False, ) - return r.json()["token"] + +def get_jupyterhub_token(note="jupyterhub-tests-deployment"): + response = create_jupyterhub_token(note=note) + return response.json()["token"] def monkeypatch_ssl_context(): diff --git a/tests/tests_e2e/cypress.config.js b/tests/tests_e2e/cypress.config.js new file mode 100644 index 0000000000..135c9d44c1 --- /dev/null +++ b/tests/tests_e2e/cypress.config.js @@ -0,0 +1,12 @@ +const { defineConfig } = require('cypress') + +module.exports = defineConfig({ + video: true, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require('./cypress/plugins/index.js')(on, config) + }, + }, +}) diff --git a/tests/tests_e2e/cypress.json b/tests/tests_e2e/cypress.json deleted file mode 100644 index 2c63c08510..0000000000 --- a/tests/tests_e2e/cypress.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/tests/tests_e2e/cypress/integration/main.js b/tests/tests_e2e/cypress/e2e/main.cy.js similarity index 94% rename from tests/tests_e2e/cypress/integration/main.js rename to tests/tests_e2e/cypress/e2e/main.cy.js index 1184ba76d6..6fd5beff15 100644 --- a/tests/tests_e2e/cypress/integration/main.js +++ b/tests/tests_e2e/cypress/e2e/main.cy.js @@ -52,8 +52,8 @@ describe('First Test', () => { cy.get('h1') .should('contain', 'Server Options'); - cy.get('input.btn.btn-jupyter') - .should('have.attr', 'value', 'Start').click(); + cy.get('button.btn.btn-jupyter') + .should('contain', 'Start').click(); // Minimal check that JupyterLab has opened cy.get('div#jp-MainLogo', { timeout: 60000 }).should('exist').wait(4000); @@ -87,7 +87,7 @@ describe('First Test', () => { cy.visit('/monitoring/dashboards'); - cy.get('div.page-header h1', { timeout: 20000 }).should('contain', 'Dashboards'); + cy.get('div#pageContent h1', { timeout: 20000 }).should('contain', 'Dashboards'); // Visit Keycloak User Profile diff --git a/tests/tests_e2e/cypress/support/index.js b/tests/tests_e2e/cypress/support/e2e.js similarity index 100% rename from tests/tests_e2e/cypress/support/index.js rename to tests/tests_e2e/cypress/support/e2e.js diff --git a/tests/tests_e2e/package-lock.json b/tests/tests_e2e/package-lock.json index 0252ae09a5..3b24c9bf8e 100644 --- a/tests/tests_e2e/package-lock.json +++ b/tests/tests_e2e/package-lock.json @@ -9,64 +9,25 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "cypress": "^6.8.0", + "cypress": "^13.10.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21" } }, - "node_modules/@cypress/listr-verbose-renderer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", - "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=", - "dev": true, - "dependencies": { - "chalk": "^1.1.3", - "cli-cursor": "^1.0.2", - "date-fns": "^1.27.2", - "figures": "^1.7.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@cypress/listr-verbose-renderer/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@cypress/listr-verbose-renderer/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@cypress/listr-verbose-renderer/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, + "optional": true, "engines": { - "node": ">=0.8.0" + "node": ">=0.1.90" } }, "node_modules/@cypress/request": { - "version": "2.88.5", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz", - "integrity": "sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -76,19 +37,17 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", + "http-signature": "~1.3.6", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" + "uuid": "^8.3.2" }, "engines": { "node": ">= 6" @@ -113,36 +72,20 @@ "ms": "^2.1.1" } }, - "node_modules/@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", + "node_modules/@types/node": { + "version": "20.12.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz", + "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==", "dev": true, + "optional": true, "dependencies": { - "any-observable": "^0.3.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependenciesMeta": { - "rxjs": { - "optional": true - }, - "zen-observable": { - "optional": true - } + "undici-types": "~5.26.4" } }, - "node_modules/@types/node": { - "version": "12.12.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.50.tgz", - "integrity": "sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w==", - "dev": true - }, "node_modules/@types/sinonjs__fake-timers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz", - "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", "dev": true }, "node_modules/@types/sizzle": { @@ -151,38 +94,60 @@ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", "dev": true }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" } }, "node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/ansi-styles": { @@ -200,15 +165,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -236,9 +192,9 @@ "dev": true }, "node_modules/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, "dependencies": { "safer-buffer": "~2.1.0" @@ -247,12 +203,21 @@ "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, "engines": { "node": ">=0.8" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -262,7 +227,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, "node_modules/at-least-node": { @@ -277,16 +242,16 @@ "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true, "engines": { "node": "*" } }, "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", "dev": true }, "node_modules/balanced-match": { @@ -295,10 +260,30 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, "dependencies": { "tweetnacl": "^0.14.3" @@ -326,21 +311,39 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "engines": { "node": "*" } }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -350,10 +353,29 @@ "node": ">=6" } }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, "node_modules/chalk": { @@ -382,85 +404,70 @@ } }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } }, "node_modules/cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "dependencies": { - "restore-cursor": "^1.0.1" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/cli-table3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", - "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "dependencies": { - "object-assign": "^4.1.0", "string-width": "^4.2.0" }, "engines": { "node": "10.* || >= 12.*" }, "optionalDependencies": { - "colors": "^1.1.2" + "@colors/colors": "1.5.0" } }, "node_modules/cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", - "dev": true, - "dependencies": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "dependencies": { - "number-is-nan": "^1.0.0" + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "node": ">=8" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/color-convert": { @@ -481,15 +488,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" - } + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -504,9 +507,9 @@ } }, "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, "engines": { "node": ">= 6" @@ -527,25 +530,10 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, "node_modules/cross-spawn": { @@ -563,64 +551,81 @@ } }, "node_modules/cypress": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-6.8.0.tgz", - "integrity": "sha512-W2e9Oqi7DmF48QtOD0LfsOLVq6ef2hcXZvJXI/E3PgFNmZXEVwBefhAxVCW9yTPortjYA2XkM20KyC4HRkOm9w==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz", + "integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/listr-verbose-renderer": "^0.4.1", - "@cypress/request": "^2.88.5", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "12.12.50", - "@types/sinonjs__fake-timers": "^6.0.1", + "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", - "arch": "^2.1.2", - "blob-util": "2.0.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", "bluebird": "^3.7.2", + "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", - "cli-table3": "~0.6.0", - "commander": "^5.1.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", "common-tags": "^1.8.0", - "dayjs": "^1.9.3", - "debug": "4.3.2", - "eventemitter2": "^6.4.2", - "execa": "^4.0.2", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", "executable": "^4.1.1", - "extract-zip": "^1.7.0", - "fs-extra": "^9.0.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.2", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", - "listr": "^0.14.3", - "lodash": "^4.17.19", + "listr2": "^3.8.3", + "lodash": "^4.17.21", "log-symbols": "^4.0.0", - "minimist": "^1.2.5", - "moment": "^2.29.1", + "minimist": "^1.2.8", "ospath": "^1.2.2", - "pretty-bytes": "^5.4.1", - "ramda": "~0.27.1", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "supports-color": "^7.2.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", - "url": "^0.11.0", "yauzl": "^2.10.0" }, "bin": { "cypress": "bin/cypress" }, "engines": { - "node": ">=10.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, "dependencies": { "assert-plus": "^1.0.0" @@ -629,12 +634,6 @@ "node": ">=0.10" } }, - "node_modules/date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, "node_modules/dayjs": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz", @@ -642,9 +641,9 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -658,10 +657,27 @@ } } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "engines": { "node": ">=0.4.0" @@ -670,22 +686,13 @@ "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, - "node_modules/elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -701,19 +708,53 @@ "once": "^1.4.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "engines": { "node": ">=0.8.0" } }, "node_modules/eventemitter2": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz", - "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true }, "node_modules/execa": { @@ -751,15 +792,6 @@ "node": ">=4" } }, - "node_modules/exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -767,82 +799,62 @@ "dev": true }, "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", + "debug": "^4.1.1", + "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "bin": { "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true, "engines": [ "node >=0.6.0" ] }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "dependencies": { "pend": "~1.2.0" } }, "node_modules/figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "dependencies": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" + "escape-string-regexp": "^1.0.5" }, "engines": { - "node": ">=0.10.0" - } - }, + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true, "engines": { "node": "*" @@ -883,6 +895,34 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -910,7 +950,7 @@ "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, "dependencies": { "assert-plus": "^1.0.0" @@ -937,83 +977,107 @@ } }, "node_modules/global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", "dev": true, "dependencies": { - "ini": "1.3.7" + "ini": "2.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "dev": true, "dependencies": { "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" }, "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" + "node": ">=0.10" } }, "node_modules/human-signals": { @@ -1025,13 +1089,33 @@ "node": ">=8.12.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/inflight": { @@ -1051,18 +1135,21 @@ "dev": true }, "node_modules/ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } }, "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, "dependencies": { - "ci-info": "^2.0.0" + "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" @@ -1078,33 +1165,21 @@ } }, "node_modules/is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, "dependencies": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, - "dependencies": { - "symbol-observable": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -1114,12 +1189,6 @@ "node": ">=8" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, "node_modules/is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -1132,7 +1201,7 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, "node_modules/is-unicode-supported": { @@ -1147,12 +1216,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1162,7 +1225,7 @@ "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, "node_modules/js-yaml": { @@ -1180,25 +1243,19 @@ "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, "node_modules/json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, "node_modules/jsonfile": { @@ -1214,9 +1271,9 @@ } }, "node_modules/jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "dev": true, "engines": [ "node >=0.6.0" @@ -1224,7 +1281,7 @@ "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -1237,245 +1294,31 @@ "node": "> 0.8" } }, - "node_modules/listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", "dev": true, "dependencies": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=6" - } - }, - "node_modules/listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", - "dev": true, - "dependencies": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=6" + "node": ">=10.0.0" }, "peerDependencies": { - "listr": "^0.14.2" - } - }, - "node_modules/listr-update-renderer/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "enquirer": ">= 2.3.0 < 3" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "dependencies": { - "chalk": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", - "dev": true, - "dependencies": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/listr-verbose-renderer/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/listr-verbose-renderer/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } } }, "node_modules/lodash": { @@ -1507,63 +1350,52 @@ } }, "node_modules/log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, "dependencies": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" + "node": ">=10" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "dependencies": { - "mimic-fn": "^1.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/merge-stream": { @@ -1573,21 +1405,21 @@ "dev": true }, "node_modules/mime-db": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", - "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", - "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "dependencies": { - "mime-db": "1.46.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -1615,30 +1447,12 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", - "dev": true, - "engines": { - "node": "*" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/ms": { @@ -1659,31 +1473,13 @@ "node": ">=8" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/once": { @@ -1717,12 +1513,18 @@ "dev": true }, "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/path-is-absolute": { @@ -1746,13 +1548,13 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, "node_modules/pify": { @@ -1776,16 +1578,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", "dev": true }, "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, "node_modules/pump": { @@ -1799,57 +1610,33 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/ramda": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", - "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", - "dev": true - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, "node_modules/request-progress": { @@ -1861,27 +1648,30 @@ "throttleit": "^1.0.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "dependencies": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true }, "node_modules/rimraf": { "version": "3.0.2", @@ -1899,15 +1689,12 @@ } }, "node_modules/rxjs": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", - "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "^2.1.0" } }, "node_modules/safe-buffer": { @@ -1930,11 +1717,40 @@ } ] }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/shebang-command": { "version": "2.0.0", @@ -1957,6 +1773,24 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -1964,18 +1798,23 @@ "dev": true }, "node_modules/slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, "dependencies": { "asn1": "~0.2.3", @@ -1997,66 +1836,30 @@ "node": ">=0.10.0" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/strip-final-newline": { @@ -2080,21 +1883,18 @@ "node": ">=8" } }, - "node_modules/symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", "dev": true }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -2108,28 +1908,39 @@ } }, "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=0.8" + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -2141,14 +1952,27 @@ "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "optional": true }, "node_modules/universalify": { "version": "2.0.0", @@ -2168,50 +1992,29 @@ "node": ">=8" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" } }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "bin": { - "uuid": "bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "dev": true, "engines": [ "node >=0.6.0" @@ -2238,59 +2041,20 @@ } }, "node_modules/wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", - "dev": true, - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" + "node": ">=10" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -2302,7 +2066,7 @@ "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", @@ -2311,49 +2075,17 @@ } }, "dependencies": { - "@cypress/listr-verbose-renderer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", - "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=", + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-cursor": "^1.0.2", - "date-fns": "^1.27.2", - "figures": "^1.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } + "optional": true }, "@cypress/request": { - "version": "2.88.5", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz", - "integrity": "sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -2363,19 +2095,17 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", + "http-signature": "~1.3.6", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" + "uuid": "^8.3.2" } }, "@cypress/xvfb": { @@ -2399,25 +2129,20 @@ } } }, - "@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", + "@types/node": { + "version": "20.12.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz", + "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==", "dev": true, + "optional": true, "requires": { - "any-observable": "^0.3.0" + "undici-types": "~5.26.4" } }, - "@types/node": { - "version": "12.12.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.50.tgz", - "integrity": "sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w==", - "dev": true - }, "@types/sinonjs__fake-timers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz", - "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", "dev": true }, "@types/sizzle": { @@ -2426,28 +2151,45 @@ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", "dev": true }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, + "optional": true, "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@types/node": "*" } }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -2459,12 +2201,6 @@ "color-convert": "^2.0.1" } }, - "any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true - }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -2478,9 +2214,9 @@ "dev": true }, "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, "requires": { "safer-buffer": "~2.1.0" @@ -2489,7 +2225,13 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { @@ -2501,7 +2243,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, "at-least-node": { @@ -2513,13 +2255,13 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true }, "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", "dev": true }, "balanced-match": { @@ -2528,10 +2270,16 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, "requires": { "tweetnacl": "^0.14.3" @@ -2559,16 +2307,20 @@ "concat-map": "0.0.1" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true }, "cachedir": { @@ -2577,10 +2329,23 @@ "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", "dev": true }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, "chalk": { @@ -2600,69 +2365,46 @@ "dev": true }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "requires": { - "restore-cursor": "^1.0.1" + "restore-cursor": "^3.1.0" } }, "cli-table3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", - "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "requires": { - "colors": "^1.1.2", - "object-assign": "^4.1.0", + "@colors/colors": "1.5.0", "string-width": "^4.2.0" } }, "cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "requires": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2678,12 +2420,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "optional": true + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "combined-stream": { "version": "1.0.8", @@ -2695,9 +2436,9 @@ } }, "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true }, "common-tags": { @@ -2712,22 +2453,10 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, "cross-spawn": { @@ -2742,68 +2471,75 @@ } }, "cypress": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-6.8.0.tgz", - "integrity": "sha512-W2e9Oqi7DmF48QtOD0LfsOLVq6ef2hcXZvJXI/E3PgFNmZXEVwBefhAxVCW9yTPortjYA2XkM20KyC4HRkOm9w==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz", + "integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==", "dev": true, "requires": { - "@cypress/listr-verbose-renderer": "^0.4.1", - "@cypress/request": "^2.88.5", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "12.12.50", - "@types/sinonjs__fake-timers": "^6.0.1", + "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", - "arch": "^2.1.2", - "blob-util": "2.0.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", "bluebird": "^3.7.2", + "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", - "cli-table3": "~0.6.0", - "commander": "^5.1.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", "common-tags": "^1.8.0", - "dayjs": "^1.9.3", - "debug": "4.3.2", - "eventemitter2": "^6.4.2", - "execa": "^4.0.2", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", "executable": "^4.1.1", - "extract-zip": "^1.7.0", - "fs-extra": "^9.0.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.2", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", - "listr": "^0.14.3", - "lodash": "^4.17.19", + "listr2": "^3.8.3", + "lodash": "^4.17.21", "log-symbols": "^4.0.0", - "minimist": "^1.2.5", - "moment": "^2.29.1", + "minimist": "^1.2.8", "ospath": "^1.2.2", - "pretty-bytes": "^5.4.1", - "ramda": "~0.27.1", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "supports-color": "^7.2.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", - "url": "^0.11.0", "yauzl": "^2.10.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, "requires": { "assert-plus": "^1.0.0" } }, - "date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, "dayjs": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz", @@ -2811,36 +2547,41 @@ "dev": true }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" } }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, - "elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2856,16 +2597,41 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + } + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, "eventemitter2": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz", - "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true }, "execa": { @@ -2894,12 +2660,6 @@ "pify": "^2.2.0" } }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2907,75 +2667,45 @@ "dev": true }, "extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "requires": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", "yauzl": "^2.10.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } } }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "requires": { "pend": "~1.2.0" } }, "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" + "escape-string-regexp": "^1.0.5" } }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true }, "form-data": { @@ -3007,6 +2737,25 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -3028,7 +2777,7 @@ "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, "requires": { "assert-plus": "^1.0.0" @@ -3049,12 +2798,21 @@ } }, "global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "requires": { + "ini": "2.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, "requires": { - "ini": "1.3.7" + "get-intrinsic": "^1.1.3" } }, "graceful-fs": { @@ -3063,46 +2821,51 @@ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" + "es-define-property": "^1.0.0" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "function-bind": "^1.1.2" } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "dev": true, "requires": { "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" } }, "human-signals": { @@ -3111,10 +2874,16 @@ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, "inflight": { @@ -3134,18 +2903,18 @@ "dev": true }, "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true }, "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, "requires": { - "ci-info": "^2.0.0" + "ci-info": "^3.2.0" } }, "is-fullwidth-code-point": { @@ -3155,22 +2924,13 @@ "dev": true }, "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dev": true, - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, "requires": { - "symbol-observable": "^1.1.0" + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" } }, "is-path-inside": { @@ -3179,12 +2939,6 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, - "is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -3194,7 +2948,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, "is-unicode-supported": { @@ -3203,12 +2957,6 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3218,7 +2966,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, "js-yaml": { @@ -3233,25 +2981,19 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, "jsonfile": { @@ -3265,14 +3007,14 @@ } }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -3282,194 +3024,20 @@ "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", "dev": true }, - "listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "requires": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - }, - "dependencies": { - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - } - } - }, - "listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true - }, - "listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "^1.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", + "listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", "dev": true, "requires": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" } }, "lodash": { @@ -3495,48 +3063,37 @@ } }, "log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" }, "dependencies": { - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } } } @@ -3548,18 +3105,18 @@ "dev": true }, "mime-db": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", - "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true }, "mime-types": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", - "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "requires": { - "mime-db": "1.46.0" + "mime-db": "1.52.0" } }, "mimic-fn": { @@ -3578,24 +3135,9 @@ } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "ms": { @@ -3613,22 +3155,10 @@ "path-key": "^3.0.0" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true }, "once": { @@ -3656,10 +3186,13 @@ "dev": true }, "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } }, "path-is-absolute": { "version": "1.0.1", @@ -3676,13 +3209,13 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, "pify": { @@ -3697,16 +3230,22 @@ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true + }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", "dev": true }, "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, "pump": { @@ -3720,52 +3259,26 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "ramda": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", - "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } + "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -3775,24 +3288,28 @@ "throttleit": "^1.0.0" } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - }, - "dependencies": { - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - } + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" } }, + "rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3803,12 +3320,12 @@ } }, "rxjs": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", - "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "requires": { - "tslib": "^1.9.0" + "tslib": "^2.1.0" } }, "safe-buffer": { @@ -3823,6 +3340,26 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3838,6 +3375,18 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -3845,15 +3394,20 @@ "dev": true }, "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } }, "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -3867,58 +3421,24 @@ "tweetnacl": "~0.14.0" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^5.0.1" } }, "strip-final-newline": { @@ -3936,18 +3456,18 @@ "has-flag": "^4.0.0" } }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true - }, "throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", "dev": true }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -3958,25 +3478,35 @@ } }, "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + } } }, "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "requires": { "safe-buffer": "^5.0.1" @@ -3985,15 +3515,22 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "optional": true + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -4006,49 +3543,26 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "dev": true, "requires": { "assert-plus": "^1.0.0", @@ -4066,46 +3580,14 @@ } }, "wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "wrappy": { @@ -4117,7 +3599,7 @@ "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "requires": { "buffer-crc32": "~0.2.3", diff --git a/tests/tests_e2e/package.json b/tests/tests_e2e/package.json index b63ee860b6..bf8dcb269d 100644 --- a/tests/tests_e2e/package.json +++ b/tests/tests_e2e/package.json @@ -10,7 +10,7 @@ "author": "", "license": "ISC", "devDependencies": { - "cypress": "^6.8.0", + "cypress": "^13.10.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21" } diff --git a/tests/tests_unit/cli_validate/min.happy.jupyterlab.gallery_settings.yaml b/tests/tests_unit/cli_validate/min.happy.jupyterlab.gallery_settings.yaml new file mode 100644 index 0000000000..02df930fdd --- /dev/null +++ b/tests/tests_unit/cli_validate/min.happy.jupyterlab.gallery_settings.yaml @@ -0,0 +1,10 @@ +project_name: test +jupyterlab: + gallery_settings: + title: Example repositories + destination: examples + exhibits: + - title: Nebari + git: https://github.com/nebari-dev/nebari.git + homepage: https://github.com/nebari-dev/nebari + description: 🪴 Nebari - your open source data science platform diff --git a/tests/tests_unit/test_cli_init.py b/tests/tests_unit/test_cli_init.py index 0cd0fe03d2..9afab5ddc5 100644 --- a/tests/tests_unit/test_cli_init.py +++ b/tests/tests_unit/test_cli_init.py @@ -51,6 +51,8 @@ (["--ssl-cert-email"], 2, ["requires an argument"]), (["--output"], 2, ["requires an argument"]), (["-o"], 2, ["requires an argument"]), + (["--explicit"], 2, ["Missing option"]), + (["-e"], 2, ["Missing option"]), ], ) def test_cli_init_stdout(args: List[str], exit_code: int, content: List[str]): @@ -90,20 +92,22 @@ def generate_test_data_test_cli_init_happy_path(): ) in get_kubernetes_versions(provider) + [ "latest" ]: - test_data.append( - ( - provider, - region, - project_name, - domain_name, - namespace, - auth_provider, - ci_provider, - terraform_state, - email, - kubernetes_version, + for explicit in [True, False]: + test_data.append( + ( + provider, + region, + project_name, + domain_name, + namespace, + auth_provider, + ci_provider, + terraform_state, + email, + kubernetes_version, + explicit, + ) ) - ) keys = [ "provider", @@ -116,6 +120,7 @@ def generate_test_data_test_cli_init_happy_path(): "terraform_state", "email", "kubernetes_version", + "explicit", ] return {"keys": keys, "test_data": test_data} @@ -131,6 +136,7 @@ def test_cli_init_happy_path( terraform_state: str, email: str, kubernetes_version: str, + explicit: bool, ): app = create_cli() args = [ @@ -159,6 +165,8 @@ def test_cli_init_happy_path( "--region", region, ] + if explicit: + args += ["--explicit"] expected_yaml = f""" provider: {provider} diff --git a/tests/tests_unit/test_init.py b/tests/tests_unit/test_init.py index 8d880162d3..7f81927275 100644 --- a/tests/tests_unit/test_init.py +++ b/tests/tests_unit/test_init.py @@ -15,7 +15,7 @@ ], ) def test_render_config(mock_all_cloud_methods, k8s_version, cloud_provider, expected): - if type(expected) == type and issubclass(expected, Exception): + if type(expected) is type and issubclass(expected, Exception): with pytest.raises(expected): config = render_config( project_name="test", diff --git a/tests/tests_unit/test_schema.py b/tests/tests_unit/test_schema.py index 446b6d1085..fa6a0c747c 100644 --- a/tests/tests_unit/test_schema.py +++ b/tests/tests_unit/test_schema.py @@ -139,7 +139,7 @@ def test_multiple_providers(config_schema): config_schema(**config_dict) -def test_aws_premissions_boundary(config_schema): +def test_aws_permissions_boundary(config_schema): permissions_boundary = "arn:aws:iam::123456789012:policy/MyBoundaryPolicy" config_dict = { "project_name": "test", @@ -156,7 +156,7 @@ def test_aws_premissions_boundary(config_schema): @pytest.mark.parametrize("provider", ["local", "existing"]) -def test_setted_provider(config_schema, provider): +def test_set_provider(config_schema, provider): config_dict = { "project_name": "test", "provider": provider, diff --git a/tests/tests_unit/test_upgrade.py b/tests/tests_unit/test_upgrade.py index 4871a1fe07..a19095726b 100644 --- a/tests/tests_unit/test_upgrade.py +++ b/tests/tests_unit/test_upgrade.py @@ -2,6 +2,7 @@ from pathlib import Path import pytest +from rich.prompt import Prompt from _nebari.upgrade import do_upgrade from _nebari.version import __version__, rounded_ver_parse @@ -48,8 +49,29 @@ def test_upgrade_4_0( qhub_users_import_json, monkeypatch, ): - # Return "y" when asked if you've deleted the Argo CRDs - monkeypatch.setattr("builtins.input", lambda: "y") + + def mock_input(prompt, **kwargs): + # Mock different upgrade steps prompt answers + if ( + prompt + == "Have you deleted the Argo Workflows CRDs and service accounts? [y/N] " + ): + return "y" + elif ( + prompt + == "\nDo you want Nebari to update the kube-prometheus-stack CRDs and delete the prometheus-node-exporter for you? If not, you'll have to do it manually." + ): + return "N" + elif ( + prompt + == "Have you backed up your custom dashboards (if necessary), deleted the prometheus-node-exporter daemonset and updated the kube-prometheus-stack CRDs?" + ): + return "y" + # All other prompts will be answered with "y" + else: + return "y" + + monkeypatch.setattr(Prompt, "ask", mock_input) old_qhub_config_path = Path(__file__).parent / old_qhub_config_path_str diff --git a/tests/tests_unit/test_utils.py b/tests/tests_unit/test_utils.py new file mode 100644 index 0000000000..c2ae2d4965 --- /dev/null +++ b/tests/tests_unit/test_utils.py @@ -0,0 +1,44 @@ +import pytest + +from _nebari.utils import byte_unit_conversion + + +@pytest.mark.parametrize( + "value, from_unit, to_unit, expected", + [ + (1, "", "B", 1), + (1, "B", "B", 1), + (1, "KB", "B", 1000), + (1, "K", "B", 1000), + (1, "k", "b", 1000), + (1, "MB", "B", 1000**2), + (1, "GB", "B", 1000**3), + (1, "TB", "B", 1000**4), + (1, "KiB", "B", 1024), + (1, "MiB", "B", 1024**2), + (1, "GiB", "B", 1024**3), + (1, "TiB", "B", 1024**4), + (1000, "B", "KB", 1), + (1000, "KB", "K", 1000), + (1000, "K", "KB", 1000), + (1000, "MB", "KB", 1000**2), + (1000, "GB", "KB", 1000**3), + (1000, "TB", "KB", 1000**4), + (1000, "KiB", "KB", 1024), + (1000, "Ki", "KB", 1024), + (1000, "Ki", "K", 1024), + (1000, "MiB", "KB", 1024**2), + (1000, "GiB", "KB", 1024**3), + (1000, "TiB", "KB", 1024**4), + (1000**2, "B", "MB", 1), + (1000**2, "KB", "MB", 1000), + (1000**2, "MB", "MB", 1000**2), + (1000**2, "GB", "MB", 1000**3), + (1000**2, "TB", "MB", 1000**4), + (1000**2, "MiB", "MB", 1024**2), + (1000**3, "B", "GB", 1), + (1000**3, "KB", "GB", 1000), + ], +) +def test_byte_unit_conversion(value, from_unit, to_unit, expected): + assert byte_unit_conversion(f"{value} {from_unit}", to_unit) == expected