diff --git a/.github/workflows/e2e_test.yaml b/.github/workflows/e2e_test.yaml index 3cf5b688bb..8e85be457b 100644 --- a/.github/workflows/e2e_test.yaml +++ b/.github/workflows/e2e_test.yaml @@ -19,7 +19,26 @@ jobs: provider: lxd test-tox-env: integration-juju3.6 modules: '["test_e2e"]' - extra-arguments: "-m openstack" + # INTEGRATION_TOKEN, OS_PASSWORD are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ + # mapping. See CONTRIBUTING.md for more details. + extra-arguments: | + -m=openstack \ + --log-format="%(asctime)s %(levelname)s %(message)s" \ + --path="${{ vars.INTEGRATION_TEST_PATH }}" \ + --openstack-auth-url="${{ vars.INTEGRATION_TEST_OPENSTACK_AUTH_URL }}" \ + --openstack-project-domain-name="${{ vars.INTEGRATION_TEST_OPENSTACK_PROJECT_DOMAIN_NAME }}" \ + --openstack-project-name="${{ vars.INTEGRATION_TEST_OPENSTACK_PROJECT_NAME }}" \ + --openstack-user-domain-name="${{ vars.INTEGRATION_TEST_OPENSTACK_USER_DOMAIN_NAME }}" \ + --openstack-username="${{ vars.INTEGRATION_TEST_OPENSTACK_USERNAME }}" \ + --openstack-region-name="${{ vars.INTEGRATION_TEST_OPENSTACK_REGION_NAME }}" \ + --openstack-network-name="${{ vars.INTEGRATION_TEST_OPENSTACK_NETWORK_NAME }}" \ + --openstack-flavor-name="${{ vars.INTEGRATION_TEST_OPENSTACK_FLAVOR_NAME }}" \ + --https-proxy="${{ vars.INTEGRATION_TEST_HTTPS_PROXY }}" \ + --http-proxy="${{ vars.INTEGRATION_TEST_HTTP_PROXY }}" \ + --no-proxy="${{ vars.INTEGRATION_TEST_NO_PROXY }}" \ + --openstack-https-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_HTTPS_PROXY }}" \ + --openstack-http-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_HTTP_PROXY }}" \ + --openstack-no-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_NO_PROXY }}" self-hosted-runner: true self-hosted-runner-label: pfe-ci diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 5454c62245..617e038d69 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -21,9 +21,29 @@ jobs: provider: lxd test-tox-env: integration-juju3.6 modules: '["test_charm_metrics_failure", "test_charm_metrics_success", "test_charm_fork_repo", "test_charm_fork_path_change", "test_charm_no_runner", "test_charm_runner", "test_debug_ssh", "test_charm_upgrade", "test_reactive"]' - extra-arguments: '-m openstack --log-format="%(asctime)s %(levelname)s %(message)s"' + # INTEGRATION_TOKEN, OS_PASSWORD are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ + # mapping. See CONTRIBUTING.md for more details. + extra-arguments: | + -m=openstack \ + --log-format="%(asctime)s %(levelname)s %(message)s" \ + --path="${{ vars.INTEGRATION_TEST_PATH }}" \ + --openstack-auth-url="${{ vars.INTEGRATION_TEST_OPENSTACK_AUTH_URL }}" \ + --openstack-project-domain-name="${{ vars.INTEGRATION_TEST_OPENSTACK_PROJECT_DOMAIN_NAME }}" \ + --openstack-project-name="${{ vars.INTEGRATION_TEST_OPENSTACK_PROJECT_NAME }}" \ + --openstack-user-domain-name="${{ vars.INTEGRATION_TEST_OPENSTACK_USER_DOMAIN_NAME }}" \ + --openstack-username="${{ vars.INTEGRATION_TEST_OPENSTACK_USERNAME }}" \ + --openstack-region-name="${{ vars.INTEGRATION_TEST_OPENSTACK_REGION_NAME }}" \ + --openstack-network-name="${{ vars.INTEGRATION_TEST_OPENSTACK_NETWORK_NAME }}" \ + --openstack-flavor-name="${{ vars.INTEGRATION_TEST_OPENSTACK_FLAVOR_NAME }}" \ + --https-proxy="${{ vars.INTEGRATION_TEST_HTTPS_PROXY }}" \ + --http-proxy="${{ vars.INTEGRATION_TEST_HTTP_PROXY }}" \ + --no-proxy="${{ vars.INTEGRATION_TEST_NO_PROXY }}" \ + --openstack-https-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_HTTPS_PROXY }}" \ + --openstack-http-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_HTTP_PROXY }}" \ + --openstack-no-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_NO_PROXY }}" \ + --test-image-id="${{ vars.INTEGRATION_TEST_IMAGE_ID }}" self-hosted-runner: true - self-hosted-runner-label: pfe-ci + self-hosted-runner-label: pfe-ci openstack-integration-tests-cross-controller-private-endpoint: name: Cross controller integration test using private-endpoint uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main @@ -34,7 +54,27 @@ jobs: provider: lxd test-tox-env: integration-juju3.6 modules: '["test_prometheus_metrics"]' - extra-arguments: '-m openstack --log-format="%(asctime)s %(levelname)s %(message)s"' + # INTEGRATION_TOKEN, OS_PASSWORD are passed through INTEGRATION_TEST_SECRET_ENV_VALUE_ + # mapping. See CONTRIBUTING.md for more details. + extra-arguments: | + -m=openstack \ + --log-format="%(asctime)s %(levelname)s %(message)s" \ + --path="${{ vars.INTEGRATION_TEST_PATH }}" \ + --openstack-auth-url="${{ vars.INTEGRATION_TEST_OPENSTACK_AUTH_URL }}" \ + --openstack-project-domain-name="${{ vars.INTEGRATION_TEST_OPENSTACK_PROJECT_DOMAIN_NAME }}" \ + --openstack-project-name="${{ vars.INTEGRATION_TEST_OPENSTACK_PROJECT_NAME }}" \ + --openstack-user-domain-name="${{ vars.INTEGRATION_TEST_OPENSTACK_USER_DOMAIN_NAME }}" \ + --openstack-username="${{ vars.INTEGRATION_TEST_OPENSTACK_USERNAME }}" \ + --openstack-region-name="${{ vars.INTEGRATION_TEST_OPENSTACK_REGION_NAME }}" \ + --openstack-network-name="${{ vars.INTEGRATION_TEST_OPENSTACK_NETWORK_NAME }}" \ + --openstack-flavor-name="${{ vars.INTEGRATION_TEST_OPENSTACK_FLAVOR_NAME }}" \ + --https-proxy="${{ vars.INTEGRATION_TEST_HTTPS_PROXY }}" \ + --http-proxy="${{ vars.INTEGRATION_TEST_HTTP_PROXY }}" \ + --no-proxy="${{ vars.INTEGRATION_TEST_NO_PROXY }}" \ + --openstack-https-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_HTTPS_PROXY }}" \ + --openstack-http-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_HTTP_PROXY }}" \ + --openstack-no-proxy="${{ vars.INTEGRATION_TEST_OPENSTACK_NO_PROXY }}" \ + --test-image-id="${{ vars.INTEGRATION_TEST_IMAGE_ID }}" self-hosted-runner: true self-hosted-runner-label: pfe-ci allure-report: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5457a6489..eef030a238 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,19 @@ that can be used for linting and formatting code when you're preparing contribut > NOTE: Integration tests are currently intended to run on the CI and may not work locally without further argument. + +#### CI testing setup guidelines + + +To prefer explicit setting arguments passing, use `extra-arguments` of the +[following reusable workflow](https://github.com/canonical/operator-workflows/blob/main/.github/workflows/integration_test_run.yaml) +to pass in non-sensitive values. +For sensitive values (`INTEGRATION_TOKEN`: `--token`, `OS_PASSWORD`: `--openstack-password`), map +them via `INTEGRATION_TEST_SECRET_ENV_NAME_` +environment variable settings under [repository](https://github.com/canonical/github-runner-operator) +> settings > variables > actions. +This is to prevent GitHub from leaking secrets when passing them over the CLI calls (unresolved). + ### Building the charm Build the charm in this git repository using: diff --git a/actions.yaml b/actions.yaml index a8b1be89e7..cb0bb0b5ce 100644 --- a/actions.yaml +++ b/actions.yaml @@ -2,6 +2,6 @@ # See LICENSE file for licensing details. check-runners: - description: Get info on active and registered runners + description: Get info on active and registered runners flush-runners: - description: Clear out all runners and start a new set + description: Clear out all runners and start a new set diff --git a/charmcraft.yaml b/charmcraft.yaml index 72266be193..d438d84b3b 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -2,28 +2,30 @@ # See LICENSE file for licensing details. type: charm + parts: charm: charm-python-packages: - - setuptools>=75.8.0 # for jinja2 + - setuptools>=75.8.0 # for jinja2 build-packages: - - git # for installing git source of pylxd - - libffi-dev # for cffi - - libssl-dev # for cryptography + - git # for installing git source of pylxd + - libffi-dev # for cffi + - libssl-dev # for cryptography - pkg-config # for cryptography build-snaps: - rustup # for cryptography override-build: | rustup default stable craftctl default + bases: - build-on: - - name: "ubuntu" - channel: "22.04" - architectures: - - amd64 + - name: "ubuntu" + channel: "22.04" + architectures: + - amd64 run-on: - - name: "ubuntu" - channel: "22.04" - architectures: - - amd64 + - name: "ubuntu" + channel: "22.04" + architectures: + - amd64 diff --git a/metadata.yaml b/metadata.yaml index c16c8ca5a2..91767d2864 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -12,7 +12,6 @@ summary: Creates a group of self-hosted github runners. description: | A [Juju](https://juju.is/) [charm](https://juju.is/docs/olm/charmed-operators) managing [self-hosted runners for GitHub Actions](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners). - Each unit of this charm will start a configurable number of virtual machines to host self-hosted runners. Each runner performs only one job, after which it unregisters from GitHub to ensure that each job runs in a clean environment. The charm will periodically check the number of diff --git a/tests/conftest.py b/tests/conftest.py index 9968b1d664..ddae78b641 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ """Fixtures for github runner charm.""" +import os from pytest import Parser @@ -25,13 +26,11 @@ def pytest_addoption(parser: Parser): "An optionally comma separated GitHub Personal Access Token(s). " "Add more than one to help reduce rate limiting." ), + default=os.environ.get("INTEGRATION_TOKEN"), ) parser.addoption( "--charm-file", action="store", help="The prebuilt github-runner-operator charm file." ) - parser.addoption( - "--token-alt", action="store", help="An alternative token to test the change of a token." - ) parser.addoption( "--http-proxy", action="store", @@ -47,12 +46,6 @@ def pytest_addoption(parser: Parser): action="store", help="No proxy configuration value for juju model proxy configuration.", ) - parser.addoption( - "--openstack-clouds-yaml", - action="store", - help="The OpenStack clouds yaml file for the charm to use.", - default="", - ) parser.addoption( "--use-existing-app-suffix", action="store", @@ -83,95 +76,56 @@ def pytest_addoption(parser: Parser): ) # Private endpoint options AMD64 parser.addoption( - "--openstack-network-name-amd64", + "--openstack-network-name", action="store", help="The Openstack network to create testing instances under.", ) parser.addoption( - "--openstack-flavor-name-amd64", + "--openstack-flavor-name", action="store", help="The Openstack flavor to create testing instances with.", ) parser.addoption( - "--openstack-auth-url-amd64", + "--openstack-auth-url", action="store", help="The URL to Openstack authentication service, i.e. keystone.", + default=os.environ.get("OS_AUTH_URL"), ) parser.addoption( - "--openstack-password-amd64", + "--openstack-password", action="store", help="The password to authenticate to Openstack service.", + default=os.environ.get("OS_PASSWORD"), ) parser.addoption( - "--openstack-project-domain-name-amd64", + "--openstack-project-domain-name", action="store", help="The Openstack project domain name to use.", + default=os.environ.get("OS_PROJECT_DOMAIN_NAME"), ) parser.addoption( - "--openstack-project-name-amd64", + "--openstack-project-name", action="store", help="The Openstack project name to use.", + default=os.environ.get("OS_PROJECT_NAME"), ) parser.addoption( - "--openstack-user-domain-name-amd64", + "--openstack-user-domain-name", action="store", help="The Openstack user domain name to use.", + default=os.environ.get("OS_USER_DOMAIN_NAME"), ) parser.addoption( - "--openstack-username-amd64", + "--openstack-username", action="store", help="The Openstack user to authenticate as.", + default=os.environ.get("OS_USERNAME"), ) parser.addoption( - "--openstack-region-name-amd64", - action="store", - help="The Openstack region to authenticate to.", - ) - # Private endpoint options ARM64 - parser.addoption( - "--openstack-network-name-arm64", - action="store", - help="The Openstack network to create testing instances under.", - ) - parser.addoption( - "--openstack-flavor-name-arm64", - action="store", - help="The Openstack flavor to create testing instances with.", - ) - parser.addoption( - "--openstack-auth-url-arm64", - action="store", - help="The URL to Openstack authentication service, i.e. keystone.", - ) - parser.addoption( - "--openstack-password-arm64", - action="store", - help="The password to authenticate to Openstack service.", - ) - parser.addoption( - "--openstack-project-domain-name-arm64", - action="store", - help="The Openstack project domain name to use.", - ) - parser.addoption( - "--openstack-project-name-arm64", - action="store", - help="The Openstack project name to use.", - ) - parser.addoption( - "--openstack-user-domain-name-arm64", - action="store", - help="The Openstack user domain name to use.", - ) - parser.addoption( - "--openstack-username-arm64", - action="store", - help="The Openstack user to authenticate as.", - ) - parser.addoption( - "--openstack-region-name-arm64", + "--openstack-region-name", action="store", help="The Openstack region to authenticate to.", + default=os.environ.get("OS_REGION_NAME"), ) # Interface testing args parser.addoption( @@ -186,3 +140,10 @@ def pytest_addoption(parser: Parser): help="The flavor for testing openstack interfaces. The resource should be enough to boot the test image.", default=None, ) + parser.addoption( + "--test-image-id", + action="store", + help="The image ID to use for testing. If provided, any-charm will be used to mock the " + "image builder for faster testing deployment.", + default=None, + ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index bf9f9a01a7..b98ea52c58 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -2,11 +2,12 @@ # See LICENSE file for licensing details. """Fixtures for github runner charm integration tests.""" +import json import logging -import os import random import secrets import string +import textwrap from pathlib import Path from time import sleep from typing import Any, AsyncGenerator, AsyncIterator, Generator, Iterator, Optional, cast @@ -95,20 +96,13 @@ def image_builder_app_name(random_app_name_suffix: str) -> str: return f"github-runner-image-builder-{random_app_name_suffix}" -@pytest.fixture(scope="module", name="openstack_clouds_yaml") -def openstack_clouds_yaml_fixture(pytestconfig: pytest.Config) -> str | None: - """The openstack clouds yaml config.""" - return pytestconfig.getoption("--openstack-clouds-yaml") - - @pytest.fixture(scope="module") -def charm_file(pytestconfig: pytest.Config, openstack_clouds_yaml: Optional[str]) -> str: +def charm_file(pytestconfig: pytest.Config) -> str: """Path to the built charm.""" charm = pytestconfig.getoption("--charm-file") assert charm, "Please specify the --charm-file command line option" - charm_path_str = f"./{charm}" - return charm_path_str + return f"./{charm}" @pytest.fixture(scope="module") @@ -125,25 +119,13 @@ def path(pytestconfig: pytest.Config) -> str: @pytest.fixture(scope="module") def token(pytestconfig: pytest.Config) -> str: """Configured token setting.""" - token = pytestconfig.getoption("--token") or os.environ.get("INTEGRATION_TOKEN") + token = pytestconfig.getoption("--token") assert token, "Please specify the --token command line option" tokens = {token.strip() for token in token.split(",")} random_token = random.choice(list(tokens)) return random_token -@pytest.fixture(scope="module") -def token_alt(pytestconfig: pytest.Config, token: str) -> str: - """Configured token_alt setting.""" - token_alt = pytestconfig.getoption("--token-alt") or os.environ.get("INTEGRATION_TOKEN_ALT") - assert token_alt, ( - "Please specify the --token-alt command line option with GitHub Personal " - "Access Token value." - ) - assert token_alt != token, "Please specify a different token for --token-alt" - return token_alt - - @pytest.fixture(scope="module") def http_proxy(pytestconfig: pytest.Config) -> str: """Configured http_proxy setting.""" @@ -187,21 +169,20 @@ def openstack_no_proxy_fixture(pytestconfig: pytest.Config) -> str: @pytest.fixture(scope="module", name="private_endpoint_config") -def private_endpoint_config_fixture(pytestconfig: pytest.Config) -> PrivateEndpointConfigs | None: +def private_endpoint_config_fixture(pytestconfig: pytest.Config) -> PrivateEndpointConfigs: """The private endpoint configuration values.""" - auth_url = pytestconfig.getoption("--openstack-auth-url-amd64") - password = pytestconfig.getoption("--openstack-password-amd64") - password = password or os.environ.get("INTEGRATION_OPENSTACK_PASSWORD_AMD64") + auth_url = pytestconfig.getoption("--openstack-auth-url") + password = pytestconfig.getoption("--openstack-password") assert ( password - ), "Please specify the --openstack-password-amd64 option or INTEGRATION_OPENSTACK_PASSWORD_AMD64 environment variable" - project_domain_name = pytestconfig.getoption("--openstack-project-domain-name-amd64") - project_name = pytestconfig.getoption("--openstack-project-name-amd64") - user_domain_name = pytestconfig.getoption("--openstack-user-domain-name-amd64") - user_name = pytestconfig.getoption("--openstack-username-amd64") - region_name = pytestconfig.getoption("--openstack-region-name-amd64") - if any( - not val + ), "Please specify the --openstack-password option or OS_PASSWORD environment variable" + project_domain_name = pytestconfig.getoption("--openstack-project-domain-name") + project_name = pytestconfig.getoption("--openstack-project-name") + user_domain_name = pytestconfig.getoption("--openstack-user-domain-name") + user_name = pytestconfig.getoption("--openstack-username") + region_name = pytestconfig.getoption("--openstack-region-name") + assert all( + val for val in ( auth_url, password, @@ -211,8 +192,7 @@ def private_endpoint_config_fixture(pytestconfig: pytest.Config) -> PrivateEndpo user_name, region_name, ) - ): - return None + ), "Specify all OpenStack private endpoint options." return { "auth_url": auth_url, "password": str(password), @@ -224,13 +204,9 @@ def private_endpoint_config_fixture(pytestconfig: pytest.Config) -> PrivateEndpo } -@pytest.fixture(scope="module", name="private_endpoint_clouds_yaml") -def private_endpoint_clouds_yaml_fixture( - private_endpoint_config: PrivateEndpointConfigs | None, -) -> Optional[str]: +@pytest.fixture(scope="module", name="clouds_yaml_contents") +def clouds_yaml_contents_fixture(private_endpoint_config: PrivateEndpointConfigs) -> str: """The openstack private endpoint clouds yaml.""" - if not private_endpoint_config: - return None return string.Template( Path("tests/integration/data/clouds.yaml.tmpl").read_text(encoding="utf-8") ).substitute( @@ -246,33 +222,18 @@ def private_endpoint_clouds_yaml_fixture( ) -@pytest.fixture(scope="module", name="clouds_yaml_contents") -def clouds_yaml_contents_fixture( - openstack_clouds_yaml: Optional[str], private_endpoint_clouds_yaml: Optional[str] -): - """The Openstack clouds yaml or private endpoint cloud yaml contents.""" - clouds_yaml_contents = openstack_clouds_yaml or private_endpoint_clouds_yaml - assert clouds_yaml_contents, ( - "Please specify --openstack-clouds-yaml or all of private endpoint arguments " - "(--openstack-auth-url, --openstack-password, --openstack-project-domain-name, " - "--openstack-project-name, --openstack-user-domain-name, --openstack-user-name, " - "--openstack-region-name)" - ) - return clouds_yaml_contents - - @pytest.fixture(scope="module", name="network_name") def network_name_fixture(pytestconfig: pytest.Config) -> str: """Network to use to spawn test instances under.""" - network_name = pytestconfig.getoption("--openstack-network-name-amd64") - assert network_name, "Please specify the --openstack-network-name-amd64 command line option" + network_name = pytestconfig.getoption("--openstack-network-name") + assert network_name, "Please specify the --openstack-network-name command line option" return network_name @pytest.fixture(scope="module", name="flavor_name") def flavor_name_fixture(pytestconfig: pytest.Config) -> str: """Flavor to create testing instances with.""" - flavor_name = pytestconfig.getoption("--openstack-flavor-name-amd64") + flavor_name = pytestconfig.getoption("--openstack-flavor-name") assert flavor_name, "Please specify the --openstack-flavor-name command line option" return flavor_name @@ -293,6 +254,12 @@ def openstack_test_flavor_fixture(pytestconfig: pytest.Config) -> str: return test_flavor +@pytest.fixture(scope="module", name="test_image_id") +def test_image_id_fixture(pytestconfig: pytest.Config) -> Optional[str]: + """The test image ID for mocking image builder.""" + return pytestconfig.getoption("--test-image-id") + + @pytest.fixture(scope="module", name="openstack_connection") def openstack_connection_fixture( clouds_yaml_contents: str, @@ -407,33 +374,76 @@ async def image_builder_fixture( model: Model, existing_app_suffix: Optional[str], image_builder_app_name: str, + test_image_id: Optional[str], image_builder_config: dict, - flavor_name: str, - network_name: str, - openstack_model_proxy: None, openstack_connection, + request: pytest.FixtureRequest, ): - """The image builder application for OpenStack runners.""" - if not existing_app_suffix: - application_name = image_builder_app_name - app = await model.deploy( + """The image builder application for OpenStack runners. + + If --test-image-id is provided, uses any-charm to mock the image relation. + Otherwise, deploys the real github-runner-image-builder charm. + """ + if existing_app_suffix: + logging.info("Using existing image builder %s", image_builder_app_name) + yield model.applications[image_builder_app_name] + return + + if not test_image_id: + logging.info("Deploying image builder %s", image_builder_app_name) + # Deploy the real github-runner-image-builder + yield await model.deploy( "github-runner-image-builder", - application_name=application_name, + application_name=image_builder_app_name, channel="latest/edge", config=image_builder_config, + constraints={ + "root-disk": 20 * 1024, + "mem": 2 * 1024, + # 2025-11-26: Set deployment type to virtual-machine due to bug with snapd. See: + # https://github.com/canonical/snapd/pull/16131 + "virt-type": "virtual-machine", + "cores": 2, + }, ) - else: - app = model.applications[image_builder_app_name] - yield app - if not existing_app_suffix: # The github-image-builder does not clean keypairs. Until it does, # we clean them manually here. + logging.info("Cleaning up image builder resources...") for key in openstack_connection.list_keypairs(): key_name: str = key.name if key_name.startswith(image_builder_app_name): openstack_connection.delete_keypair(key_name) + return + + # Use any-charm to mock the image relation provider + any_charm_src_overwrite = { + "any_charm.py": textwrap.dedent( + f"""\ + from any_charm_base import AnyCharmBase + + class AnyCharm(AnyCharmBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.framework.observe(self.on['provide-github-runner-image-v0'].\ +relation_changed, self._image_relation_changed) + + def _image_relation_changed(self, event): + # Provide mock image relation data + event.relation.data[self.unit]['id'] = '{test_image_id}' + event.relation.data[self.unit]['tags'] = 'jammy, amd64' + """ + ), + } + logging.info("Deploying fake image builder via any-charm for image ID %s", test_image_id) + yield await model.deploy( + "any-charm", + application_name=image_builder_app_name, + channel="latest/beta", + config={"src-overwrite": json.dumps(any_charm_src_overwrite)}, + ) + @pytest_asyncio.fixture(scope="module", name="app_openstack_runner") async def app_openstack_runner_fixture( @@ -466,10 +476,7 @@ async def app_openstack_runner_fixture( https_proxy=openstack_https_proxy, no_proxy=openstack_no_proxy, reconcile_interval=DEFAULT_RECONCILE_INTERVAL, - constraints={ - "root-disk": 50 * 1024, - "mem": 2 * 1024, - }, + constraints={"root-disk": 50 * 1024, "mem": 2 * 1024, "virt-type": "virtual-machine"}, config={ OPENSTACK_CLOUDS_YAML_CONFIG_NAME: clouds_yaml_contents, OPENSTACK_NETWORK_CONFIG_NAME: network_name, @@ -480,7 +487,7 @@ async def app_openstack_runner_fixture( }, wait_idle=False, ) - await model.integrate(f"{image_builder.name}:image", f"{application.name}:image") + await model.integrate(image_builder.name, f"{application.name}:image") await model.wait_for_idle( apps=[application.name, image_builder.name], status=ACTIVE, @@ -590,6 +597,9 @@ async def tmate_ssh_server_app_fixture(model: Model) -> AsyncIterator[Applicatio tmate_app: Application = await model.deploy( "tmate-ssh-server", channel="edge", + # 2025-11-26: Set deployment type to virtual-machine due to bug with snapd. See: + # https://github.com/canonical/snapd/pull/16131 + constraints={"virt-type": "virtual-machine"}, ) return tmate_app @@ -633,6 +643,8 @@ def forked_github_repository( github_repository: Repository, ) -> Iterator[Repository]: """Create a fork for a GitHub repository.""" + # After fork creation, the repository workflow run must be enabled manually. Otherwise, a 404 + # on the workflow get API will be returned. forked_repository = github_repository.create_fork(name=f"test-{github_repository.name}") # Wait for repo to be ready @@ -740,7 +752,13 @@ async def app_for_metric_fixture( async def mongodb_fixture(model: Model, existing_app_suffix: str | None) -> Application: """Deploy MongoDB.""" if not existing_app_suffix: - mongodb = await model.deploy(MONGODB_APP_NAME, channel="6/edge") + mongodb = await model.deploy( + MONGODB_APP_NAME, + channel="6/edge", + # 2025-11-26: Set deployment type to virtual-machine due to bug with snapd. See: + # https://github.com/canonical/snapd/pull/16131 + constraints={"virt-type": "virtual-machine"}, + ) else: mongodb = model.applications["mongodb"] return mongodb diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index 0146cbb015..45042e316f 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -46,7 +46,9 @@ DISPATCH_E2E_TEST_RUN_OPENSTACK_WORKFLOW_FILENAME = "e2e_test_run_openstack.yaml" MONGODB_APP_NAME = "mongodb" -DEFAULT_RUNNER_CONSTRAINTS = {"root-disk": 15} +# 2025-11-26: Set deployment type to virtual-machine due to bug with snapd. See: +# https://github.com/canonical/snapd/pull/16131 +DEFAULT_RUNNER_CONSTRAINTS = {"root-disk": 20 * 1024, "virt-type": "virtual-machine"} logger = logging.getLogger(__name__) @@ -208,6 +210,7 @@ async def deploy_github_runner_charm( charm_file, application_name=app_name, base="ubuntu@22.04", + series="jammy", config=default_config, constraints=constraints or DEFAULT_RUNNER_CONSTRAINTS, **(deploy_kwargs or {}), diff --git a/tests/integration/test_charm_upgrade.py b/tests/integration/test_charm_upgrade.py index 8da401074d..21eb56c821 100644 --- a/tests/integration/test_charm_upgrade.py +++ b/tests/integration/test_charm_upgrade.py @@ -91,7 +91,7 @@ async def test_charm_upgrade( }, wait_idle=False, ) - await model.integrate(f"{image_builder.name}:image", f"{application.name}:image") + await model.integrate(f"{image_builder.name}", f"{application.name}:image") await model.wait_for_idle( apps=[application.name, image_builder.name], raise_on_error=False, diff --git a/tox.ini b/tox.ini index a09c975544..c691f5f7bc 100644 --- a/tox.ini +++ b/tox.ini @@ -114,9 +114,15 @@ commands = description = Run integration tests pass_env = PYTEST_ADDOPTS - INTEGRATION_OPENSTACK_PASSWORD_AMD64 INTEGRATION_TOKEN INTEGRATION_TOKEN_ALT + OS_AUTH_URL + OS_PASSWORD + OS_PROJECT_DOMAIN_NAME + OS_PROJECT_NAME + OS_REGION_NAME + OS_USER_DOMAIN_NAME + OS_USERNAME deps = juju3.1: juju==3.1.* juju3.6: juju==3.6.*