diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000..b0b310bd42 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,106 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +name: Tests + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + schedule: + - cron: '53 0 * * *' # Daily at 00:53 UTC + # Triggered on push to branch "main" by .github/workflows/release.yaml + workflow_call: + +jobs: + lint: + name: Lint + uses: canonical/data-platform-workflows/.github/workflows/lint.yaml@main + + unit-test: + name: Unit test charm + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install tox & poetry + run: | + pipx install tox + pipx install poetry + - name: Run tests + run: tox run -e unit + + + lib-check: + name: Check libraries + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - run: | + # Workaround for https://github.com/canonical/charmcraft/issues/1389#issuecomment-1880921728 + touch requirements.txt + - name: Check libs + uses: canonical/charming-actions/check-libraries@2.4.0 + with: + credentials: ${{ secrets.CHARMHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + use-labels: false + fail-build: ${{ github.event_name == 'pull_request' }} + + build: + name: Build charm + strategy: + fail-fast: false + matrix: + paths: + - to-charm-directory: "." + - to-charm-directory: "./tests/integration/relations/opensearch_provider/application-charm/" + uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@main + with: + path-to-charm-directory: ${{ matrix.paths.to-charm-directory }} + charmcraft-snap-channel: beta + cache: true + + integration-test: + strategy: + fail-fast: false + matrix: + juju: + - agent: 3.3.1 + libjuju: 3.3.0.0 + juju-snap: 3.3/stable + - agent: 3.2.4 + libjuju: ^3 + juju-snap: 3.2/stable + - agent: 3.1.7 + libjuju: ^3 + juju-snap: 3.1/stable + - agent: 3.1.6 + libjuju: ^3 + juju-snap: 3.1/stable + name: Integration test charm | ${{ matrix.juju.agent }} + needs: + - lint + - unit-test + - build + uses: canonical/data-platform-workflows/.github/workflows/integration_test_charm.yaml@main + with: + artifact-prefix: ${{ needs.build.outputs.artifact-prefix }} + cloud: lxd + juju-agent-version: ${{ matrix.juju.agent }} + libjuju-version-constraint: ${{ matrix.juju.libjuju }} + secrets: + integration-test: | + { + "AWS_ACCESS_KEY": "${{ secrets.AWS_ACCESS_KEY }}", + "AWS_SECRET_KEY": "${{ secrets.AWS_SECRET_KEY }}", + "GCP_ACCESS_KEY": "${{ secrets.GCP_ACCESS_KEY }}", + "GCP_SECRET_KEY": "${{ secrets.GCP_SECRET_KEY }}", + "GCP_SERVICE_ACCOUNT": "${{ secrets.GCP_SERVICE_ACCOUNT }}", + } diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml deleted file mode 100644 index c29be9e4d7..0000000000 --- a/.github/workflows/integration.yaml +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -on: - workflow_call: - inputs: - juju-snap-channel: - description: Snap channel for Juju CLI - default: 3.1/stable - type: string - libjuju-version-specifier: - description: | - python-libjuju version specifier (e.g. ">=1.3") - https://packaging.python.org/en/latest/glossary/#term-Version-Specifier - required: false - type: string -# secrets: -# integration-test: -# description: | -# Secrets needed in integration tests -# -# Passed to tests with `SECRETS_FROM_GITHUB` environment variable -# -# Use a string representation of a Python dict[str, str] built from multiple GitHub secrets -# Do NOT put the string into a single GitHub secret—build the string from multiple GitHub secrets so that GitHub is more likely to redact the secrets in GitHub Actions logs. -# -# Python code to verify the string format: -# ``` -# import ast -# secrets = ast.literal_eval("") -# assert isinstance(secrets, dict) -# for key, value in secrets.items(): -# assert isinstance(key, str) and isinstance(value, str) -# ``` -# required: false - -jobs: - build: - name: Build charms - uses: canonical/data-platform-workflows/.github/workflows/build_charms_with_cache.yaml@v2 - - integration-test: - needs: - - build - strategy: - fail-fast: false - matrix: - tox-environments: - - charm-integration - - tls-integration - - client-integration - - ha-base-integration - - ha-networking-integration - - ha-multi-clusters-integration - - large-deployments-integration - - plugins-integration -# - ha-backup-integration - runner: ["ubuntu-22.04"] - include: - - tox-environments: h-scaling-integration - runner: "Ubuntu_4C_16G" - name: ${{ matrix.tox-environments }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 360 - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Upgrade LXD - run: | - sudo snap refresh lxd --channel=latest/stable - - - name: Setup Juju environment - # Runs on juju 3 by default - # TODO: Replace with custom image on self-hosted runner - uses: charmed-kubernetes/actions-operator@main - with: - provider: lxd - juju-channel: ${{ inputs.juju-snap-channel }} - - - name: Download packed charm(s) - uses: actions/download-artifact@v3 - with: - name: ${{ needs.build.outputs.artifact-name }} - -# - name: Install CLI -# run: | -# sudo apt update -# sudo apt install -y pipx -# pipx install git+https://github.com/canonical/data-platform-workflows#subdirectory=python/cli -# - name: Redact secrets from log -# run: redact-secrets -# env: -# SECRETS: ${{ secrets.integration-test }} - - - name: Select tests - id: select-tests - run: | - if [ "${{ github.event_name }}" == "schedule" ] - then - echo Running unstable and stable tests - echo "mark_expression=" >> $GITHUB_OUTPUT - else - echo Skipping unstable tests - echo "mark_expression=not unstable" >> $GITHUB_OUTPUT - fi - - - name: Run integration tests - run: | - # free space in the runner - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - # Set kernel params for OpenSearch - sudo sysctl -w vm.max_map_count=262144 vm.swappiness=0 net.ipv4.tcp_retries2=5 - # Set kernel params for testing - sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.default.disable_ipv6=1 net.ipv6.conf.all.autoconf=0 - tox run -e ${{ matrix.tox-environments }} -- -m '${{ steps.select-tests.outputs.mark_expression }}' - env: - CI_PACKED_CHARMS: ${{ needs.build.outputs.charms }} - LIBJUJU_VERSION_SPECIFIER: ${{ inputs.libjuju-version-specifier }} - SECRETS_FROM_GITHUB: "" -# SECRETS_FROM_GITHUB: ${{ secrets.integration-test }} - - - - backup-microceph-integration-test: - needs: - - build - name: backup-microceph-integration-test - runs-on: "ubuntu-22.04" - timeout-minutes: 360 - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Upgrade LXD - run: | - sudo snap refresh lxd --channel=latest/stable - - - name: Setup Juju environment - # Runs on juju 3 by default - # TODO: Replace with custom image on self-hosted runner - uses: charmed-kubernetes/actions-operator@main - with: - provider: lxd - juju-channel: ${{ inputs.juju-snap-channel }} - - - name: Download packed charm(s) - uses: actions/download-artifact@v3 - with: - name: ${{ needs.build.outputs.artifact-name }} - - - name: Free space in the runner - id: free-space-runner - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - - name: Select tests - id: select-tests - run: | - if [ "${{ github.event_name }}" == "schedule" ] - then - echo Running unstable and stable tests - echo "mark_expression=" >> $GITHUB_OUTPUT - else - echo Skipping unstable tests - echo "mark_expression=not unstable" >> $GITHUB_OUTPUT - fi - - - name: Run backup integration - run: | - sudo sysctl -w vm.max_map_count=262144 vm.swappiness=0 net.ipv4.tcp_retries2=5 - tox run -e ha-backup-integration -- -m '${{ steps.select-tests.outputs.mark_expression }}' - env: - CI_PACKED_CHARMS: ${{ needs.build.outputs.charms }} - LIBJUJU_VERSION_SPECIFIER: ${{ inputs.libjuju-version-specifier }} - -# - name: Run backup test with AWS -# run: tox run -e ha-backup-integration -- -m '${{ steps.select-tests.outputs.mark_expression }}' -# env: -# CI_PACKED_CHARMS: ${{ needs.build.outputs.charms }} -# LIBJUJU_VERSION_SPECIFIER: ${{ matrix.libjuju-version }} -# SECRETS_FROM_GITHUB: | -# { -# "AWS_ACCESS_KEY": "${{ secrets.AWS_ACCESS_KEY }}", -# "AWS_SECRET_KEY": "${{ secrets.AWS_SECRET_KEY }}", -# } -# - name: Run backup test with GCP -# run: tox run -e ha-backup-integration -- -m '${{ steps.select-tests.outputs.mark_expression }}' -# env: -# CI_PACKED_CHARMS: ${{ needs.build.outputs.charms }} -# LIBJUJU_VERSION_SPECIFIER: ${{ matrix.libjuju-version }} -# SECRETS_FROM_GITHUB: | -# { -# "GCP_ACCESS_KEY": "${{ secrets.GCP_ACCESS_KEY }}", -# "GCP_SECRET_KEY": "${{ secrets.GCP_SECRET_KEY }}", -# } diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml deleted file mode 100644 index 012b8db0d5..0000000000 --- a/.github/workflows/pr.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -name: PR CI - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - pull_request: - -jobs: - pre-integration-checks: - secrets: inherit - uses: ./.github/workflows/pre_integration_checks.yaml - with: - libjuju-version-specifier: ==3.3.0.0 - - integration: - needs: - - pre-integration-checks - uses: ./.github/workflows/integration.yaml - with: - libjuju-version-specifier: ==3.3.0.0 diff --git a/.github/workflows/pre_integration_checks.yaml b/.github/workflows/pre_integration_checks.yaml deleted file mode 100644 index 373e2c3704..0000000000 --- a/.github/workflows/pre_integration_checks.yaml +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -name: Pre-integration checks - -on: - workflow_call: - inputs: - libjuju-version-specifier: - description: | - python-libjuju version specifier (e.g. ">=1.3") - https://packaging.python.org/en/latest/glossary/#term-Version-Specifier - required: false - type: string - -jobs: - lint: - name: Lint - runs-on: ubuntu-22.04 - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install tox - # TODO: Consider replacing with custom image on self-hosted runner OR pinning version - run: python3 -m pip install tox - - name: Run linters - run: tox run -e lint - - unit-test: - name: Unit tests - runs-on: ubuntu-22.04 - timeout-minutes: 5 - needs: - - lint - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install tox - # TODO: Consider replacing with custom image on self-hosted runner OR pinning version - run: python3 -m pip install tox - - name: Run tests - run: tox run -e unit - env: - LIBJUJU_VERSION_SPECIFIER: ${{ inputs.libjuju-version-specifier }} - - lib-check: - name: Check libraries - runs-on: ubuntu-22.04 - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Check libs - uses: canonical/charming-actions/check-libraries@2.1.1 - with: - credentials: "${{ secrets.CHARMHUB_TOKEN }}" # FIXME: current token will expire in 2023-07-04 - github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9ca3bc83d6..58c0288df7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,48 +8,46 @@ on: - main jobs: - pre-integration-checks: + ci-tests: + name: Tests + uses: ./.github/workflows/ci.yaml secrets: inherit - strategy: - fail-fast: false - matrix: - include: - - libjuju: ==3.3.0.0 - uses: ./.github/workflows/pre_integration_checks.yaml - with: - libjuju-version-specifier: ${{ matrix.libjuju }} + permissions: + actions: write # Needed to manage GitHub Actions cache - integration: - needs: - - pre-integration-checks - strategy: - fail-fast: false - matrix: - include: - - snap: 3.1/stable - libjuju: ==3.3.0.0 - uses: ./.github/workflows/integration.yaml + # release-libraries: + # name: Release libraries + # needs: + # - ci-tests + # runs-on: ubuntu-latest + # timeout-minutes: 60 + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # with: + # fetch-depth: 0 + # - name: Release charm libraries + # uses: canonical/charming-actions/release-libraries@2.3.0 + # with: + # credentials: ${{ secrets.CHARMHUB_TOKEN }} + # github-token: ${{ secrets.GITHUB_TOKEN }} + + build: + name: Build charm + uses: canonical/data-platform-workflows/.github/workflows/build_charm.yaml@main with: - juju-snap-channel: ${{ matrix.snap }} - libjuju-version-specifier: ${{ matrix.libjuju }} + charmcraft-snap-channel: beta - release-to-charmhub: - name: Release to CharmHub + release: + name: Release charm needs: - - pre-integration-checks - - integration - runs-on: ubuntu-22.04 - timeout-minutes: 60 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Select charmhub channel - uses: canonical/charming-actions/channel@2.1.1 - id: channel - - name: Upload charm to charmhub - uses: canonical/charming-actions/upload-charm@2.1.1 - with: - credentials: "${{ secrets.CHARMHUB_TOKEN }}" - github-token: "${{ secrets.GITHUB_TOKEN }}" - channel: "2/edge" + - build + uses: canonical/data-platform-workflows/.github/workflows/release_charm.yaml@v0.0.0 + with: + channel: latest/edge + artifact-prefix: ${{ needs.build.outputs.artifact-prefix }} + secrets: + charmhub-token: ${{ secrets.CHARMHUB_TOKEN }} + permissions: + contents: write # Needed to create GitHub release diff --git a/.github/workflows/scheduled_tests.yaml b/.github/workflows/scheduled_tests.yaml deleted file mode 100644 index 10507a6b31..0000000000 --- a/.github/workflows/scheduled_tests.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -name: Scheduled CI - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - schedule: - - cron: '53 0 * * *' # Daily at 00:53 UTC - -jobs: - pre-integration-checks: - secrets: inherit - uses: ./.github/workflows/pre_integration_checks.yaml - - integration: - needs: - - pre-integration-checks - strategy: - fail-fast: false - matrix: - include: - - snap: 3.1/stable - libjuju: ==3.3.0.0 - - snap: 2.9/stable - libjuju: ==2.9.44.0 - uses: ./.github/workflows/integration.yaml - with: - juju-snap-channel: ${{ matrix.snap }} - libjuju-version-specifier: ${{ matrix.libjuju }} diff --git a/.gitignore b/.gitignore index f1b819d2b1..51ccad860b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ __pycache__/ *.tar.xz cloudinit-userdata.yaml /.pytest_cache/ + +# Moving to Poetry, we do not need this file to be pushed any longer +/requirements.txt +/requirements-last-build.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72e5cde063..7d16019f4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,41 @@ this operator. - Please help us out in ensuring easy to review branches by rebasing your pull request branch onto the `main` branch. This also avoids merge commits and creates a linear Git commit history. + +## Build charm + +Build the charm in this git repository using tox. + +There are two alternatives to build the charm: using the charm cache or not. +Cache will speed the build by downloading all dependencies from charmcraftcache-hub. + +### Build Without Cache + +To run the traditional build only using `charmcraft`, run the following command: + +```shell +tox -e build-production +``` + +### Build With Cache + +First, ensure you have the right dependencies: +* charmcraft v2.5.4+ +* charmcraftcache + +By running the following commands: + +```shell +pip install charmcraftcache +sudo snap refresh charmcraft --channel=latest/beta +``` + +Then, start the build: + +```shell +tox -e build-dev +``` + ## Developing You can create an environment for development with `tox`: @@ -32,41 +67,67 @@ source venv/bin/activate ### Testing -To run tests, run the following +To run tests, first build the charm as described above, then run the following ```shell tox -e format # update your code according to linting rules tox -e lint # code style tox -e unit # unit tests -tox -m integration # integration tests, running on juju 2. +tox -m integration # integration tests, running on juju 3. tox # runs 'format', 'lint', and 'unit' environments ``` -Integration tests can be run for separate areas of functionality: +Integration tests can be run for separate files: ```shell -tox -e charm-integration # basic charm integration tests -tox -e tls-integration # TLS-specific integration tests -tox -e client-integration # Validates the `opensearch-client` integration -tox -e ha-integration # HA tests -tox -e h-scaling-integration # HA tests specific to horizontal scaling +tox -e integration -- tests/integration/ha/test_storage.py ``` -If you're running tests on juju 3, run the following command to change libjuju to the correct version: +#### Running different versions of libjuju + +To try different versions of libjuju, run: ```shell -export LIBJUJU_VERSION_SPECIFIER="==3.3.0.0" +poetry add --lock --group integration,unit juju@ ``` -## Build charm +#### Testing Backups -Build the charm in this git repository using: +Backup testing installs microceph and, optionally, can run on AWS and GCP object stores. +For that, pass the access / secret / service account information as env. variables. + +To run the test only against microceph: ```shell -charmcraft pack +tox -e integration -- tests/integration/ha/test_backups.py --group='microceph' # test backup service +``` + +And against public clouds + microceph: + +```shell +SECRETS_FROM_GITHUB=$(cat /credentials.json) tox -e integration -- tests/integration/ha/test_backups.py +``` + +Where, for AWS only, `credentials.json` should look like (the `IGNORE` is necessary to remove GCP): +```json +{ "AWS_ACCESS_KEY": ..., "AWS_SECRET_KEY": ..., "GCP_SECRET_KEY": "IGNORE" } ``` -### Deploy +For GCP only, some additional information is needed. The pytest script runs with `boto3` and needs `GCP_{ACCESS,SECRET}_KEY` to clean up the object store; whereas OpenSearch uses the `service account` json file. + +```json +{ "AWS_SECRET_KEY": "IGNORE", "GCP_ACCESS_KEY": ..., "GCP_SECRET_KEY": ..., "GCP_SERVICE_ACCOUNT": ... } +``` + +Combine both if AWS and GCP are to be used. + + +#### Cleaning Test Environment + +Remove the following files / folders: `poetry.lock`, `.tox`, `requirements*`. + + +## Deploy OpenSearch has a set of system requirements to correctly function, you can find the list [here](https://opensearch.org/docs/latest/install-and-configure/install-opensearch/index/). Some of those settings must be set using cloudinit-userdata on the model, while others must be set on the host machine: diff --git a/charmcraft.yaml b/charmcraft.yaml index bd045e1edd..e55daa571d 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -3,15 +3,26 @@ type: charm bases: + # Whenever "bases" is changed: + # - Update tests/integration/conftest.py::pytest_configure() + # - Update .github/workflow/ci.yaml integration-test matrix - build-on: - name: "ubuntu" channel: "22.04" run-on: - name: "ubuntu" channel: "22.04" - parts: charm: + override-pull: | + craftctl default + if [[ ! -f requirements.txt ]] + then + echo 'ERROR: Use "tox run -e build-dev" instead of calling "charmcraft pack" directly' >&2 + exit 1 + fi + charm-strict-dependencies: true + charm-entrypoint: src/charm.py build-packages: - cargo - libffi-dev diff --git a/pyproject.toml b/pyproject.toml index 44aa33b16d..eec6c956b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,92 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -# Testing tools configuration +[tool.poetry] +# Charm is not packed as a standard Python package; this information is not used +name = "charm" +version = "0.1.0" +description = "" +authors = [] + +[tool.poetry.dependencies] +python = "^3.10" +ops = "^2.5.0" +tenacity = "^8.2.2" +boto3 = "^1.28.22" +jinja2 = "^3.1.2" +overrides = "7.4.0" +requests = "2.31.0" +# Official name: ruamel.yaml, but due to Poetry GH#109 - replace dots with dashs +ruamel-yaml = "0.17.35" +shortuuid = "1.0.11" +jproperties = "2.1.1" +pydantic = "^1.10, <2" +cryptography = "^42.0.2" +jsonschema = "^4.21.1" + + +[tool.poetry.group.charm-libs.dependencies] +# data_platform_libs/v0/data_interfaces.py +ops = ">=2.0.0" +# data_platform_libs/v0/upgrade.py +# grafana_agent/v0/cos_agent.py requires pydantic <2 +poetry-core = "^1.8.1" +pydantic = "^1.10, <2" +# tls_certificates_interface/v1/tls_certificates.py +cryptography = "^42.0.2" +jsonschema = "^4.21.1" +# grafana_agent/v0/cos_agent.py +cosl = ">=0.0.7" +bcrypt = "4.0.1" + +[tool.poetry.group.format] +optional = true + +[tool.poetry.group.format.dependencies] +black = "^23.7.0" +isort = "^5.12.0" + +[tool.poetry.group.lint] +optional = true + +[tool.poetry.group.lint.dependencies] +black = "^23.7.0" +isort = "^5.12.0" +flake8 = "^6.0.0" +flake8-docstrings = "^1.7.0" +flake8-copyright = "^0.2.4" +flake8-builtins = "^2.1.0" +pyproject-flake8 = "^6.0.0.post1" +pep8-naming = "^0.13.3" +codespell = "^2.2.5" +shellcheck-py = "^0.9.0.5" + +[tool.poetry.group.unit.dependencies] +juju = "^3.2.2" +pytest = "^7.4.0" +pytest-asyncio = "<0.23" +pytest-mock = "^3.11.1" +coverage = {extras = ["toml"], version = "^7.4.1"} +parameterized = "^0.9.0" + +[tool.poetry.group.integration.dependencies] +boto3 = "^1.28.23" +pytest = "^7.4.0" +pytest-github-secrets = {git = "https://github.com/canonical/data-platform-workflows", branch = "main", subdirectory = "python/pytest_plugins/github_secrets"} +pytest-asyncio = "<0.23" +pytest-operator = "^0.32.0" +pytest-operator-cache = {git = "https://github.com/canonical/data-platform-workflows", branch = "main", subdirectory = "python/pytest_plugins/pytest_operator_cache"} +pytest-operator-groups = {git = "https://github.com/canonical/data-platform-workflows", branch = "main", subdirectory = "python/pytest_plugins/pytest_operator_groups"} +pytest-microceph = {git = "https://github.com/canonical/data-platform-workflows", branch = "main", subdirectory = "python/pytest_plugins/microceph"} +juju = "^3.2.2" +ops = "^2.5.0" +pytest-mock = "^3.11.1" +tenacity = "^8.2.2" +pyyaml = "^6.0.1" +urllib3 = "^1.26.16" +protobuf = "3.20.0" +opensearch-py = "^2.4.2" + [tool.coverage.run] branch = true @@ -11,8 +96,8 @@ show_missing = true [tool.pytest.ini_options] minversion = "6.0" log_cli_level = "INFO" -asyncio_mode = "auto" markers = ["unstable"] +asyncio_mode = "auto" # Formatting tools configuration [tool.black] diff --git a/requirements.txt b/requirements.txt index c1befdf4ba..cea8cd1454 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,577 @@ -bcrypt==4.1.2 -cosl==0.0.7 -cryptography==42.0.1 -jproperties==2.1.1 -jsonschema==4.21.1 -ops==2.9.0 -overrides==7.6.0 -parameterized==0.9.0 -pydantic==1.10.14 -pytest-mock==3.12.0 -requests==2.31.0 -ruamel.yaml==0.18.5 -shortuuid==1.0.11 -tenacity==8.2.3 -urllib3==2.1.0 +attrs==23.2.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ + --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 +bcrypt==4.0.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535 \ + --hash=sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0 \ + --hash=sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410 \ + --hash=sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd \ + --hash=sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665 \ + --hash=sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab \ + --hash=sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71 \ + --hash=sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215 \ + --hash=sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b \ + --hash=sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda \ + --hash=sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9 \ + --hash=sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a \ + --hash=sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344 \ + --hash=sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f \ + --hash=sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d \ + --hash=sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c \ + --hash=sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c \ + --hash=sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2 \ + --hash=sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d \ + --hash=sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e \ + --hash=sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3 +boto3==1.34.35 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:4052031f9ac18924e94be7c30a5f0af5843a4752b8c8cb9034cd978be252b61b \ + --hash=sha256:53897701ab4f307fbcfdade673eae809dfc5eabb6102053c84907aa27de66e53 +botocore==1.34.35 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:8a2b53ab772584a5f7e2fe1e4a59028b0602cfef8e39d622db7c6b670e4b1ee6 \ + --hash=sha256:b67b8c865973202dc655a493317ae14b33d115e49ed6960874eb05d950167b37 +certifi==2024.2.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ + --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 +cffi==1.16.0 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy" \ + --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ + --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ + --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ + --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ + --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ + --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ + --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ + --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ + --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ + --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ + --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ + --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ + --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ + --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ + --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ + --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ + --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ + --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ + --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ + --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ + --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ + --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ + --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ + --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ + --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ + --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ + --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ + --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ + --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ + --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ + --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ + --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ + --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ + --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ + --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ + --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ + --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ + --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ + --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ + --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ + --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ + --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ + --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ + --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ + --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ + --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ + --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ + --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ + --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ + --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ + --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ + --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 +charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ + --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ + --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ + --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ + --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ + --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ + --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ + --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ + --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ + --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ + --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ + --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ + --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ + --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ + --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ + --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ + --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ + --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ + --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ + --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ + --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ + --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ + --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ + --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ + --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ + --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ + --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ + --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ + --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ + --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ + --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ + --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ + --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ + --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ + --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ + --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ + --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ + --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ + --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ + --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ + --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ + --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ + --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ + --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ + --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ + --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ + --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ + --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ + --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ + --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ + --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ + --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ + --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ + --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ + --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ + --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ + --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ + --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ + --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ + --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ + --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ + --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ + --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ + --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ + --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ + --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ + --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ + --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ + --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ + --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ + --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ + --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ + --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ + --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ + --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ + --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ + --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ + --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ + --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ + --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ + --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ + --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ + --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ + --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ + --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ + --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ + --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ + --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ + --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ + --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 +cosl==0.0.7 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:ed7cf980b47f4faa0e65066d65e5b4274f1972fb6cd3533441a90edae360b4a7 \ + --hash=sha256:edf07a81d152720c3ee909a1201063e5b1a35c49f574a7ec1deb989a8bc6fada +cryptography==42.0.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380 \ + --hash=sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589 \ + --hash=sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea \ + --hash=sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65 \ + --hash=sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a \ + --hash=sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3 \ + --hash=sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008 \ + --hash=sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1 \ + --hash=sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2 \ + --hash=sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635 \ + --hash=sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2 \ + --hash=sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90 \ + --hash=sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee \ + --hash=sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a \ + --hash=sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242 \ + --hash=sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12 \ + --hash=sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2 \ + --hash=sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d \ + --hash=sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be \ + --hash=sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee \ + --hash=sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6 \ + --hash=sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529 \ + --hash=sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929 \ + --hash=sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1 \ + --hash=sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6 \ + --hash=sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a \ + --hash=sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446 \ + --hash=sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9 \ + --hash=sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888 \ + --hash=sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4 \ + --hash=sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33 \ + --hash=sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f +idna==3.6 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +jinja2==3.1.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ + --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 +jmespath==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ + --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe +jproperties==2.1.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:40b71124e8d257e8954899a91cd2d5c0f72e0f67f1b72048a5ba264567604f29 \ + --hash=sha256:4dfcd7cab56d9c79bce4453f7ca9ffbe0ff0574ddcf1c2a99a8646df60634664 +jsonschema-specifications==2023.12.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ + --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c +jsonschema==4.21.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f \ + --hash=sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5 +markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ + --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ + --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ + --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ + --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ + --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ + --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ + --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ + --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ + --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ + --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ + --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ + --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ + --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ + --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ + --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ + --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ + --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ + --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ + --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ + --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ + --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ + --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ + --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ + --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ + --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ + --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ + --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ + --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ + --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ + --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ + --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ + --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ + --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ + --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ + --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ + --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ + --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ + --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ + --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ + --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ + --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ + --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ + --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ + --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ + --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ + --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ + --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ + --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ + --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ + --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ + --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ + --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ + --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ + --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ + --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ + --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ + --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ + --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ + --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 +ops==2.10.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:25ecac7054e53b500d1ea6cccf47de4ed69d39de4eaf9a2bc05d49a12f8651bf \ + --hash=sha256:7b0bf23c7ae20891d3fd7943830d75f1b38e7cafb216d5b727771d9c2f69d654 +overrides==7.4.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d \ + --hash=sha256:9502a3cca51f4fac40b5feca985b6703a5c1f6ad815588a7ca9e285b9dca6757 +poetry-core==1.9.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:4e0c9c6ad8cf89956f03b308736d84ea6ddb44089d16f2adc94050108ec1f5a1 \ + --hash=sha256:fa7a4001eae8aa572ee84f35feb510b321bd652e5cf9293249d62853e1f935a2 +pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy" \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +pydantic==1.10.14 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8 \ + --hash=sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f \ + --hash=sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f \ + --hash=sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593 \ + --hash=sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046 \ + --hash=sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9 \ + --hash=sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf \ + --hash=sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea \ + --hash=sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022 \ + --hash=sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca \ + --hash=sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f \ + --hash=sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6 \ + --hash=sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597 \ + --hash=sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f \ + --hash=sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee \ + --hash=sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c \ + --hash=sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7 \ + --hash=sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e \ + --hash=sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054 \ + --hash=sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d \ + --hash=sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87 \ + --hash=sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c \ + --hash=sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7 \ + --hash=sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5 \ + --hash=sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663 \ + --hash=sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01 \ + --hash=sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe \ + --hash=sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc \ + --hash=sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee \ + --hash=sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4 \ + --hash=sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c \ + --hash=sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347 \ + --hash=sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a \ + --hash=sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f \ + --hash=sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a \ + --hash=sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b +python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ + --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 +pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f +referencing==0.33.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5 \ + --hash=sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7 +requests==2.31.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ + --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +rpds-py==0.17.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147 \ + --hash=sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7 \ + --hash=sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2 \ + --hash=sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68 \ + --hash=sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1 \ + --hash=sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382 \ + --hash=sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d \ + --hash=sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921 \ + --hash=sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38 \ + --hash=sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4 \ + --hash=sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a \ + --hash=sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d \ + --hash=sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518 \ + --hash=sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e \ + --hash=sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d \ + --hash=sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf \ + --hash=sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5 \ + --hash=sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba \ + --hash=sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6 \ + --hash=sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59 \ + --hash=sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253 \ + --hash=sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6 \ + --hash=sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f \ + --hash=sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3 \ + --hash=sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea \ + --hash=sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1 \ + --hash=sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76 \ + --hash=sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93 \ + --hash=sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad \ + --hash=sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad \ + --hash=sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc \ + --hash=sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049 \ + --hash=sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d \ + --hash=sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90 \ + --hash=sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d \ + --hash=sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd \ + --hash=sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25 \ + --hash=sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2 \ + --hash=sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f \ + --hash=sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6 \ + --hash=sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4 \ + --hash=sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c \ + --hash=sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8 \ + --hash=sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d \ + --hash=sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b \ + --hash=sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19 \ + --hash=sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453 \ + --hash=sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9 \ + --hash=sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde \ + --hash=sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296 \ + --hash=sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58 \ + --hash=sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec \ + --hash=sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99 \ + --hash=sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a \ + --hash=sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb \ + --hash=sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383 \ + --hash=sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d \ + --hash=sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896 \ + --hash=sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc \ + --hash=sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6 \ + --hash=sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b \ + --hash=sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7 \ + --hash=sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22 \ + --hash=sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf \ + --hash=sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394 \ + --hash=sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0 \ + --hash=sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57 \ + --hash=sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74 \ + --hash=sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83 \ + --hash=sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29 \ + --hash=sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9 \ + --hash=sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f \ + --hash=sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745 \ + --hash=sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb \ + --hash=sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811 \ + --hash=sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55 \ + --hash=sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342 \ + --hash=sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23 \ + --hash=sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82 \ + --hash=sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041 \ + --hash=sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb \ + --hash=sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066 \ + --hash=sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55 \ + --hash=sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6 \ + --hash=sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a \ + --hash=sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140 \ + --hash=sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b \ + --hash=sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9 \ + --hash=sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256 \ + --hash=sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c \ + --hash=sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772 \ + --hash=sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4 \ + --hash=sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae \ + --hash=sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920 \ + --hash=sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a \ + --hash=sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b \ + --hash=sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361 \ + --hash=sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8 \ + --hash=sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a +ruamel-yaml-clib==0.2.8 ; platform_python_implementation == "CPython" and python_version < "3.13" and python_version >= "3.10" \ + --hash=sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d \ + --hash=sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001 \ + --hash=sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462 \ + --hash=sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9 \ + --hash=sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe \ + --hash=sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b \ + --hash=sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b \ + --hash=sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615 \ + --hash=sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62 \ + --hash=sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15 \ + --hash=sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b \ + --hash=sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1 \ + --hash=sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9 \ + --hash=sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675 \ + --hash=sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899 \ + --hash=sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7 \ + --hash=sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7 \ + --hash=sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312 \ + --hash=sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa \ + --hash=sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91 \ + --hash=sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b \ + --hash=sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6 \ + --hash=sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3 \ + --hash=sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334 \ + --hash=sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5 \ + --hash=sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3 \ + --hash=sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe \ + --hash=sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c \ + --hash=sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed \ + --hash=sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337 \ + --hash=sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880 \ + --hash=sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f \ + --hash=sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d \ + --hash=sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248 \ + --hash=sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d \ + --hash=sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf \ + --hash=sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512 \ + --hash=sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069 \ + --hash=sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb \ + --hash=sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942 \ + --hash=sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d \ + --hash=sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31 \ + --hash=sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92 \ + --hash=sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5 \ + --hash=sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28 \ + --hash=sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d \ + --hash=sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1 \ + --hash=sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2 \ + --hash=sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875 \ + --hash=sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412 +ruamel-yaml==0.17.35 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:801046a9caacb1b43acc118969b49b96b65e8847f29029563b29ac61d02db61b \ + --hash=sha256:b105e3e6fc15b41fdb201ba1b95162ae566a4ef792b9f884c46b4ccc5513a87a +s3transfer==0.10.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e \ + --hash=sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b +shortuuid==1.0.11 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:27ea8f28b1bd0bf8f15057a3ece57275d2059d2b0bb02854f02189962c13b6aa \ + --hash=sha256:fc75f2615914815a8e4cb1501b3a513745cb66ef0fd5fc6fb9f8c3fa3481f789 +six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +tenacity==8.2.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a \ + --hash=sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c +typing-extensions==4.9.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd +urllib3==1.26.18 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ + --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 +websocket-client==1.7.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6 \ + --hash=sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 60fdaf9472..0000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -import os -from unittest.mock import PropertyMock - -import pytest -from ops import JujuVersion -from pytest_mock import MockerFixture - - -@pytest.fixture(autouse=True) -def juju_has_secrets(mocker: MockerFixture): - """This fixture will force the usage of secrets whenever run on Juju 3.x. - - NOTE: This is needed, as normally JujuVersion is set to 0.0.0 in tests - (i.e. not the real juju version) - """ - juju_version = os.environ["LIBJUJU_VERSION_SPECIFIER"] - if juju_version.startswith("=="): - juju_version = juju_version[2:] # Removing == symbol - - if juju_version < "3": - mocker.patch.object(JujuVersion, "has_secrets", new_callable=PropertyMock).return_value = ( - False - ) - return False - - mocker.patch.object(JujuVersion, "has_secrets", new_callable=PropertyMock).return_value = True - return True - - -@pytest.fixture -def only_with_juju_secrets(juju_has_secrets): - """Pretty way to skip Juju 3 tests.""" - if not juju_has_secrets: - pytest.skip("Secrets test only applies on Juju 3.x") - - -@pytest.fixture -def only_without_juju_secrets(juju_has_secrets): - """Pretty way to skip Juju 2-specific tests. - - Typically: to save CI time, when the same check were executed in a Juju 3-specific way already - """ - if juju_has_secrets: - pytest.skip("Skipping legacy secrets tests") diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000000..db3bfe1a65 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py deleted file mode 100644 index da132958d9..0000000000 --- a/tests/integration/conftest.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -import json -import os -from pathlib import Path - -import pytest -from pytest_operator.plugin import OpsTest - - -@pytest.fixture(scope="module") -def ops_test(ops_test: OpsTest) -> OpsTest: - if os.environ.get("CI") == "true": - # Running in GitHub Actions; skip build step - # (GitHub Actions uses a separate, cached build step. See .github/workflows/ci.yaml) - packed_charms = json.loads(os.environ["CI_PACKED_CHARMS"]) - - async def build_charm(charm_path, bases_index: int = None) -> Path: - for charm in packed_charms: - if Path(charm_path) == Path(charm["directory_path"]): - if bases_index is None or bases_index == charm["bases_index"]: - return charm["file_path"] - raise ValueError(f"Unable to find .charm file for {bases_index=} at {charm_path=}") - - ops_test.build_charm = build_charm - return ops_test diff --git a/tests/integration/ha/__init__.py b/tests/integration/ha/__init__.py new file mode 100644 index 0000000000..db3bfe1a65 --- /dev/null +++ b/tests/integration/ha/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/tests/integration/ha/conftest.py b/tests/integration/ha/conftest.py new file mode 100644 index 0000000000..6448f997d4 --- /dev/null +++ b/tests/integration/ha/conftest.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +import logging + +import pytest +from pytest_operator.plugin import OpsTest + +from ..helpers import APP_NAME, get_application_unit_ids +from .continuous_writes import ContinuousWrites +from .helpers import ORIGINAL_RESTART_DELAY, app_name, update_restart_delay + +logger = logging.getLogger(__name__) + + +SECOND_APP_NAME = "second-opensearch" +RESTART_DELAY = 360 + + +@pytest.fixture(scope="function") +async def reset_restart_delay(ops_test: OpsTest): + """Resets service file delay on all units.""" + yield + app = (await app_name(ops_test)) or APP_NAME + for unit_id in get_application_unit_ids(ops_test, app): + await update_restart_delay(ops_test, app, unit_id, ORIGINAL_RESTART_DELAY) + + +@pytest.fixture(scope="function") +async def c_writes(ops_test: OpsTest): + """Creates instance of the ContinuousWrites.""" + app = (await app_name(ops_test)) or APP_NAME + return ContinuousWrites(ops_test, app) + + +@pytest.fixture(scope="function") +async def c_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): + """Starts continuous write operations and clears writes at the end of the test.""" + await c_writes.start() + yield + await c_writes.clear() + logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") + + +@pytest.fixture(scope="function") +async def c_balanced_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): + """Same as previous runner, but starts continuous writes on cluster wide replicated index.""" + await c_writes.start(repl_on_all_nodes=True) + yield + await c_writes.clear() + logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") diff --git a/tests/integration/ha/continuous_writes.py b/tests/integration/ha/continuous_writes.py index 7481a2135d..610e300ebb 100644 --- a/tests/integration/ha/continuous_writes.py +++ b/tests/integration/ha/continuous_writes.py @@ -22,11 +22,7 @@ wait_random, ) -from tests.integration.helpers import ( - get_admin_secrets, - get_application_unit_ips, - opensearch_client, -) +from ..helpers import get_admin_secrets, get_application_unit_ips, opensearch_client logging.getLogger("opensearch").setLevel(logging.ERROR) logging.getLogger("opensearchpy.helpers").setLevel(logging.ERROR) diff --git a/tests/integration/ha/helpers.py b/tests/integration/ha/helpers.py index 4c73a8ea75..1e528d2aaa 100644 --- a/tests/integration/ha/helpers.py +++ b/tests/integration/ha/helpers.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. + import json import logging import subprocess @@ -18,9 +19,7 @@ wait_random, ) -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers_data import index_docs_count -from tests.integration.helpers import ( +from ..helpers import ( get_application_unit_ids, get_application_unit_ids_hostnames, get_application_unit_ids_ips, @@ -28,10 +27,17 @@ juju_version_major, run_action, ) +from .continuous_writes import ContinuousWrites +from .helpers_data import index_docs_count OPENSEARCH_SERVICE_PATH = "/etc/systemd/system/snap.opensearch.daemon.service" +SECOND_APP_NAME = "second-opensearch" +ORIGINAL_RESTART_DELAY = 20 +RESTART_DELAY = 360 + + logger = logging.getLogger(__name__) @@ -207,6 +213,16 @@ async def all_nodes(ops_test: OpsTest, unit_ip: str) -> List[Node]: return result +async def assert_continuous_writes_increasing( + c_writes: ContinuousWrites, +) -> None: + """Asserts that the continuous writes are increasing.""" + writes_count = await c_writes.count() + time.sleep(5) + more_writes = await c_writes.count() + assert more_writes > writes_count, "Writes not continuing to DB" + + async def assert_continuous_writes_consistency( ops_test: OpsTest, c_writes: ContinuousWrites, app: str ) -> None: @@ -476,8 +492,8 @@ async def wait_restore_finish(ops_test, unit_ip): raise Exception() -async def continuous_writes_increases(ops_test: OpsTest, unit_ip: str, app: str) -> bool: - """Asserts that TEST_BACKUP_INDEX is writable while under continuous writes. +async def start_and_check_continuous_writes(ops_test: OpsTest, unit_ip: str, app: str) -> bool: + """Asserts that ContinuousWrites.INDEX_NAME is writable. Given we are restoring an index, we need to make sure ContinuousWrites restart at the tip of that index instead of doc_id = 0. @@ -490,18 +506,19 @@ async def continuous_writes_increases(ops_test: OpsTest, unit_ip: str, app: str) ) writer = ContinuousWrites(ops_test, app, initial_count=initial_count) await writer.start() - time.sleep(5) + time.sleep(10) result = await writer.stop() return result.count > initial_count -async def backup_cluster(ops_test: OpsTest, leader_id: int) -> bool: +async def backup_cluster(ops_test: OpsTest, leader_id: int) -> int: """Runs the backup of the cluster.""" action = await run_action(ops_test, leader_id, "create-backup") logger.debug(f"create-backup output: {action}") await wait_backup_finish(ops_test, leader_id) - return action.status == "completed" + assert action.status == "completed" + return int(action.response["backup-id"]) async def restore_cluster(ops_test: OpsTest, backup_id: int, unit_ip: str, leader_id: int) -> bool: diff --git a/tests/integration/ha/helpers_data.py b/tests/integration/ha/helpers_data.py index 5b194e4a9f..3e98fcc170 100644 --- a/tests/integration/ha/helpers_data.py +++ b/tests/integration/ha/helpers_data.py @@ -10,7 +10,7 @@ from pytest_operator.plugin import OpsTest from tenacity import Retrying, retry, stop_after_attempt, wait_fixed, wait_random -from tests.integration.helpers import http_request +from ..helpers import http_request logger = logging.getLogger(__name__) diff --git a/tests/integration/ha/test_backups.py b/tests/integration/ha/test_backups.py deleted file mode 100644 index e9bd9ec830..0000000000 --- a/tests/integration/ha/test_backups.py +++ /dev/null @@ -1,338 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -import asyncio -import logging -import os -import random -import subprocess - -# from pathlib import Path -# -# import boto3 -import pytest -import requests -from pytest_operator.plugin import OpsTest - -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers import ( - app_name, - assert_continuous_writes_consistency, - backup_cluster, - continuous_writes_increases, - restore_cluster, -) -from tests.integration.ha.helpers_data import index_docs_count -from tests.integration.ha.test_horizontal_scaling import IDLE_PERIOD -from tests.integration.helpers import ( - APP_NAME, - MODEL_CONFIG, - SERIES, - get_leader_unit_id, - get_leader_unit_ip, - get_reachable_unit_ips, - http_request, - run_action, -) -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME - -logger = logging.getLogger(__name__) - - -S3_INTEGRATOR_NAME = "s3-integrator" -TEST_BACKUP_DOC_ID = 10 -CLOUD_CONFIGS = { - "aws": { - "endpoint": "https://s3.amazonaws.com", - "bucket": "data-charms-testing", - "path": "opensearch", - "region": "us-east-1", - }, - "gcp": { - "endpoint": "https://storage.googleapis.com", - "bucket": "data-charms-testing", - "path": "opensearch", - "region": "", - }, -} - -backups_by_cloud = {} -value_before_backup, value_after_backup = None, None - - -# @pytest.fixture(scope="session") -# def cloud_credentials(github_secrets) -> dict[str, dict[str, str]]: -# """Read cloud credentials.""" -# return { -# "aws": { -# "access-key": github_secrets["AWS_ACCESS_KEY"], -# "secret-key": github_secrets["AWS_SECRET_KEY"], -# }, -# "gcp": { -# "access-key": github_secrets["GCP_ACCESS_KEY"], -# "secret-key": github_secrets["GCP_SECRET_KEY"], -# }, -# } - - -# @pytest.fixture(scope="session", autouse=True) -# def clean_backups_from_buckets(cloud_credentials) -> None: -# """Teardown to clean up created backups from clouds.""" -# yield -# -# logger.info("Cleaning backups from cloud buckets") -# for cloud_name, config in CLOUD_CONFIGS.items(): -# backup = backups_by_cloud.get(cloud_name) -# -# if not backup: -# continue -# -# session = boto3.session.Session( -# aws_access_key_id=cloud_credentials[cloud_name]["access-key"], -# aws_secret_access_key=cloud_credentials[cloud_name]["secret-key"], -# region_name=config["region"], -# ) -# s3 = session.resource("s3", endpoint_url=config["endpoint"]) -# bucket = s3.Bucket(config["bucket"]) -# -# # GCS doesn't support batch delete operation, so delete the objects one by one -# backup_path = str(Path(config["path"]) / backups_by_cloud[cloud_name]) -# for bucket_object in bucket.objects.filter(Prefix=backup_path): -# bucket_object.delete() - - -@pytest.fixture() -async def c_writes(ops_test: OpsTest): - """Creates instance of the ContinuousWrites.""" - app = (await app_name(ops_test)) or APP_NAME - return ContinuousWrites(ops_test, app) - - -@pytest.fixture() -async def c_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Starts continuous write operations and clears writes at the end of the test.""" - await c_writes.start() - yield - - reachable_ip = random.choice(await get_reachable_unit_ips(ops_test)) - await http_request(ops_test, "GET", f"https://{reachable_ip}:9200/_cat/nodes", json_resp=False) - await http_request( - ops_test, "GET", f"https://{reachable_ip}:9200/_cat/shards", json_resp=False - ) - - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - -# TODO: Remove this method as soon as poetry gets merged. -@pytest.fixture(scope="session") -def microceph(): - """Starts microceph radosgw.""" - if "microceph" not in subprocess.check_output(["sudo", "snap", "list"]).decode(): - uceph = "/tmp/microceph.sh" - - with open(uceph, "w") as f: - # TODO: if this code stays, then the script below should be added as a file - # in the charm. - resp = requests.get( - "https://raw.githubusercontent.com/canonical/microceph-action/main/microceph.sh" - ) - f.write(resp.content.decode()) - - os.chmod(uceph, 0o755) - subprocess.check_output( - [ - "sudo", - uceph, - "-c", - "latest/edge", - "-d", - "/dev/sdc", - "-a", - "accesskey", - "-s", - "secretkey", - "-b", - "data-charms-testing", - "-z", - "5G", - ] - ) - ip = subprocess.check_output(["hostname", "-I"]).decode().split()[0] - # TODO: if this code stays, then we should generate random keys for the test. - return {"url": f"http://{ip}", "access-key": "accesskey", "secret-key": "secretkey"} - - -@pytest.mark.abort_on_fail -@pytest.mark.skip_if_deployed -async def test_build_and_deploy( - ops_test: OpsTest, microceph -) -> None: # , cloud_credentials) -> None: - """Build and deploy an HA cluster of OpenSearch and corresponding S3 integration.""" - # it is possible for users to provide their own cluster for HA testing. - # Hence, check if there is a pre-existing cluster. - - if await app_name(ops_test): - return - - s3_config = { - "bucket": "data-charms-testing", - "path": "/", - "endpoint": microceph["url"], - "region": "default", - } - - my_charm = await ops_test.build_charm(".") - await ops_test.model.set_config(MODEL_CONFIG) - - # Deploy TLS Certificates operator. - tls_config = {"ca-common-name": "CN_CA"} - - # Convert to integer as environ always returns string - app_num_units = 3 - - await asyncio.gather( - ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=tls_config), - ops_test.model.deploy(S3_INTEGRATOR_NAME, channel="stable", config=s3_config), - ops_test.model.deploy(my_charm, num_units=app_num_units, series=SERIES), - ) - s3_creds = { - "access-key": microceph["access-key"], - "secret-key": microceph["secret-key"], - } - - await run_action( - ops_test, - 0, - "sync-s3-credentials", - params=s3_creds, - app=S3_INTEGRATOR_NAME, - ) - - # Relate it to OpenSearch to set up TLS. - await ops_test.model.integrate(APP_NAME, TLS_CERTIFICATES_APP_NAME) - await ops_test.model.integrate(APP_NAME, S3_INTEGRATOR_NAME) - await ops_test.model.wait_for_idle( - apps=[TLS_CERTIFICATES_APP_NAME, APP_NAME], - status="active", - timeout=1400, - idle_period=IDLE_PERIOD, - ) - - -@pytest.mark.abort_on_fail -async def test_backup_cluster( - ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner -) -> None: - """Runs the backup process whilst writing to the cluster into 'noisy-index'.""" - app = (await app_name(ops_test)) or APP_NAME - leader_id = await get_leader_unit_id(ops_test) - - assert await backup_cluster( - ops_test, - leader_id, - ) - # continuous writes checks - await assert_continuous_writes_consistency(ops_test, c_writes, app) - - -@pytest.mark.abort_on_fail -async def test_restore_cluster(ops_test: OpsTest) -> None: - """Deletes the TEST_BACKUP_INDEX, restores the cluster and tries to search for index.""" - unit_ip = await get_leader_unit_ip(ops_test) - app = (await app_name(ops_test)) or APP_NAME - leader_id = await get_leader_unit_id(ops_test) - - assert await restore_cluster( - ops_test, - 1, # backup_id - unit_ip, - leader_id, - ) - count = await index_docs_count(ops_test, app, unit_ip, ContinuousWrites.INDEX_NAME) - assert count > 0 - await continuous_writes_increases(ops_test, unit_ip, app) - - -@pytest.mark.abort_on_fail -async def test_restore_cluster_after_app_destroyed(ops_test: OpsTest) -> None: - """Deletes the entire OpenSearch cluster and redeploys from scratch. - - Restores the backup and then checks if the same TEST_BACKUP_INDEX is there. - """ - app = (await app_name(ops_test)) or APP_NAME - - logging.info("Destroying the application") - await ops_test.model.remove_application(app, block_until_done=True) - app_num_units = 3 - my_charm = await ops_test.build_charm(".") - # Redeploy - await asyncio.gather( - ops_test.model.deploy(my_charm, num_units=app_num_units, series=SERIES), - ) - # Relate it to OpenSearch to set up TLS. - await ops_test.model.integrate(APP_NAME, TLS_CERTIFICATES_APP_NAME) - await ops_test.model.integrate(APP_NAME, S3_INTEGRATOR_NAME) - await ops_test.model.wait_for_idle( - apps=[APP_NAME], - status="active", - timeout=1400, - idle_period=IDLE_PERIOD, - ) - - leader_id = await get_leader_unit_id(ops_test) - leader_unit_ip = await get_leader_unit_ip(ops_test, app=app) - assert await restore_cluster( - ops_test, - 1, # backup_id - leader_unit_ip, - leader_id, - ) - # Count the number of docs in the index - count = await index_docs_count(ops_test, app, leader_unit_ip, ContinuousWrites.INDEX_NAME) - assert count > 0 - await continuous_writes_increases(ops_test, leader_unit_ip, app) - - -@pytest.mark.abort_on_fail -async def test_remove_and_readd_s3_relation(ops_test: OpsTest) -> None: - """Removes and re-adds the s3-credentials relation to test backup and restore.""" - app = (await app_name(ops_test)) or APP_NAME - leader_id = await get_leader_unit_id(ops_test) - unit_ip = await get_leader_unit_ip(ops_test) - - logger.info("Remove s3-credentials relation") - # Remove relation - await ops_test.model.applications[app].destroy_relation( - "s3-credentials", f"{S3_INTEGRATOR_NAME}:s3-credentials" - ) - await ops_test.model.wait_for_idle( - apps=[app], - status="active", - timeout=1400, - idle_period=IDLE_PERIOD, - ) - - logger.info("Re-add s3-credentials relation") - await ops_test.model.integrate(APP_NAME, S3_INTEGRATOR_NAME) - await ops_test.model.wait_for_idle( - apps=[app], - status="active", - timeout=1400, - idle_period=IDLE_PERIOD, - ) - - assert await backup_cluster( - ops_test, - leader_id, - ) - assert await restore_cluster( - ops_test, - 1, # backup_id - unit_ip, - leader_id, - ) - count = await index_docs_count(ops_test, app, unit_ip, ContinuousWrites.INDEX_NAME) - assert count > 0 - await continuous_writes_increases(ops_test, unit_ip, app) diff --git a/tests/integration/ha/test_ha.py b/tests/integration/ha/test_ha.py index ab5beb1b18..2fcb9277e5 100644 --- a/tests/integration/ha/test_ha.py +++ b/tests/integration/ha/test_ha.py @@ -9,25 +9,7 @@ import pytest from pytest_operator.plugin import OpsTest -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers import ( - all_processes_down, - app_name, - assert_continuous_writes_consistency, - get_elected_cm_unit_id, - get_shards_by_index, - send_kill_signal_to_process, - update_restart_delay, -) -from tests.integration.ha.helpers_data import ( - create_index, - default_doc, - delete_index, - index_doc, - search, -) -from tests.integration.ha.test_horizontal_scaling import IDLE_PERIOD -from tests.integration.helpers import ( +from ..helpers import ( APP_NAME, MODEL_CONFIG, SERIES, @@ -40,51 +22,28 @@ get_reachable_unit_ips, is_up, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from ..helpers_deployments import wait_until +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .continuous_writes import ContinuousWrites +from .helpers import ( + ORIGINAL_RESTART_DELAY, + RESTART_DELAY, + all_processes_down, + app_name, + assert_continuous_writes_consistency, + assert_continuous_writes_increasing, + get_elected_cm_unit_id, + get_shards_by_index, + send_kill_signal_to_process, + update_restart_delay, +) +from .helpers_data import create_index, default_doc, delete_index, index_doc, search +from .test_horizontal_scaling import IDLE_PERIOD logger = logging.getLogger(__name__) -SECOND_APP_NAME = "second-opensearch" -ORIGINAL_RESTART_DELAY = 20 -RESTART_DELAY = 360 - - -@pytest.fixture() -async def reset_restart_delay(ops_test: OpsTest): - """Resets service file delay on all units.""" - yield - app = (await app_name(ops_test)) or APP_NAME - for unit_id in get_application_unit_ids(ops_test, app): - await update_restart_delay(ops_test, app, unit_id, ORIGINAL_RESTART_DELAY) - - -@pytest.fixture() -async def c_writes(ops_test: OpsTest): - """Creates instance of the ContinuousWrites.""" - app = (await app_name(ops_test)) or APP_NAME - return ContinuousWrites(ops_test, app) - - -@pytest.fixture() -async def c_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Starts continuous write operations and clears writes at the end of the test.""" - await c_writes.start() - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - -@pytest.fixture() -async def c_balanced_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Same as previous runner, but starts continuous writes on cluster wide replicated index.""" - await c_writes.start(repl_on_all_nodes=True) - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: @@ -96,7 +55,6 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: my_charm = await ops_test.build_charm(".") await ops_test.model.set_config(MODEL_CONFIG) - # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await asyncio.gather( @@ -115,6 +73,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == 3 +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_replication_across_members( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner @@ -139,7 +98,7 @@ async def test_replication_across_members( await index_doc(ops_test, app, leader_unit_ip, index_name, doc_id) # check that the doc can be retrieved from any node - for u_id, u_ip in units.items(): + for u_ip in units.values(): docs = await search( ops_test, app, @@ -157,6 +116,7 @@ async def test_replication_across_members( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_kill_db_process_node_with_primary_shard( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -189,12 +149,7 @@ async def test_kill_db_process_node_with_primary_shard( ops_test, app, first_unit_with_primary_shard, signal="SIGKILL" ) - # verify new writes are continuing by counting the number of writes before and after 5 seconds - # should also be plenty for the shard primary reelection to happen - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # verify that the opensearch service is back running on the old primary unit assert await is_up( @@ -223,6 +178,7 @@ async def test_kill_db_process_node_with_primary_shard( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_kill_db_process_node_with_elected_cm( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -252,12 +208,7 @@ async def test_kill_db_process_node_with_elected_cm( # Kill the opensearch process await send_kill_signal_to_process(ops_test, app, first_elected_cm_unit_id, signal="SIGKILL") - # verify new writes are continuing by counting the number of writes before and after 5 seconds - # should also be plenty for the cluster manager reelection to happen - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # verify that the opensearch service is back running on the old elected cm unit assert await is_up( @@ -279,6 +230,7 @@ async def test_kill_db_process_node_with_elected_cm( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_freeze_db_process_node_with_primary_shard( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -318,12 +270,7 @@ async def test_freeze_db_process_node_with_primary_shard( is_node_up = await is_up(ops_test, units_ips[first_unit_with_primary_shard], retries=3) assert not is_node_up - # verify new writes are continuing by counting the number of writes before and after 5 seconds - # should also be plenty for the shard primary reelection to happen - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # get reachable unit to perform requests against, in case the previously stopped unit # is leader unit, so its address is not reachable @@ -368,6 +315,7 @@ async def test_freeze_db_process_node_with_primary_shard( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_freeze_db_process_node_with_elected_cm( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -406,12 +354,7 @@ async def test_freeze_db_process_node_with_elected_cm( is_node_up = await is_up(ops_test, units_ips[first_elected_cm_unit_id], retries=3) assert not is_node_up - # verify new writes are continuing by counting the number of writes before and after 5 seconds - # should also be plenty for the cluster manager reelection to happen - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # get reachable unit to perform requests against, in case the previously stopped unit # is leader unit, so its address is not reachable @@ -446,6 +389,7 @@ async def test_freeze_db_process_node_with_elected_cm( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_restart_db_process_node_with_elected_cm( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -475,12 +419,7 @@ async def test_restart_db_process_node_with_elected_cm( # restart the opensearch process await send_kill_signal_to_process(ops_test, app, first_elected_cm_unit_id, signal="SIGTERM") - # verify new writes are continuing by counting the number of writes before and after 5 seconds - # should also be plenty for the cluster manager reelection to happen - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # verify that the opensearch service is back running on the unit previously elected CM unit assert await is_up( @@ -501,6 +440,7 @@ async def test_restart_db_process_node_with_elected_cm( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_restart_db_process_node_with_primary_shard( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -533,12 +473,7 @@ async def test_restart_db_process_node_with_primary_shard( ops_test, app, first_unit_with_primary_shard, signal="SIGTERM" ) - # verify new writes are continuing by counting the number of writes before and after 5 seconds - # should also be plenty for the cluster manager reelection to happen - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # verify that the opensearch service is back running on the previous primary shard unit assert await is_up( @@ -566,6 +501,7 @@ async def test_restart_db_process_node_with_primary_shard( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) async def test_full_cluster_crash( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner, reset_restart_delay ) -> None: @@ -607,11 +543,7 @@ async def test_full_cluster_crash( ops_test, leader_ip, get_application_unit_names(ops_test, app=app) ) - # verify new writes are continuing by counting the number of writes before and after 5 seconds - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # check that cluster health is green (all primary and replica shards allocated) health_resp = await cluster_health(ops_test, leader_ip) @@ -621,6 +553,7 @@ async def test_full_cluster_crash( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_full_cluster_restart( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner, reset_restart_delay @@ -663,11 +596,7 @@ async def test_full_cluster_restart( ops_test, leader_ip, get_application_unit_names(ops_test, app=app) ) - # verify new writes are continuing by counting the number of writes before and after 5 seconds - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + await assert_continuous_writes_increasing(c_writes) # check that cluster health is green (all primary and replica shards allocated) health_resp = await cluster_health(ops_test, leader_ip) diff --git a/tests/integration/ha/test_ha_multi_clusters.py b/tests/integration/ha/test_ha_multi_clusters.py index fd1524bd2f..67c3616d7f 100644 --- a/tests/integration/ha/test_ha_multi_clusters.py +++ b/tests/integration/ha/test_ha_multi_clusters.py @@ -8,66 +8,25 @@ import pytest from pytest_operator.plugin import OpsTest -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers import ( - app_name, - assert_continuous_writes_consistency, - update_restart_delay, -) -from tests.integration.ha.helpers_data import delete_index, index_doc, search -from tests.integration.ha.test_horizontal_scaling import IDLE_PERIOD -from tests.integration.helpers import ( +from ..helpers import ( APP_NAME, MODEL_CONFIG, SERIES, + app_name, get_application_unit_ids, get_leader_unit_ip, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from ..helpers_deployments import wait_until +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .continuous_writes import ContinuousWrites +from .helpers import SECOND_APP_NAME, assert_continuous_writes_consistency +from .helpers_data import delete_index, index_doc, search +from .test_horizontal_scaling import IDLE_PERIOD logger = logging.getLogger(__name__) -SECOND_APP_NAME = "second-opensearch" -ORIGINAL_RESTART_DELAY = 20 -RESTART_DELAY = 360 - - -@pytest.fixture() -async def reset_restart_delay(ops_test: OpsTest): - """Resets service file delay on all units.""" - yield - app = (await app_name(ops_test)) or APP_NAME - for unit_id in get_application_unit_ids(ops_test, app): - await update_restart_delay(ops_test, app, unit_id, ORIGINAL_RESTART_DELAY) - - -@pytest.fixture() -async def c_writes(ops_test: OpsTest): - """Creates instance of the ContinuousWrites.""" - app = (await app_name(ops_test)) or APP_NAME - return ContinuousWrites(ops_test, app) - - -@pytest.fixture() -async def c_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Starts continuous write operations and clears writes at the end of the test.""" - await c_writes.start() - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - -@pytest.fixture() -async def c_balanced_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Same as previous runner, but starts continuous writes on cluster wide replicated index.""" - await c_writes.start(repl_on_all_nodes=True) - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: @@ -79,7 +38,6 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: my_charm = await ops_test.build_charm(".") await ops_test.model.set_config(MODEL_CONFIG) - # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await asyncio.gather( @@ -101,6 +59,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: # put this test at the end of the list of tests, as we delete an app during cleanup # and the safeguards we have on the charm prevent us from doing so, so we'll keep # using a unit without need - when other tests may need the unit on the CI +@pytest.mark.group(1) async def test_multi_clusters_db_isolation( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner ) -> None: diff --git a/tests/integration/ha/test_ha_networking.py b/tests/integration/ha/test_ha_networking.py index eabe086829..d3d431667a 100644 --- a/tests/integration/ha/test_ha_networking.py +++ b/tests/integration/ha/test_ha_networking.py @@ -4,15 +4,13 @@ import asyncio import logging -import time import pytest from pytest_operator.plugin import OpsTest -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers import ( - app_name, +from ..ha.helpers import ( assert_continuous_writes_consistency, + assert_continuous_writes_increasing, cut_network_from_unit_with_ip_change, cut_network_from_unit_without_ip_change, get_elected_cm_unit_id, @@ -22,11 +20,11 @@ restore_network_for_unit_with_ip_change, restore_network_for_unit_without_ip_change, ) -from tests.integration.ha.test_horizontal_scaling import IDLE_PERIOD -from tests.integration.helpers import ( +from ..helpers import ( APP_NAME, MODEL_CONFIG, SERIES, + app_name, check_cluster_formation_successful, get_application_unit_ids_hostnames, get_application_unit_ids_ips, @@ -35,40 +33,21 @@ get_leader_unit_ip, is_up, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from ..helpers_deployments import wait_until +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .continuous_writes import ContinuousWrites +from .test_horizontal_scaling import IDLE_PERIOD logger = logging.getLogger(__name__) -@pytest.fixture() -async def c_writes(ops_test: OpsTest): - """Creates instance of the ContinuousWrites.""" - app = (await app_name(ops_test)) or APP_NAME - return ContinuousWrites(ops_test, app) - - -@pytest.fixture() -async def c_balanced_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Same as previous runner, but starts continuous writes on cluster wide replicated index.""" - await c_writes.start(repl_on_all_nodes=True) - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: """Build and deploy one unit of OpenSearch.""" - # it is possible for users to provide their own cluster for HA testing. - # Hence, check if there is a pre-existing cluster. - if await app_name(ops_test): - return - my_charm = await ops_test.build_charm(".") await ops_test.model.set_config(MODEL_CONFIG) - # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await asyncio.gather( @@ -87,6 +66,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == 3 +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_full_network_cut_with_ip_change_node_with_elected_cm( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -146,11 +126,7 @@ async def test_full_network_cut_with_ip_change_node_with_elected_cm( ops_test, first_elected_cm_unit_ip, retries=3 ), "Connection still possible to the first CM node where the network was cut." - # verify new writes are continuing by counting the number of writes before and after 5 seconds - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + assert_continuous_writes_increasing(c_writes) # check new CM got elected leader_unit_ip = await get_leader_unit_ip(ops_test, app=app) @@ -192,6 +168,7 @@ async def test_full_network_cut_with_ip_change_node_with_elected_cm( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_full_network_cut_with_ip_change_node_with_primary_shard( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -252,11 +229,7 @@ async def test_full_network_cut_with_ip_change_node_with_primary_shard( ops_test, first_unit_with_primary_shard_ip, retries=3 ), "Connection still possible to the first unit with primary shard where the network was cut." - # verify new writes are continuing by counting the number of writes before and after 5 seconds - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + assert_continuous_writes_increasing(c_writes) # check new primary shard got elected leader_unit_ip = await get_leader_unit_ip(ops_test, app=app) @@ -313,6 +286,7 @@ async def test_full_network_cut_with_ip_change_node_with_primary_shard( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_full_network_cut_without_ip_change_node_with_elected_cm( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -367,11 +341,7 @@ async def test_full_network_cut_without_ip_change_node_with_elected_cm( ops_test, first_elected_cm_unit_ip, retries=3 ), "Connection still possible to the first CM node where the network was cut." - # verify new writes are continuing by counting the number of writes before and after 5 seconds - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + assert_continuous_writes_increasing(c_writes) # check new CM got elected leader_unit_ip = await get_leader_unit_ip(ops_test, app=app) @@ -404,6 +374,7 @@ async def test_full_network_cut_without_ip_change_node_with_elected_cm( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_full_network_cut_without_ip_change_node_with_primary_shard( ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner @@ -461,11 +432,7 @@ async def test_full_network_cut_without_ip_change_node_with_primary_shard( ops_test, first_unit_with_primary_shard_ip, retries=3 ), "Connection still possible to the first unit with primary shard where the network was cut." - # verify new writes are continuing by counting the number of writes before and after 5 seconds - writes = await c_writes.count() - time.sleep(5) - more_writes = await c_writes.count() - assert more_writes > writes, "Writes not continuing to DB" + assert_continuous_writes_increasing(c_writes) # check new primary shard got elected leader_unit_ip = await get_leader_unit_ip(ops_test, app=app) diff --git a/tests/integration/ha/test_horizontal_scaling.py b/tests/integration/ha/test_horizontal_scaling.py index 7df21c784a..2f4897643b 100644 --- a/tests/integration/ha/test_horizontal_scaling.py +++ b/tests/integration/ha/test_horizontal_scaling.py @@ -11,8 +11,7 @@ from charms.opensearch.v0.helper_cluster import ClusterTopology from pytest_operator.plugin import OpsTest -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers import ( +from ..ha.helpers import ( all_nodes, assert_continuous_writes_consistency, get_elected_cm_unit_id, @@ -20,12 +19,7 @@ get_shards_by_index, get_shards_by_state, ) -from tests.integration.ha.helpers_data import ( - create_dummy_docs, - create_dummy_indexes, - delete_dummy_indexes, -) -from tests.integration.helpers import ( +from ..helpers import ( APP_NAME, IDLE_PERIOD, MODEL_CONFIG, @@ -38,28 +32,15 @@ get_leader_unit_id, get_leader_unit_ip, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from ..helpers_deployments import wait_until +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .continuous_writes import ContinuousWrites +from .helpers_data import create_dummy_docs, create_dummy_indexes, delete_dummy_indexes logger = logging.getLogger(__name__) -@pytest.fixture() -async def c_writes(ops_test: OpsTest): - """Creates instance of the ContinuousWrites.""" - app = (await app_name(ops_test)) or APP_NAME - return ContinuousWrites(ops_test, app) - - -@pytest.fixture() -async def c_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Starts continuous write operations and clears writes at the end of the test.""" - await c_writes.start() - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: @@ -71,7 +52,6 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: my_charm = await ops_test.build_charm(".") await ops_test.model.set_config(MODEL_CONFIG) - # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await asyncio.gather( @@ -87,6 +67,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == 1 +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_horizontal_scale_up( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner @@ -134,6 +115,7 @@ async def test_horizontal_scale_up( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_safe_scale_down_shards_realloc( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner @@ -243,6 +225,7 @@ async def test_safe_scale_down_shards_realloc( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_safe_scale_down_roles_reassigning( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner @@ -337,6 +320,7 @@ async def test_safe_scale_down_roles_reassigning( await assert_continuous_writes_consistency(ops_test, c_writes, app) +@pytest.mark.group(1) async def test_safe_scale_down_remove_leaders( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner ) -> None: @@ -352,6 +336,8 @@ async def test_safe_scale_down_remove_leaders( app = (await app_name(ops_test)) or APP_NAME init_units_count = len(ops_test.model.applications[app].units) + c_writes_obj = c_writes + # scale up by 2 units await ops_test.model.applications[app].add_unit(count=3) await wait_until( @@ -422,7 +408,7 @@ async def test_safe_scale_down_remove_leaders( # sleep for a couple of minutes for the model to stabilise time.sleep(IDLE_PERIOD + 60) - writes = await c_writes.count() + writes = await c_writes_obj.count() # check that the primary shard reelection happened leader_unit_ip = await get_leader_unit_ip(ops_test, app=app) @@ -437,8 +423,8 @@ async def test_safe_scale_down_remove_leaders( # check that writes are still going after the removal / p_shard reelection time.sleep(3) - new_writes = await c_writes.count() + new_writes = await c_writes_obj.count() assert new_writes > writes # continuous writes checks - await assert_continuous_writes_consistency(ops_test, c_writes, app) + await assert_continuous_writes_consistency(ops_test, c_writes_obj, app) diff --git a/tests/integration/ha/test_large_deployments.py b/tests/integration/ha/test_large_deployments.py index 0eec7a8dcd..8e5a72fa1c 100644 --- a/tests/integration/ha/test_large_deployments.py +++ b/tests/integration/ha/test_large_deployments.py @@ -9,63 +9,26 @@ from charms.opensearch.v0.constants_charm import PClusterWrongNodesCountForQuorum from pytest_operator.plugin import OpsTest -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers import all_nodes, app_name, update_restart_delay -from tests.integration.ha.test_horizontal_scaling import IDLE_PERIOD -from tests.integration.helpers import ( +from ..helpers import ( APP_NAME, MODEL_CONFIG, SERIES, check_cluster_formation_successful, cluster_health, - get_application_unit_ids, get_application_unit_names, get_application_unit_status, get_leader_unit_ip, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from ..helpers_deployments import wait_until +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .continuous_writes import ContinuousWrites +from .helpers import all_nodes, app_name +from .test_horizontal_scaling import IDLE_PERIOD logger = logging.getLogger(__name__) -ORIGINAL_RESTART_DELAY = 20 - - -@pytest.fixture() -async def reset_restart_delay(ops_test: OpsTest): - """Resets service file delay on all units.""" - yield - app = (await app_name(ops_test)) or APP_NAME - for unit_id in get_application_unit_ids(ops_test, app): - await update_restart_delay(ops_test, app, unit_id, ORIGINAL_RESTART_DELAY) - - -@pytest.fixture() -async def c_writes(ops_test: OpsTest): - """Creates instance of the ContinuousWrites.""" - app = (await app_name(ops_test)) or APP_NAME - return ContinuousWrites(ops_test, app) - - -@pytest.fixture() -async def c_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Starts continuous write operations and clears writes at the end of the test.""" - await c_writes.start() - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - -@pytest.fixture() -async def c_balanced_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Same as previous runner, but starts continuous writes on cluster wide replicated index.""" - await c_writes.start(repl_on_all_nodes=True) - yield - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: @@ -77,7 +40,6 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: my_charm = await ops_test.build_charm(".") await ops_test.model.set_config(MODEL_CONFIG) - # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await asyncio.gather( @@ -98,6 +60,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == 3 +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_set_roles_manually( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner diff --git a/tests/integration/ha/test_storage.py b/tests/integration/ha/test_storage.py index 32f7e51c5d..e36f96388a 100644 --- a/tests/integration/ha/test_storage.py +++ b/tests/integration/ha/test_storage.py @@ -4,98 +4,27 @@ import asyncio import logging -import random import time import pytest from pytest_operator.plugin import OpsTest -from tests.integration.ha.continuous_writes import ContinuousWrites -from tests.integration.ha.helpers import ( - app_name, - storage_id, - storage_type, - update_restart_delay, -) -from tests.integration.ha.test_horizontal_scaling import IDLE_PERIOD -from tests.integration.helpers import ( - APP_NAME, - MODEL_CONFIG, - SERIES, - get_application_unit_ids, - get_reachable_unit_ips, - http_request, -) -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from ..ha.helpers import app_name, storage_id, storage_type +from ..ha.test_horizontal_scaling import IDLE_PERIOD +from ..helpers import APP_NAME, MODEL_CONFIG, SERIES, get_application_unit_ids +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .continuous_writes import ContinuousWrites logger = logging.getLogger(__name__) -SECOND_APP_NAME = "second-opensearch" -ORIGINAL_RESTART_DELAY = 20 -RESTART_DELAY = 360 - - -@pytest.fixture() -async def reset_restart_delay(ops_test: OpsTest): - """Resets service file delay on all units.""" - yield - app = (await app_name(ops_test)) or APP_NAME - for unit_id in get_application_unit_ids(ops_test, app): - await update_restart_delay(ops_test, app, unit_id, ORIGINAL_RESTART_DELAY) - - -@pytest.fixture() -async def c_writes(ops_test: OpsTest): - """Creates instance of the ContinuousWrites.""" - app = (await app_name(ops_test)) or APP_NAME - return ContinuousWrites(ops_test, app) - - -@pytest.fixture() -async def c_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Starts continuous write operations and clears writes at the end of the test.""" - await c_writes.start() - yield - - reachable_ip = random.choice(await get_reachable_unit_ips(ops_test)) - await http_request(ops_test, "GET", f"https://{reachable_ip}:9200/_cat/nodes", json_resp=False) - await http_request( - ops_test, "GET", f"https://{reachable_ip}:9200/_cat/shards", json_resp=False - ) - - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - -@pytest.fixture() -async def c_balanced_writes_runner(ops_test: OpsTest, c_writes: ContinuousWrites): - """Same as previous runner, but starts continuous writes on cluster wide replicated index.""" - await c_writes.start(repl_on_all_nodes=True) - yield - - reachable_ip = random.choice(await get_reachable_unit_ips(ops_test)) - await http_request(ops_test, "GET", f"https://{reachable_ip}:9200/_cat/nodes", json_resp=False) - await http_request( - ops_test, "GET", f"https://{reachable_ip}:9200/_cat/shards", json_resp=False - ) - - await c_writes.clear() - logger.info("\n\n\n\nThe writes have been cleared.\n\n\n\n") - - +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: """Build and deploy one unit of OpenSearch.""" - # it is possible for users to provide their own cluster for HA testing. - # Hence, check if there is a pre-existing cluster. - if await app_name(ops_test): - return - my_charm = await ops_test.build_charm(".") await ops_test.model.set_config(MODEL_CONFIG) - # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await asyncio.gather( @@ -114,6 +43,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == 1 +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_storage_reuse_after_scale_down( ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner @@ -176,9 +106,10 @@ async def test_storage_reuse_after_scale_down( assert writes_result.max_stored_id == (await c_writes.max_stored_id()) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_storage_reuse_in_new_cluster_after_app_removal( - ops_test: OpsTest, c_writes: ContinuousWrites, c_balanced_writes_runner + ops_test: OpsTest, c_writes: ContinuousWrites, c_writes_runner ): """Check storage is reused and data accessible after removing app and deploying new cluster.""" app = (await app_name(ops_test)) or APP_NAME diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 339647f00f..aa6aeee725 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -13,11 +13,6 @@ import requests import yaml from charms.opensearch.v0.helper_networking import is_reachable -from integration.helpers_deployments import ( - Status, - get_application_units, - get_unit_hostname, -) from opensearchpy import OpenSearch from pytest_operator.plugin import OpsTest from tenacity import ( @@ -29,6 +24,8 @@ wait_random, ) +from .helpers_deployments import Status, get_application_units, get_unit_hostname + METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) APP_NAME = METADATA["name"] diff --git a/tests/integration/plugins/__init__.py b/tests/integration/plugins/__init__.py new file mode 100644 index 0000000000..db3bfe1a65 --- /dev/null +++ b/tests/integration/plugins/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/tests/integration/plugins/helpers.py b/tests/integration/plugins/helpers.py index fc033079ff..d6ab3bb155 100644 --- a/tests/integration/plugins/helpers.py +++ b/tests/integration/plugins/helpers.py @@ -18,8 +18,8 @@ wait_random, ) -from tests.integration.ha.helpers_data import bulk_insert, create_index -from tests.integration.helpers import get_application_unit_ids, http_request +from ..ha.helpers_data import bulk_insert, create_index +from ..helpers import get_application_unit_ids, http_request logger = logging.getLogger(__name__) diff --git a/tests/integration/plugins/test_plugins.py b/tests/integration/plugins/test_plugins.py index 1c9ec5aa23..45827271a9 100644 --- a/tests/integration/plugins/test_plugins.py +++ b/tests/integration/plugins/test_plugins.py @@ -9,10 +9,10 @@ from pytest_operator.plugin import OpsTest from tenacity import RetryError -from tests.integration.ha.helpers import app_name -from tests.integration.ha.helpers_data import bulk_insert, create_index, search -from tests.integration.ha.test_horizontal_scaling import IDLE_PERIOD -from tests.integration.helpers import ( +from ..ha.helpers import app_name +from ..ha.helpers_data import bulk_insert, create_index, search +from ..ha.test_horizontal_scaling import IDLE_PERIOD +from ..helpers import ( APP_NAME, MODEL_CONFIG, SERIES, @@ -25,7 +25,7 @@ http_request, run_action, ) -from tests.integration.plugins.helpers import ( +from ..plugins.helpers import ( create_index_and_bulk_insert, generate_bulk_training_data, get_application_unit_ids_start_time, @@ -33,15 +33,14 @@ is_knn_training_complete, run_knn_training, ) -from tests.integration.relations.opensearch_provider.helpers import ( - get_unit_relation_data, -) -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from ..relations.helpers import get_unit_relation_data +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME COS_APP_NAME = "grafana-agent" COS_RELATION_NAME = "cos-agent" +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: @@ -62,10 +61,10 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await asyncio.gather( - ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), ops_test.model.deploy( my_charm, num_units=3, series=SERIES, config={"plugin_opensearch_knn": True} ), + ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), ) # Relate it to OpenSearch to set up TLS. @@ -79,6 +78,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == 3 +@pytest.mark.group(1) async def test_prometheus_exporter_enabled_by_default(ops_test): """Test that Prometheus Exporter is running before the relation is there.""" leader_unit_ip = await get_leader_unit_ip(ops_test, app=APP_NAME) @@ -90,6 +90,7 @@ async def test_prometheus_exporter_enabled_by_default(ops_test): assert len(response_str.split("\n")) > 500 +@pytest.mark.group(1) async def test_prometheus_exporter_cos_relation(ops_test): await ops_test.model.deploy(COS_APP_NAME, channel="edge"), await ops_test.model.integrate(APP_NAME, COS_APP_NAME) @@ -119,6 +120,7 @@ async def test_prometheus_exporter_cos_relation(ops_test): assert relation_data["scheme"] == "https" +@pytest.mark.group(1) async def test_monitoring_user_fetch_prometheus_data(ops_test): leader_unit_ip = await get_leader_unit_ip(ops_test, app=APP_NAME) endpoint = f"https://{leader_unit_ip}:9200/_prometheus/metrics" @@ -139,6 +141,7 @@ async def test_monitoring_user_fetch_prometheus_data(ops_test): assert len(response_str.split("\n")) > 500 +@pytest.mark.group(1) async def test_prometheus_monitor_user_password_change(ops_test): # Password change applied as expected leader_id = await get_leader_unit_id(ops_test, APP_NAME) @@ -161,6 +164,7 @@ async def test_prometheus_monitor_user_password_change(ops_test): assert relation_data["password"] == new_password +@pytest.mark.group(1) async def test_knn_enabled_disabled(ops_test): config = await ops_test.model.applications[APP_NAME].get_config() assert config["plugin_opensearch_knn"]["default"] is True @@ -179,6 +183,7 @@ async def test_knn_enabled_disabled(ops_test): assert config["plugin_opensearch_knn"]["value"] is True +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_knn_search_with_hnsw_faiss(ops_test: OpsTest) -> None: """Uploads data and runs a query search against the FAISS KNNEngine.""" @@ -222,6 +227,7 @@ async def test_knn_search_with_hnsw_faiss(ops_test: OpsTest) -> None: assert len(docs) == 2 +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_knn_search_with_hnsw_nmslib(ops_test: OpsTest) -> None: """Uploads data and runs a query search against the NMSLIB KNNEngine.""" @@ -265,6 +271,7 @@ async def test_knn_search_with_hnsw_nmslib(ops_test: OpsTest) -> None: assert len(docs) == 2 +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_knn_training_search(ops_test: OpsTest) -> None: """Tests the entire cycle of KNN plugin. diff --git a/tests/integration/relations/__init__.py b/tests/integration/relations/__init__.py new file mode 100644 index 0000000000..db3bfe1a65 --- /dev/null +++ b/tests/integration/relations/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/tests/integration/relations/conftest.py b/tests/integration/relations/conftest.py new file mode 100644 index 0000000000..aa54a92e63 --- /dev/null +++ b/tests/integration/relations/conftest.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +import glob +import os +import pathlib +import shutil +from importlib import reload + +import pytest +import pytest_operator +from pytest_operator.plugin import OpsTest + + +def should_rebuild_charm(target: pathlib.Path) -> bool: + """Returns the latest change in the charm path. + + Compare it with the *.charm file, if source files were changed more recently, + then rebuild the charm. + """ + ignored_prefixes = [".", "_"] + target_path = os.path.dirname(target) + for root, dirs, files in os.walk(target_path): + if any([p for p in ignored_prefixes if root.startswith(p)]): + continue + for f in files: + if f.endswith(".charm") or any([p for p in ignored_prefixes if f.startswith(p)]): + continue + if os.path.getctime(target) < os.path.getctime(f): + return True + return False + + +async def _build(ops_test, charmpath): + """Decides if we should rebuild the charm or not. + + If the charm is not built yet or the source files were changed more recently than .charm, + then rebuild the charm. + + Besides that, the current DP workflow only checks for the actual charm. It disconsiders + any other testing charms existing in the tests/ folder. + """ + if ( + # This is a build process that should be only valid whenever we are not in CI + ("CI" not in os.environ or os.environ["CI"] != "true") + and ( + # Now, check for the .charm file and if we had any recent updates in the sources + not glob.glob(f"{charmpath}/*.charm") + or should_rebuild_charm(glob.glob(f"{charmpath}/*.charm")[0]) + ) + ): + name = await reload(pytest_operator.plugin).OpsTest.build_charm(ops_test, charmpath) + return name + return await ops_test.build_charm(charmpath) + + +@pytest.fixture(scope="module") +async def application_charm(ops_test: OpsTest): + """Build the application charm.""" + shutil.copyfile( + "./lib/charms/data_platform_libs/v0/data_interfaces.py", + "./tests/integration/relations/opensearch_provider/application-charm/lib/charms/data_platform_libs/v0/data_interfaces.py", + ) + test_charm_path = "./tests/integration/relations/opensearch_provider/application-charm" + return await _build(ops_test, test_charm_path) + + +@pytest.fixture(scope="module") +async def opensearch_charm(ops_test: OpsTest): + """Build the opensearch charm as well.""" + return await _build(ops_test, ".") diff --git a/tests/integration/relations/opensearch_provider/helpers.py b/tests/integration/relations/helpers.py similarity index 100% rename from tests/integration/relations/opensearch_provider/helpers.py rename to tests/integration/relations/helpers.py diff --git a/tests/integration/relations/opensearch_provider/conftest.py b/tests/integration/relations/opensearch_provider/conftest.py deleted file mode 100644 index 6d6bbf6bf4..0000000000 --- a/tests/integration/relations/opensearch_provider/conftest.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -import shutil - -import pytest -from pytest_operator.plugin import OpsTest - - -@pytest.fixture(scope="module") -async def application_charm(ops_test: OpsTest): - """Build the application charm.""" - shutil.copyfile( - "./lib/charms/data_platform_libs/v0/data_interfaces.py", - "./tests/integration/relations/opensearch_provider/application-charm/lib/charms/data_platform_libs/v0/data_interfaces.py", - ) - test_charm_path = "./tests/integration/relations/opensearch_provider/application-charm" - return await ops_test.build_charm(test_charm_path) - - -@pytest.fixture(scope="module") -async def opensearch_charm(ops_test: OpsTest): - """Build the opensearch charm.""" - return await ops_test.build_charm(".") diff --git a/tests/integration/relations/opensearch_provider/test_opensearch_provider.py b/tests/integration/relations/test_opensearch_provider.py similarity index 77% rename from tests/integration/relations/opensearch_provider/test_opensearch_provider.py rename to tests/integration/relations/test_opensearch_provider.py index 5f7b63b70d..459b072b9c 100644 --- a/tests/integration/relations/opensearch_provider/test_opensearch_provider.py +++ b/tests/integration/relations/test_opensearch_provider.py @@ -11,8 +11,8 @@ from charms.opensearch.v0.constants_charm import ClientRelationName from pytest_operator.plugin import OpsTest -from tests.integration.helpers import APP_NAME as OPENSEARCH_APP_NAME -from tests.integration.helpers import ( +from ..helpers import APP_NAME as OPENSEARCH_APP_NAME +from ..helpers import ( MODEL_CONFIG, SERIES, get_application_unit_ids, @@ -20,8 +20,9 @@ http_request, scale_application, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.relations.opensearch_provider.helpers import ( +from ..helpers_deployments import wait_until +from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .helpers import ( get_application_relation_data, ip_to_url, run_request, @@ -32,7 +33,6 @@ CLIENT_APP_NAME = "application" SECONDARY_CLIENT_APP_NAME = "secondary-application" -TLS_CERTIFICATES_APP_NAME = "self-signed-certificates" ALL_APPS = [OPENSEARCH_APP_NAME, TLS_CERTIFICATES_APP_NAME, CLIENT_APP_NAME] NUM_UNITS = 3 @@ -51,6 +51,7 @@ ] +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_create_relation(ops_test: OpsTest, application_charm, opensearch_charm): """Test basic functionality of relation interface.""" @@ -59,8 +60,10 @@ async def test_create_relation(ops_test: OpsTest, application_charm, opensearch_ new_model_conf = MODEL_CONFIG.copy() new_model_conf["update-status-hook-interval"] = "1m" + config = {"ca-common-name": "CN_CA"} + await ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config) + await ops_test.model.set_config(new_model_conf) - tls_config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy( application_charm, @@ -72,7 +75,6 @@ async def test_create_relation(ops_test: OpsTest, application_charm, opensearch_ num_units=NUM_UNITS, series=SERIES, ), - ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=tls_config), ) await ops_test.model.integrate(OPENSEARCH_APP_NAME, TLS_CERTIFICATES_APP_NAME) wait_for_relation_joined_between(ops_test, OPENSEARCH_APP_NAME, TLS_CERTIFICATES_APP_NAME) @@ -89,8 +91,11 @@ async def test_create_relation(ops_test: OpsTest, application_charm, opensearch_ timeout=1600, status="active", ) + # Return all update-status-hook-interval to 5m, as all idle_period values are set to 70s + await ops_test.model.set_config({"update-status-hook-interval": "5m"}) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_index_usage(ops_test: OpsTest): """Check we can update and delete things. @@ -128,6 +133,7 @@ async def test_index_usage(ops_test: OpsTest): ) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_bulk_index_usage(ops_test: OpsTest): """Check we can update and delete things using bulk api.""" @@ -168,6 +174,7 @@ async def test_bulk_index_usage(ops_test: OpsTest): assert set(artists) == {"Herbie Hancock", "Lydian Collective", "Vulfpeck"} +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_version(ops_test: OpsTest): """Check version reported in the databag is consistent with the version on the charm.""" @@ -188,8 +195,8 @@ async def test_version(ops_test: OpsTest): assert version == results.get("version", {}).get("number"), results +@pytest.mark.group(1) @pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_without_juju_secrets") async def test_scaling(ops_test: OpsTest): """Test that scaling correctly updates endpoints in databag. @@ -253,8 +260,8 @@ async def get_secret_data(ops_test, secret_uri): return json.loads(stdout)[secret_unique_id]["content"]["Data"] +@pytest.mark.group(1) @pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_with_juju_secrets") async def test_scaling_secrets(ops_test: OpsTest): """Test that scaling correctly updates endpoints in databag. @@ -317,6 +324,7 @@ async def _is_number_of_endpoints_valid(client_app: str, rel: str) -> bool: ), await rel_endpoints(CLIENT_APP_NAME, FIRST_RELATION_NAME) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_multiple_relations(ops_test: OpsTest, application_charm): """Test that two different applications can connect to the database.""" @@ -381,6 +389,7 @@ async def test_multiple_relations(ops_test: OpsTest, application_charm): assert "403 Client Error: Forbidden for url:" in results[0], results +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_multiple_relations_accessing_same_index(ops_test: OpsTest): """Test that two different applications can connect to the database.""" @@ -417,6 +426,7 @@ async def test_multiple_relations_accessing_same_index(ops_test: OpsTest): assert set(artists) == {"Herbie Hancock", "Lydian Collective", "Vulfpeck"} +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_admin_relation(ops_test: OpsTest): """Test we can create relations with admin permissions.""" @@ -452,72 +462,8 @@ async def test_admin_relation(ops_test: OpsTest): assert set(artists) == {"Herbie Hancock", "Lydian Collective", "Vulfpeck"} +@pytest.mark.group(1) @pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_without_juju_secrets") -async def test_admin_permissions(ops_test: OpsTest): - """Test admin permissions behave the way we want. - - admin-only actions include: - - creating multiple indices - - removing indices they've created - - set cluster roles. - - verify that: - - we can't remove .opendistro_security index - - otherwise create client-admin-role - - verify neither admin nor default users can access user api - - otherwise create client-default-role - """ - test_unit = ops_test.model.applications[CLIENT_APP_NAME].units[0] - # Verify admin can't access security API - security_api_endpoint = "/_plugins/_security/api/internalusers" - run_dump_users = await run_request( - ops_test, - unit_name=test_unit.name, - endpoint=security_api_endpoint, - method="GET", - relation_id=admin_relation.id, - relation_name=ADMIN_RELATION_NAME, - ) - results = json.loads(run_dump_users["results"]) - logging.info(results) - assert "403 Client Error: Forbidden for url:" in results[0], results - - # verify admin can't delete users - first_relation_user = await get_application_relation_data( - ops_test, f"{CLIENT_APP_NAME}/0", FIRST_RELATION_NAME, "username" - ) - first_relation_user_endpoint = f"/_plugins/_security/api/internalusers/{first_relation_user}" - run_delete_users = await run_request( - ops_test, - unit_name=test_unit.name, - endpoint=first_relation_user_endpoint, - method="DELETE", - relation_id=admin_relation.id, - relation_name=ADMIN_RELATION_NAME, - ) - results = json.loads(run_delete_users["results"]) - logging.info(results) - assert "403 Client Error: Forbidden for url:" in results[0], results - - # verify admin can't modify protected indices - for protected_index in PROTECTED_INDICES: - protected_index_endpoint = f"/{protected_index}" - run_remove_distro = await run_request( - ops_test, - unit_name=test_unit.name, - endpoint=protected_index_endpoint, - method="DELETE", - relation_id=admin_relation.id, - relation_name=ADMIN_RELATION_NAME, - ) - results = json.loads(run_remove_distro["results"]) - logging.info(results) - assert "Error:" in results[0], results - - -@pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_with_juju_secrets") async def test_admin_permissions_secrets(ops_test: OpsTest): """Test admin permissions behave the way we want. @@ -584,66 +530,8 @@ async def test_admin_permissions_secrets(ops_test: OpsTest): assert "Error:" in results[0], results +@pytest.mark.group(1) @pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_without_juju_secrets") -async def test_normal_user_permissions(ops_test: OpsTest): - """Test normal user permissions behave the way we want. - - verify that: - - we can't remove .opendistro_security index - - verify neither admin nor default users can access user api - """ - test_unit = ops_test.model.applications[CLIENT_APP_NAME].units[0] - - # Verify normal users can't access security API - security_api_endpoint = "/_plugins/_security/api/internalusers" - run_dump_users = await run_request( - ops_test, - unit_name=test_unit.name, - endpoint=security_api_endpoint, - method="GET", - relation_id=client_relation.id, - relation_name=FIRST_RELATION_NAME, - ) - results = json.loads(run_dump_users["results"]) - logging.info(results) - assert "403 Client Error: Forbidden for url:" in results[0], results - - # verify normal users can't delete users - first_relation_user = await get_application_relation_data( - ops_test, f"{CLIENT_APP_NAME}/0", FIRST_RELATION_NAME, "username" - ) - first_relation_user_endpoint = f"/_plugins/_security/api/internalusers/{first_relation_user}" - run_delete_users = await run_request( - ops_test, - unit_name=test_unit.name, - endpoint=first_relation_user_endpoint, - method="DELETE", - relation_id=client_relation.id, - relation_name=FIRST_RELATION_NAME, - ) - results = json.loads(run_delete_users["results"]) - logging.info(results) - assert "403 Client Error: Forbidden for url:" in results[0], results - - # verify user can't modify protected indices - for protected_index in PROTECTED_INDICES: - protected_index_endpoint = f"/{protected_index}" - run_remove_index = await run_request( - ops_test, - unit_name=test_unit.name, - endpoint=protected_index_endpoint, - method="DELETE", - relation_id=client_relation.id, - relation_name=FIRST_RELATION_NAME, - ) - results = json.loads(run_remove_index["results"]) - logging.info(results) - assert "Error:" in results[0], results - - -@pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_with_juju_secrets") async def test_normal_user_permissions_secrets(ops_test: OpsTest): """Test normal user permissions behave the way we want. @@ -703,57 +591,8 @@ async def test_normal_user_permissions_secrets(ops_test: OpsTest): assert "Error:" in results[0], results +@pytest.mark.group(1) @pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_without_juju_secrets") -async def test_relation_broken(ops_test: OpsTest): - """Test that the user is removed when the relation is broken.""" - # Retrieve the relation user. - relation_user = await get_application_relation_data( - ops_test, f"{CLIENT_APP_NAME}/0", FIRST_RELATION_NAME, "username" - ) - await ops_test.model.wait_for_idle( - status="active", - apps=[SECONDARY_CLIENT_APP_NAME] + ALL_APPS, - idle_period=70, - timeout=1600, - ) - - # Break the relation. - await asyncio.gather( - ops_test.model.applications[OPENSEARCH_APP_NAME].remove_relation( - f"{OPENSEARCH_APP_NAME}:{ClientRelationName}", - f"{CLIENT_APP_NAME}:{FIRST_RELATION_NAME}", - ), - ops_test.model.applications[OPENSEARCH_APP_NAME].remove_relation( - f"{OPENSEARCH_APP_NAME}:{ClientRelationName}", - f"{CLIENT_APP_NAME}:{ADMIN_RELATION_NAME}", - ), - ) - - await asyncio.gather( - ops_test.model.wait_for_idle(apps=[CLIENT_APP_NAME], status="blocked", idle_period=70), - ops_test.model.wait_for_idle( - apps=[OPENSEARCH_APP_NAME, TLS_CERTIFICATES_APP_NAME, SECONDARY_CLIENT_APP_NAME], - status="active", - idle_period=70, - timeout=1600, - ), - ) - - leader_ip = await get_leader_unit_ip(ops_test) - users = await http_request( - ops_test, - "GET", - f"https://{ip_to_url(leader_ip)}:9200/_plugins/_security/api/internalusers/", - verify=False, - ) - logger.info(relation_user) - logger.info(users) - assert relation_user not in users.keys() - - -@pytest.mark.abort_on_fail -@pytest.mark.usefixtures("only_with_juju_secrets") async def test_relation_broken_secrets(ops_test: OpsTest): """Test that the user is removed when the relation is broken.""" # Retrieve the relation user. @@ -807,6 +646,7 @@ async def test_relation_broken_secrets(ops_test: OpsTest): assert relation_user not in users.keys() +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_data_persists_on_relation_rejoin(ops_test: OpsTest): """Verify that if we recreate a relation, we can access the same index.""" diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 401eb13d4b..ee28eacc72 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -7,7 +7,7 @@ import pytest from pytest_operator.plugin import OpsTest -from tests.integration.helpers import ( +from .helpers import ( APP_NAME, MODEL_CONFIG, SERIES, @@ -18,8 +18,8 @@ http_request, run_action, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.tls.test_tls import TLS_CERTIFICATES_APP_NAME +from .helpers_deployments import wait_until +from .tls.test_tls import TLS_CERTIFICATES_APP_NAME logger = logging.getLogger(__name__) @@ -27,6 +27,7 @@ DEFAULT_NUM_UNITS = 2 +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy(ops_test: OpsTest) -> None: @@ -42,6 +43,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: await ops_test.model.wait_for_idle(wait_for_exact_units=DEFAULT_NUM_UNITS, timeout=1800) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_status(ops_test: OpsTest) -> None: """Verifies that the application and unit are active.""" @@ -54,6 +56,7 @@ async def test_status(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == DEFAULT_NUM_UNITS +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_actions_get_admin_password(ops_test: OpsTest) -> None: """Test the retrieval of admin secrets.""" @@ -64,14 +67,13 @@ async def test_actions_get_admin_password(ops_test: OpsTest) -> None: # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} await ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config) - await ops_test.model.wait_for_idle( - apps=[TLS_CERTIFICATES_APP_NAME], status="active", timeout=1000 - ) - # Relate it to OpenSearch to set up TLS. await ops_test.model.integrate(APP_NAME, TLS_CERTIFICATES_APP_NAME) await ops_test.model.wait_for_idle( - apps=[APP_NAME], status="active", timeout=1200, wait_for_exact_units=DEFAULT_NUM_UNITS + apps=[APP_NAME], + status="active", + timeout=1200, + wait_for_exact_units=DEFAULT_NUM_UNITS, ) leader_ip = await get_leader_unit_ip(ops_test) @@ -92,6 +94,7 @@ async def test_actions_get_admin_password(ops_test: OpsTest) -> None: assert result.status == "failed" +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_actions_rotate_admin_password(ops_test: OpsTest) -> None: """Test the rotation and change of admin password.""" diff --git a/tests/integration/tls/__init__.py b/tests/integration/tls/__init__.py new file mode 100644 index 0000000000..db3bfe1a65 --- /dev/null +++ b/tests/integration/tls/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. diff --git a/tests/integration/tls/helpers.py b/tests/integration/tls/helpers.py index 3c3542988c..ce8c184782 100644 --- a/tests/integration/tls/helpers.py +++ b/tests/integration/tls/helpers.py @@ -5,7 +5,7 @@ from pytest_operator.plugin import OpsTest from tenacity import retry, stop_after_attempt, wait_fixed, wait_random -from tests.integration.helpers import http_request +from ..helpers import http_request @retry( diff --git a/tests/integration/tls/test_tls.py b/tests/integration/tls/test_tls.py index fb12c4c125..234241b09e 100644 --- a/tests/integration/tls/test_tls.py +++ b/tests/integration/tls/test_tls.py @@ -7,7 +7,7 @@ import pytest from pytest_operator.plugin import OpsTest -from tests.integration.helpers import ( +from ..helpers import ( APP_NAME, MODEL_CONFIG, SERIES, @@ -17,17 +17,16 @@ get_application_unit_names, get_leader_unit_ip, ) -from tests.integration.helpers_deployments import wait_until -from tests.integration.tls.helpers import ( - check_security_index_initialised, - check_unit_tls_configured, -) +from ..helpers_deployments import wait_until +from ..tls.helpers import check_security_index_initialised, check_unit_tls_configured logger = logging.getLogger(__name__) + TLS_CERTIFICATES_APP_NAME = "self-signed-certificates" +@pytest.mark.group(1) @pytest.mark.abort_on_fail @pytest.mark.skip_if_deployed async def test_build_and_deploy_active(ops_test: OpsTest) -> None: @@ -58,6 +57,7 @@ async def test_build_and_deploy_active(ops_test: OpsTest) -> None: assert len(ops_test.model.applications[APP_NAME].units) == len(UNIT_IDS) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_security_index_initialised(ops_test: OpsTest) -> None: """Test that the security index is well initialised.""" @@ -66,6 +66,7 @@ async def test_security_index_initialised(ops_test: OpsTest) -> None: assert await check_security_index_initialised(ops_test, leader_unit_ip) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_tls_configured(ops_test: OpsTest) -> None: """Test that TLS is enabled when relating to the TLS Certificates Operator.""" @@ -73,6 +74,7 @@ async def test_tls_configured(ops_test: OpsTest) -> None: assert await check_unit_tls_configured(ops_test, unit_ip, unit_name) +@pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_cluster_formation_after_tls(ops_test: OpsTest) -> None: """Test that the cluster formation is successful after TLS setup.""" diff --git a/tests/unit/lib/test_opensearch_secrets.py b/tests/unit/lib/test_opensearch_secrets.py index 60fa011039..77f6cfda8e 100644 --- a/tests/unit/lib/test_opensearch_secrets.py +++ b/tests/unit/lib/test_opensearch_secrets.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock, patch -import pytest from charms.opensearch.v0.constants_charm import ClientRelationName, PeerRelationName from charms.opensearch.v0.constants_tls import CertType from charms.opensearch.v0.opensearch_base_charm import SERVICE_MANAGER @@ -17,7 +16,11 @@ from charm import OpenSearchOperatorCharm -@pytest.mark.usefixtures("only_with_juju_secrets") +class JujuVersionMock: + def has_secrets(self): + return True + + class TestOpenSearchSecrets(TestOpenSearchInternalData): """Ensuring that secrets interfaces and expected behavior are preserved. @@ -37,6 +40,8 @@ def setUp(self): self.secrets = self.charm.secrets self.store = self.charm.secrets + JujuVersion.from_environ = MagicMock(return_value=JujuVersionMock()) + self.peers_rel_id = self.harness.add_relation(PeerRelationName, self.charm.app.name) self.service_rel_id = self.harness.add_relation(SERVICE_MANAGER, self.charm.app.name) self.client_rel_id = self.harness.add_relation(ClientRelationName, "application") diff --git a/tox.ini b/tox.ini index 9b18274619..200a27b269 100644 --- a/tox.ini +++ b/tox.ini @@ -1,147 +1,108 @@ + # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. [tox] no_package = True -skip_missing_interpreters = True -env_list = format, lint, unit -labels = - # Don't run this group in parallel, or with --keep-models because it creates a lot of local VMs. - integration = {charm, tls, ha, ha-networking, ha-multi-clusters, large-deployments, client, h-scaling, ha-storage}-integration +env_list = lint, unit [vars] src_path = {tox_root}/src tests_path = {tox_root}/tests -lib_path = {tox_root}/lib/charms/opensearch/v0 -all_path = {[vars]src_path} {[vars]lib_path} {[vars]tests_path} +lib_path = {tox_root}/lib/charms/opensearch +all_path = {[vars]src_path} {[vars]tests_path} {[vars]lib_path} [testenv] set_env = - PYTHONPATH = {tox_root}:{tox_root}/lib:{[vars]src_path} - PYTHONBREAKPOINT=ipdb.set_trace + PYTHONPATH = {[vars]src_path}:{tox_root}/lib PY_COLORS=1 - LIBJUJU_VERSION_SPECIFIER={env:LIBJUJU_VERSION_SPECIFIER:==3.3.0.0} +allowlist_externals = + poetry +[testenv:build-{production,dev,wrapper}] +# Wrap `charmcraft pack` pass_env = - PYTHONPATH - CHARM_BUILD_DIR - MODEL_SETTINGS - LIBJUJU_VERSION_SPECIFIER + CI +allowlist_externals = + {[testenv]allowlist_externals} + charmcraft + charmcraftcache + mv +commands_pre = + poetry export --only main,charm-libs --output requirements.txt +commands = + build-production: charmcraft pack {posargs} + build-dev: charmcraftcache pack {posargs} +commands_post = + mv requirements.txt requirements-last-build.txt [testenv:format] description = Apply coding style standards to code -deps = - black - isort +commands_pre = + poetry install --only format commands = - isort {[vars]all_path} - black {[vars]all_path} + poetry lock --no-update + poetry run isort {[vars]all_path} + poetry run black {[vars]all_path} [testenv:lint] description = Check code against coding style standards -pass_env = - {[testenv]pass_env} -deps = - black - flake8 - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell +allowlist_externals = + {[testenv]allowlist_externals} + find +commands_pre = + poetry install --only lint commands = - codespell {[vars]lib_path} - codespell {tox_root} --skip {tox_root}/.git --skip {tox_root}/.tox \ - --skip {tox_root}/build --skip {tox_root}/lib --skip {tox_root}/venv \ - --skip {tox_root}/.mypy_cache --skip {tox_root}/icon.svg + poetry check --lock + poetry run codespell {[vars]all_path} # pflake8 wrapper supports config from pyproject.toml - pflake8 {[vars]all_path} - isort --check-only --diff {[vars]all_path} - black --check --diff {[vars]all_path} + # exclude the lib folder, that is copied from lib/charms/data_platform_libs/ + poetry run pflake8 --exclude '.git,__pycache__,.tox,build,dist,*.egg_info,venv,tests/integration/relations/opensearch_provider/application-charm/lib/charms/data_platform_libs/' {[vars]all_path} + poetry run isort --check-only --diff {[vars]all_path} + poetry run black --check --diff {[vars]all_path} + find {[vars]all_path} -type f \( -name "*.sh" -o -name "*.bash" \) -exec poetry run shellcheck --color=always \{\} + [testenv:unit] description = Run unit tests -deps = - pytest - pytest-asyncio - coverage[toml] - -r {tox_root}/requirements.txt +set_env = + {[testenv]set_env} + LIBJUJU_VERSION_SPECIFIER = {env:LIBJUJU_VERSION_SPECIFIER:3.2.3.0} +commands_pre = + poetry install --only main,charm-libs,unit commands = - coverage run --source={[vars]src_path} --source={[vars]lib_path} \ + poetry run coverage run --source={[vars]src_path},{[vars]lib_path} \ -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit - coverage report + poetry run coverage report + poetry run coverage xml -[testenv:{charm, tls, client, ha-base, h-scaling, ha-storage, ha-networking, ha-multi-clusters, large-deployments, plugins}-integration] +[testenv:integration] description = Run integration tests -pass_env = - {[testenv]pass_env} - CI - CI_PACKED_CHARMS - S3_ACCESS_KEY - S3_SECRET_KEY - S3_BUCKET - S3_REGION - S3_SERVER_URL - S3_CA_BUNDLE_PATH - # For AWS testing - AWS_ACCESS_KEY - AWS_SECRET_KEY - # For GCP testing - GCP_ACCESS_KEY - GCP_SECRET_KEY - # Generic secrets from CI: - SECRETS_FROM_GITHUB - TEST_NUM_APP_UNITS -deps = - # This applies to libjuju, not Juju. - juju{env:LIBJUJU_VERSION_SPECIFIER} - opensearch-py - pytest - pytest-asyncio - pytest-operator - -r {tox_root}/requirements.txt -commands = - charm: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/test_charm.py - tls: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/tls/test_tls.py - client: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/relations/opensearch_provider/test_opensearch_provider.py - ha-base: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/ha/test_ha.py - # h-scaling must run on a machine with more than 2 cores - h-scaling: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/ha/test_horizontal_scaling.py - ha-storage: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/ha/test_storage.py - ha-networking: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/ha/test_ha_networking.py - ha-multi-clusters: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/ha/test_ha_multi_clusters.py - large-deployments: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/ha/test_large_deployments.py - plugins: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/plugins/test_plugins.py +set_env = + {[testenv]set_env} + # Workaround for https://github.com/python-poetry/poetry/issues/6958 + POETRY_INSTALLER_PARALLEL = false + LIBJUJU_VERSION_SPECIFIER = {env:LIBJUJU_VERSION_SPECIFIER:3.2.3.0} + CI={env:CI:true} + GITHUB_OUTPUT={env:GITHUB_OUTPUT:} + S3_INTEGRATOR_CHARMPATH={env:S3_INTEGRATOR_CHARMPATH:} + # If not specified, it passes a dummy value that will ignore. + # To not be ignored, the AWS|GCP cloud MUST specify the *_ACCESS_KEY + SECRETS_FROM_GITHUB={env:SECRETS_FROM_GITHUB:{"AWS_SECRET_KEY": "IGNORE", "GCP_SECRET_KEY": "IGNORE"}} +allowlist_externals = + {[testenv:build-wrapper]allowlist_externals} -[testenv:ha-backup-integration] -description = Run integration tests -pass_env = - {[testenv]pass_env} - CI - CI_PACKED_CHARMS - # For microceph testing - S3_ACCESS_KEY - S3_SECRET_KEY - S3_BUCKET - S3_REGION - S3_SERVER_URL - S3_CA_BUNDLE_PATH - # For AWS testing - AWS_ACCESS_KEY - AWS_SECRET_KEY - # For GCP testing - GCP_ACCESS_KEY - GCP_SECRET_KEY - TEST_NUM_APP_UNITS -deps = - # This applies to libjuju, not Juju. - juju{env:LIBJUJU_VERSION_SPECIFIER} - opensearch-py - pytest - pytest-asyncio - pytest-operator - -r {tox_root}/requirements.txt + # Set the testing host before starting the lxd cloud + sudo + sysctl + +commands_pre = + poetry install --only main,charm-libs,integration + + # Set the testing host before starting the lxd cloud + sudo sysctl -w vm.max_map_count=262144 vm.swappiness=0 net.ipv4.tcp_retries2=5 + + {[testenv:build-wrapper]commands_pre} commands = - ha-backup: pytest -v --tb native --log-cli-level=INFO -s {posargs} {[vars]tests_path}/integration/ha/test_backups.py + poetry run pytest -v --tb native --log-cli-level=INFO -s --ignore={[vars]tests_path}/unit/ {posargs} +commands_post = + {[testenv:build-wrapper]commands_post}