Skip to content

Commit 3dba162

Browse files
All integrations tests should run in openstack (#413)
1 parent 318550e commit 3dba162

File tree

8 files changed

+225
-101
lines changed

8 files changed

+225
-101
lines changed

.github/workflows/integration_test.yaml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@ jobs:
1616
secrets: inherit
1717
with:
1818
juju-channel: 3.1/stable
19-
pre-run-script: scripts/pre-integration-test.sh
19+
pre-run-script: scripts/setup-lxd.sh
2020
provider: lxd
2121
test-tox-env: integration-juju3.1
22-
# These important local LXD test have no OpenStack integration versions.
23-
# test_charm_scheduled_events ensures reconcile events are fired on a schedule.
24-
# test_debug_ssh ensures tmate SSH actions works.
25-
# The test test_charm_upgrade needs to run to ensure the charm can be upgraded.
2622
modules: '["test_charm_scheduled_events", "test_debug_ssh", "test_charm_upgrade"]'
23+
extra-arguments: "-m openstack"
24+
self-hosted-runner: true
25+
self-hosted-runner-label: stg-private-endpoint
2726
openstack-interface-tests-private-endpoint:
2827
name: openstack interface test using private-endpoint
2928
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
@@ -39,7 +38,6 @@ jobs:
3938
openstack-integration-tests-private-endpoint:
4039
name: Integration test using private-endpoint
4140
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
42-
needs: openstack-interface-tests-private-endpoint
4341
secrets: inherit
4442
with:
4543
juju-channel: 3.6/stable

tests/integration/conftest.py

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ async def app_openstack_runner_fixture(
440440
reconcile_interval=60,
441441
constraints={
442442
"root-disk": 50 * 1024,
443-
"mem": 16 * 1024,
443+
"mem": 2 * 1024,
444444
},
445445
config={
446446
OPENSTACK_CLOUDS_YAML_CONFIG_NAME: clouds_yaml_contents,
@@ -470,43 +470,28 @@ async def app_one_runner(model: Model, app_no_runner: Application) -> AsyncItera
470470
return app_no_runner
471471

472472

473-
@pytest_asyncio.fixture(scope="module")
474-
async def app_scheduled_events(
473+
@pytest_asyncio.fixture(scope="module", name="app_scheduled_events")
474+
async def app_scheduled_events_fixture(
475475
model: Model,
476-
charm_file: str,
477-
app_name: str,
478-
path: str,
479-
token: str,
480-
http_proxy: str,
481-
https_proxy: str,
482-
no_proxy: str,
483-
) -> AsyncIterator[Application]:
484-
"""Application with no token.
485-
486-
Test should ensure it returns with the application having one runner.
487-
488-
This fixture has to deploy a new application. The scheduled events are set
489-
to one hour in other application to avoid conflicting with the tests.
490-
Changes to the duration of scheduled interval only takes effect after the
491-
next trigger. Therefore, it would take a hour for the duration change to
492-
take effect.
493-
"""
494-
application = await deploy_github_runner_charm(
495-
model=model,
496-
charm_file=charm_file,
497-
app_name=app_name,
498-
path=path,
499-
token=token,
500-
runner_storage="memory",
501-
http_proxy=http_proxy,
502-
https_proxy=https_proxy,
503-
no_proxy=no_proxy,
504-
reconcile_interval=8,
505-
)
506-
476+
app_openstack_runner,
477+
):
478+
"""Application to check scheduled events."""
479+
application = app_openstack_runner
480+
await application.set_config({"reconcile-interval": "8"})
507481
await application.set_config({VIRTUAL_MACHINES_CONFIG_NAME: "1"})
482+
await model.wait_for_idle(apps=[application.name], status=ACTIVE, timeout=90 * 60)
508483
await reconcile(app=application, model=model)
484+
return application
509485

486+
487+
@pytest_asyncio.fixture(scope="module", name="app_no_wait_tmate")
488+
async def app_no_wait_tmate_fixture(
489+
model: Model,
490+
app_openstack_runner,
491+
):
492+
"""Application to check tmate ssh with openstack without waiting for active."""
493+
application = app_openstack_runner
494+
await application.set_config({"reconcile-interval": "60", VIRTUAL_MACHINES_CONFIG_NAME: "1"})
510495
return application
511496

512497

@@ -569,11 +554,11 @@ async def app_no_wait_fixture(
569554

570555
@pytest_asyncio.fixture(scope="module", name="tmate_ssh_server_app")
571556
async def tmate_ssh_server_app_fixture(
572-
model: Model, app_no_wait: Application
557+
model: Model, app_no_wait_tmate: Application
573558
) -> AsyncIterator[Application]:
574559
"""tmate-ssh-server charm application related to GitHub-Runner app charm."""
575560
tmate_app: Application = await model.deploy("tmate-ssh-server", channel="edge")
576-
await app_no_wait.relate("debug-ssh", f"{tmate_app.name}:debug-ssh")
561+
await app_no_wait_tmate.relate("debug-ssh", f"{tmate_app.name}:debug-ssh")
577562
await model.wait_for_idle(apps=[tmate_app.name], status=ACTIVE, timeout=60 * 30)
578563

579564
return tmate_app

tests/integration/helpers/common.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,39 @@ class InstanceHelper(typing.Protocol):
5555
"""Helper for running commands in instances."""
5656

5757
async def run_in_instance(
58-
self, unit: Unit, command: str, timeout: int | None = None
58+
self,
59+
unit: Unit,
60+
command: str,
61+
timeout: int | None = None,
62+
assert_on_failure: bool = False,
63+
assert_msg: str | None = None,
5964
) -> tuple[int, str | None, str | None]:
6065
"""Run command in instance.
6166
6267
Args:
6368
unit: Juju unit to execute the command in.
6469
command: Command to execute.
6570
timeout: Amount of time to wait for the execution.
71+
assert_on_failure: Perform assertion on non-zero exit code.
72+
assert_msg: Message for the failure assertion.
73+
"""
74+
...
75+
76+
async def expose_to_instance(
77+
self,
78+
unit: Unit,
79+
port: int,
80+
host: str = "localhost",
81+
) -> None:
82+
"""Expose a port on the juju machine to the OpenStack instance.
83+
84+
Uses SSH remote port forwarding from the juju machine to the OpenStack instance containing
85+
the runner.
86+
87+
Args:
88+
unit: The juju unit of the github-runner charm.
89+
port: The port on the juju machine to expose to the runner.
90+
host: Host for the reverse tunnel.
6691
"""
6792
...
6893

@@ -74,6 +99,14 @@ async def ensure_charm_has_runner(self, app: Application):
7499
"""
75100
...
76101

102+
async def get_runner_names(self, unit: Unit) -> list[str]:
103+
"""Get the name of all the runners in the unit.
104+
105+
Args:
106+
unit: The GitHub Runner Charm unit to get the runner names for.
107+
"""
108+
...
109+
77110
async def get_runner_name(self, unit: Unit) -> str:
78111
"""Get the name of the runner.
79112
@@ -82,6 +115,14 @@ async def get_runner_name(self, unit: Unit) -> str:
82115
"""
83116
...
84117

118+
async def delete_single_runner(self, unit: Unit) -> None:
119+
"""Delete the only runner.
120+
121+
Args:
122+
unit: The GitHub Runner Charm unit to delete the runner name for.
123+
"""
124+
...
125+
85126

86127
async def check_runner_binary_exists(unit: Unit) -> bool:
87128
"""Checks if runner binary exists in the charm.

tests/integration/helpers/lxd.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,49 @@ class LXDInstanceHelper(InstanceHelper):
2020
"""Helper class to interact with LXD instances."""
2121

2222
async def run_in_instance(
23-
self, unit: Unit, command: str, timeout: int | None = None
23+
self,
24+
unit: Unit,
25+
command: str,
26+
timeout: int | None = None,
27+
assert_on_failure: bool = False,
28+
assert_msg: str | None = None,
2429
) -> tuple[int, str | None, str | None]:
2530
"""Run command in LXD instance.
2631
2732
Args:
2833
unit: Juju unit to execute the command in.
2934
command: Command to execute.
3035
timeout: Amount of time to wait for the execution.
36+
assert_on_failure: Not used in lxd
37+
assert_msg: Not used in lxd
3138
3239
Returns:
3340
Tuple of return code, stdout and stderr.
3441
"""
3542
name = await self.get_runner_name(unit)
3643
return await run_in_lxd_instance(unit, name, command, timeout=timeout)
3744

45+
async def expose_to_instance(
46+
self,
47+
unit: Unit,
48+
port: int,
49+
host: str = "localhost",
50+
) -> None:
51+
"""Expose a port on the juju machine to the OpenStack instance.
52+
53+
Uses SSH remote port forwarding from the juju machine to the OpenStack instance containing
54+
the runner.
55+
56+
Args:
57+
unit: The juju unit of the github-runner charm.
58+
port: The port on the juju machine to expose to the runner.
59+
host: Host for the reverse tunnel.
60+
61+
Raises:
62+
NotImplementedError: Not implemented yet.
63+
"""
64+
raise NotImplementedError
65+
3866
async def ensure_charm_has_runner(self, app: Application):
3967
"""Reconcile the charm to contain one runner.
4068
@@ -56,6 +84,29 @@ async def get_runner_name(self, unit: Unit) -> str:
5684
"""
5785
return await get_runner_name(unit)
5886

87+
async def get_runner_names(self, unit: Unit) -> list[str]:
88+
"""Get the name of all the runners in the unit.
89+
90+
Args:
91+
unit: The GitHub Runner Charm unit to get the runner names for.
92+
93+
Raises:
94+
NotImplementedError: Not implemented yet.
95+
"""
96+
raise NotImplementedError
97+
98+
async def delete_single_runner(self, unit: Unit) -> None:
99+
"""Delete the only runner.
100+
101+
102+
Args:
103+
unit: The GitHub Runner Charm unit to check.
104+
105+
Raises:
106+
NotImplementedError: Not implemented yet.
107+
"""
108+
raise NotImplementedError
109+
59110

60111
async def assert_resource_lxd_profile(unit: Unit, configs: dict[str, Any]) -> None:
61112
"""Check for LXD profile of the matching resource config.

tests/integration/helpers/openstack.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import secrets
55
from asyncio import sleep
6-
from typing import Optional, TypedDict, cast
6+
from typing import Optional, TypedDict
77

88
import openstack.connection
99
from juju.application import Application
@@ -32,6 +32,7 @@ async def expose_to_instance(
3232
self,
3333
unit: Unit,
3434
port: int,
35+
host: str = "localhost",
3536
) -> None:
3637
"""Expose a port on the juju machine to the OpenStack instance.
3738
@@ -41,6 +42,7 @@ async def expose_to_instance(
4142
Args:
4243
unit: The juju unit of the github-runner charm.
4344
port: The port on the juju machine to expose to the runner.
45+
host: Host for the reverse tunnel.
4446
"""
4547
runner = self._get_single_runner(unit=unit)
4648
assert runner, f"Runner not found for unit {unit.name}"
@@ -61,7 +63,7 @@ async def expose_to_instance(
6163
key_path = f"/home/{RUNNER_MANAGER_USER}/.ssh/{runner.name}.key"
6264
exit_code, _, _ = await run_in_unit(unit, f"ls {key_path}")
6365
assert exit_code == 0, f"Unable to find key file {key_path}"
64-
ssh_cmd = f'ssh -fNT -R {port}:localhost:{port} -i {key_path} -o "StrictHostKeyChecking no" -o "ControlPersist yes" ubuntu@{ip} &'
66+
ssh_cmd = f'ssh -fNT -R {port}:{host}:{port} -i {key_path} -o "StrictHostKeyChecking no" -o "ControlPersist yes" ubuntu@{ip} &'
6567
exit_code, _, stderr = await run_in_unit(unit, ssh_cmd)
6668
assert (
6769
exit_code == 0
@@ -150,6 +152,18 @@ async def _set_app_runner_amount(app: Application, num_runners: int) -> None:
150152
await app.set_config({VIRTUAL_MACHINES_CONFIG_NAME: f"{num_runners}"})
151153
await reconcile(app=app, model=app.model)
152154

155+
async def get_runner_names(self, unit: Unit) -> list[str]:
156+
"""Get the name of all the runners in the unit.
157+
158+
Args:
159+
unit: The GitHub Runner Charm unit to get the runner names for.
160+
161+
Returns:
162+
List of names for the runners.
163+
"""
164+
runners = self._get_runners(unit)
165+
return [runner.name for runner in runners]
166+
153167
async def get_runner_name(self, unit: Unit) -> str:
154168
"""Get the name of the runner.
155169
@@ -161,24 +175,27 @@ async def get_runner_name(self, unit: Unit) -> str:
161175
Returns:
162176
The Github runner name deployed in the given unit.
163177
"""
164-
runners = await self._get_runner_names(unit)
178+
runners = self._get_runners(unit)
165179
assert len(runners) == 1
166-
return runners[0]
180+
return runners[0].name
167181

168-
async def _get_runner_names(self, unit: Unit) -> tuple[str, ...]:
169-
"""Get names of the runners in LXD.
182+
async def delete_single_runner(self, unit: Unit) -> None:
183+
"""Delete the only runner.
170184
171185
Args:
172-
unit: Unit instance to check for the LXD profile.
173-
174-
Returns:
175-
Tuple of runner names.
186+
unit: The GitHub Runner Charm unit to delete the runner name for.
176187
"""
177188
runner = self._get_single_runner(unit)
178-
assert runner, "Failed to find runner server"
179-
return (cast(str, runner.name),)
189+
self.openstack_connection.delete_server(name_or_id=runner.id)
190+
191+
def _get_runners(self, unit: Unit) -> list[Server]:
192+
"""Get all runners for the unit."""
193+
servers: list[Server] = self.openstack_connection.list_servers()
194+
unit_name_without_slash = unit.name.replace("/", "-")
195+
runners = [server for server in servers if server.name.startswith(unit_name_without_slash)]
196+
return runners
180197

181-
def _get_single_runner(self, unit: Unit) -> Server | None:
198+
def _get_single_runner(self, unit: Unit) -> Server:
182199
"""Get the only runner for the unit.
183200
184201
This method asserts for exactly one runner for the unit.
@@ -189,9 +206,7 @@ def _get_single_runner(self, unit: Unit) -> Server | None:
189206
Returns:
190207
The runner server.
191208
"""
192-
servers: list[Server] = self.openstack_connection.list_servers()
193-
unit_name_without_slash = unit.name.replace("/", "-")
194-
runners = [server for server in servers if server.name.startswith(unit_name_without_slash)]
209+
runners = self._get_runners(unit)
195210
assert (
196211
len(runners) == 1
197212
), f"In {unit.name} found more than one runners or no runners: {runners}"

0 commit comments

Comments
 (0)