diff --git a/.github/workflows/integration_test_charm.yaml b/.github/workflows/integration_test_charm.yaml index 17514b0d..6f559d62 100644 --- a/.github/workflows/integration_test_charm.yaml +++ b/.github/workflows/integration_test_charm.yaml @@ -13,8 +13,16 @@ on: Use canonical/data-platform-workflows build_charm.yaml to build the charm(s) required: true type: string + architecture: + # Keep description synchronized with "Parse architecture input" step + description: | + Processor architecture + + Must be one of "amd64", "arm64" + default: amd64 + type: string cloud: - # Keep description synchronized with "Validate input" step + # Keep description synchronized with "Parse cloud input" step description: | Juju cloud @@ -102,13 +110,34 @@ jobs: # ci.yaml will not show up on the GitHub Actions sidebar. # If this workflow is called with a matrix (e.g. to test multiple juju versions), the ci.yaml # job name containing the Juju version will be lost. - # So, we add the Juju version to one of the first jobs in this workflow. + # So, we add the Juju version & architecture to one of the first jobs in this workflow. # (In the UI, when this workflow is called with a matrix, GitHub will separate each matrix # combination and preserve job ordering within a matrix combination.) - name: ${{ inputs.juju-agent-version || inputs.juju-snap-channel }} | Collect integration test groups + name: ${{ inputs.juju-agent-version || inputs.juju-snap-channel }} | ${{ inputs.architecture }} | Collect integration test groups runs-on: ubuntu-latest timeout-minutes: 5 steps: + - name: Parse architecture input + id: parse-architecture + shell: python + # Keep synchronized with inputs.architecture description + run: | + import json + import os + + DEFAULT_RUNNERS = { + "amd64": "ubuntu-latest", + "arm64": "Ubuntu_ARM64_4C_16G_01", + } + ARCHITECTURE = "${{ inputs.architecture }}" + try: + default_runner = DEFAULT_RUNNERS[ARCHITECTURE] + except KeyError: + raise ValueError(f"`architecture` input not recognized: {ARCHITECTURE}") + output = f"default_runner={json.dumps(default_runner)}" + print(output) + with open(os.environ["GITHUB_OUTPUT"], "a") as file: + file.write(output) - name: Checkout uses: actions/checkout@v4 - name: Install tox & poetry @@ -135,6 +164,7 @@ jobs: run: tox run -e integration -- tests/integration -m '${{ steps.select-test-stability.outputs.mark_expression }}' --collect-groups outputs: groups: ${{ steps.collect-groups.outputs.groups }} + default_runner: ${{ steps.parse-architecture.outputs.default_runner }} integration-test: strategy: @@ -145,11 +175,11 @@ jobs: needs: - get-workflow-version - collect-integration-tests - runs-on: ${{ matrix.groups.runner || 'ubuntu-latest' }} + runs-on: ${{ matrix.groups.runner || fromJSON(needs.collect-integration-tests.outputs.default_runner) }} timeout-minutes: 120 steps: - name: Free up disk space - if: ${{ !matrix.groups.self_hosted }} + if: ${{ !(inputs.architecture == 'arm64' || matrix.groups.self_hosted) }} run: | printf '\nDisk usage before cleanup\n' df --human-readable @@ -159,10 +189,13 @@ jobs: printf '\nDisk usage after cleanup\n' df --human-readable - name: (self-hosted) Disk usage - if: ${{ matrix.groups.self_hosted }} + if: ${{ inputs.architecture == 'arm64' || matrix.groups.self_hosted }} run: df --human-readable + - name: (arm64 GitHub-hosted) Link python to python3 + if: ${{ inputs.architecture == 'arm64' && !matrix.groups.self_hosted }} + run: sudo ln -s /usr/bin/python3 /usr/bin/python - name: (self-hosted) Install pipx - if: ${{ matrix.groups.self_hosted }} + if: ${{ inputs.architecture == 'arm64' || matrix.groups.self_hosted }} run: | sudo apt-get update sudo apt-get install python3-pip python3-venv -y @@ -275,6 +308,11 @@ jobs: juju add-model test pipx install tox pipx install poetry + - name: Add architecture model constraint + if: ${{ inputs.cloud == 'lxd' }} + # Unable to set constraint on all models because of Juju bug: + # https://bugs.launchpad.net/juju/+bug/2065050 + run: juju set-model-constraints arch='${{ inputs.architecture }}' - name: Update python-libjuju version if: ${{ inputs.libjuju-version-constraint }} run: poetry add --lock --group integration juju@'${{ inputs.libjuju-version-constraint }}' @@ -319,7 +357,7 @@ jobs: if: ${{ (success() || (failure() && steps.tests.outcome == 'failure')) && inputs._beta_allure_report && github.event_name == 'schedule' && github.run_attempt == '1' }} uses: actions/upload-artifact@v4 with: - name: allure-results-integration-test-charm-${{ inputs.cloud }}-juju-${{ inputs.juju-agent-version || steps.parse-versions.outputs.snap_channel_for_artifact }}-${{ matrix.groups.artifact_group_id }} + name: allure-results-integration-test-charm-${{ inputs.cloud }}-juju-${{ inputs.juju-agent-version || steps.parse-versions.outputs.snap_channel_for_artifact }}-${{ inputs.architecture }}-${{ matrix.groups.artifact_group_id }} path: allure-results/ if-no-files-found: error - name: Select model @@ -348,7 +386,7 @@ jobs: if: ${{ success() || (failure() && steps.tests.outcome == 'failure') }} uses: actions/upload-artifact@v4 with: - name: logs-intergration-test-charm-${{ inputs.cloud }}-juju-${{ inputs.juju-agent-version || steps.parse-versions.outputs.snap_channel_for_artifact }}-${{ matrix.groups.artifact_group_id }} + name: logs-intergration-test-charm-${{ inputs.cloud }}-juju-${{ inputs.juju-agent-version || steps.parse-versions.outputs.snap_channel_for_artifact }}-${{ inputs.architecture }}-${{ matrix.groups.artifact_group_id }} path: ~/logs/ if-no-files-found: error - name: Disk usage @@ -393,7 +431,7 @@ jobs: uses: actions/download-artifact@v4 with: path: allure-results/ - pattern: allure-results-integration-test-charm-${{ inputs.cloud }}-juju-${{ inputs.juju-agent-version || needs.integration-test.outputs.juju-snap-channel-for-artifact }}-* + pattern: allure-results-integration-test-charm-${{ inputs.cloud }}-juju-${{ inputs.juju-agent-version || needs.integration-test.outputs.juju-snap-channel-for-artifact }}-${{ inputs.architecture }}-* merge-multiple: true - name: Load test report history run: | diff --git a/python/pytest_plugins/pytest_operator_cache/pytest_operator_cache/_plugin.py b/python/pytest_plugins/pytest_operator_cache/pytest_operator_cache/_plugin.py index 06f7cd30..f7e1b26d 100644 --- a/python/pytest_plugins/pytest_operator_cache/pytest_operator_cache/_plugin.py +++ b/python/pytest_plugins/pytest_operator_cache/pytest_operator_cache/_plugin.py @@ -1,5 +1,6 @@ import os import pathlib +import subprocess import typing import yaml @@ -22,17 +23,31 @@ async def build_charm( self, charm_path: typing.Union[str, os.PathLike], bases_index: int = None ) -> pathlib.Path: charm_path = pathlib.Path(charm_path) - # TODO: add support for multiple architectures + architecture = subprocess.run( + ["dpkg", "--print-architecture"], + capture_output=True, + check=True, + encoding="utf-8", + ).stdout.strip() + assert architecture in ("amd64", "arm64") if bases_index is not None: charmcraft_yaml = yaml.safe_load((charm_path / "charmcraft.yaml").read_text()) assert charmcraft_yaml["type"] == "charm" base = charmcraft_yaml["bases"][bases_index] # Handle multiple base formats # See https://discourse.charmhub.io/t/charmcraft-bases-provider-support/4713 - version = base.get("build-on", [base])[0]["channel"] - packed_charms = list(charm_path.glob(f"*{version}-amd64.charm")) + build_on = base.get("build-on", [base])[0] + version = build_on["channel"] + architectures = build_on.get("architectures", ["amd64"]) + assert ( + len(architectures) == 1 + ), f"Multiple architectures ({architectures}) in one (charmcraft.yaml) base not supported. Use one base per architecture" + assert ( + architectures[0] == architecture + ), f"Architecture for {bases_index=} ({architectures[0]}) does not match host architecture ({architecture})" + packed_charms = list(charm_path.glob(f"*{version}-{architecture}.charm")) else: - packed_charms = list(charm_path.glob("*-amd64.charm")) + packed_charms = list(charm_path.glob(f"*-{architecture}.charm")) if len(packed_charms) == 1: # python-libjuju's model.deploy(), juju deploy, and juju bundle files expect local charms # to begin with `./` or `/` to distinguish them from Charmhub charms. @@ -43,13 +58,11 @@ async def build_charm( # `pathlib.Path`.) return packed_charms[0].resolve(strict=True) elif len(packed_charms) > 1: - message = f"More than one matching .charm file found at {charm_path=}: {packed_charms}." + message = f"More than one matching .charm file found at {charm_path=} for {architecture=}: {packed_charms}." if bases_index is None: message += " Specify `bases_index`" - else: - message += " Does charmcraft.yaml contain non-amd64 architecture?" raise ValueError(message) else: raise ValueError( - f"Unable to find amd64 .charm file for {bases_index=} at {charm_path=}" + f"Unable to find .charm file for {architecture=} and {bases_index=} at {charm_path=}" )