Skip to content

Commit

Permalink
test: ssh-debug integration test (#193)
Browse files Browse the repository at this point in the history
* feat: initial ssh-debug integration

* fix: linting and test fixes

* test: add debug info tests

* fix: relation data & tests

* test: fix unit test

* feat: flush runners on relation changed

* fix: lint

* test: validate SHA fingerprint format

* fix: flush only idle runners

* fix: remove remote unit filter

* fix: use underscore for env vars

* test: tmate action test

* test: tmate action test

* test: add ssh_debug test to workflow

* test: rename relation

* fix: json serializable state

* fix: optional state serialization

* fix: use machine address

* fix: test branch naming

* test: fix iterator error

* test: trigger w/ longer timeout

* test: trigger

* chore: move test dependencies

* test: refactor tests

* test: refactor tests

* fix: lint

* fix: lint

* test: move imports helpers - charm_metrics_helpers - modules

* Update tests/integration/helpers.py

Co-authored-by: Christopher Bartz <[email protected]>

* test: lint fix

---------

Co-authored-by: Christopher Bartz <[email protected]>
  • Loading branch information
yanksyoon and cbartz authored Jan 19, 2024
1 parent 446ba8e commit e6b935c
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 102 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
pre-run-script: scripts/pre-integration-test.sh
provider: lxd
test-tox-env: integration-juju2.9
modules: '["test_charm_fork_repo", "test_charm_no_runner", "test_charm_scheduled_events", "test_charm_one_runner", "test_charm_metrics_success", "test_charm_metrics_failure", "test_self_hosted_runner", "test_charm_with_proxy", "test_charm_with_juju_storage"]'
modules: '["test_charm_fork_repo", "test_charm_no_runner", "test_charm_scheduled_events", "test_charm_one_runner", "test_charm_metrics_success", "test_charm_metrics_failure", "test_self_hosted_runner", "test_charm_with_proxy", "test_charm_with_juju_storage", "test_debug_ssh"]'
integration-tests-juju3:
name: Integration test with juju 3.1
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
Expand All @@ -23,4 +23,4 @@ jobs:
pre-run-script: scripts/pre-integration-test.sh
provider: lxd
test-tox-env: integration-juju3.1
modules: '["test_charm_fork_repo", "test_charm_no_runner", "test_charm_scheduled_events", "test_charm_one_runner", "test_charm_metrics_success", "test_charm_metrics_failure", "test_self_hosted_runner", "test_charm_with_proxy", "test_charm_with_juju_storage"]'
modules: '["test_charm_fork_repo", "test_charm_no_runner", "test_charm_scheduled_events", "test_charm_one_runner", "test_charm_metrics_success", "test_charm_metrics_failure", "test_self_hosted_runner", "test_charm_with_proxy", "test_charm_with_juju_storage", "test_debug_ssh"]'
1 change: 1 addition & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,7 @@ def _on_debug_ssh_relation_changed(self, _: ops.RelationChangedEvent) -> None:
"""Handle debug ssh relation changed event."""
runner_manager = self._get_runner_manager()
runner_manager.flush(flush_busy=False)
self._reconcile_runners(runner_manager)


if __name__ == "__main__":
Expand Down
3 changes: 3 additions & 0 deletions src/charm_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ def from_charm(cls, charm: CharmBase) -> "State":
# Convert pydantic object to python object serializable by json module.
state_dict["proxy_config"] = json.loads(state_dict["proxy_config"].json())
state_dict["charm_config"] = json.loads(state_dict["charm_config"].json())
state_dict["ssh_debug_info"] = (
json.loads(state_dict["ssh_debug_info"].json()) if ssh_debug_info else None
)
json_data = json.dumps(state_dict, ensure_ascii=False)
CHARM_STATE_PATH.write_text(json_data, encoding="utf-8")

Expand Down
95 changes: 1 addition & 94 deletions tests/integration/charm_metrics_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@

import json
import logging
from datetime import datetime, timezone
from time import sleep

import requests
from github.Branch import Branch
from github.Repository import Repository
from github.Workflow import Workflow
from juju.application import Application
Expand All @@ -19,76 +17,13 @@
from github_type import JobConclusion
from metrics import METRICS_LOG_PATH
from runner_metrics import PostJobStatus
from tests.integration.helpers import (
DISPATCH_FAILURE_TEST_WORKFLOW_FILENAME,
DISPATCH_TEST_WORKFLOW_FILENAME,
get_runner_name,
get_runner_names,
run_in_unit,
)
from tests.integration.helpers import JOB_LOG_START_MSG_TEMPLATE, get_runner_name, run_in_unit

TEST_WORKFLOW_NAMES = [
"Workflow Dispatch Tests",
"Workflow Dispatch Crash Tests",
"Workflow Dispatch Failure Tests 2a34f8b1-41e4-4bcb-9bbf-7a74e6c482f7",
]
JOB_LOG_START_MSG_TEMPLATE = "Job is about to start running on the runner: {runner_name}"


async def _wait_until_runner_is_used_up(runner_name: str, unit: Unit):
"""Wait until the runner is used up.
Args:
runner_name: The runner name to wait for.
unit: The unit which contains the runner.
"""
for _ in range(30):
runners = await get_runner_names(unit)
if runner_name not in runners:
break
sleep(30)
else:
assert False, "Timeout while waiting for the runner to be used up"


async def _assert_workflow_run_conclusion(
runner_name: str, conclusion: str, workflow: Workflow, start_time: datetime
):
"""Assert that the workflow run has the expected conclusion.
Args:
runner_name: The runner name to assert the workflow run conclusion for.
conclusion: The expected workflow run conclusion.
workflow: The workflow to assert the workflow run conclusion for.
start_time: The start time of the workflow.
"""
for run in workflow.get_runs(created=f">={start_time.isoformat()}"):
logs_url = run.jobs()[0].logs_url()
logs = requests.get(logs_url).content.decode("utf-8")

if JOB_LOG_START_MSG_TEMPLATE.format(runner_name=runner_name) in logs:
assert run.jobs()[0].conclusion == conclusion


async def _wait_for_workflow_to_complete(
unit: Unit, workflow: Workflow, conclusion: str, start_time: datetime
):
"""Wait for the workflow to complete.
Args:
unit: The unit which contains the runner.
workflow: The workflow to wait for.
conclusion: The workflow conclusion to wait for.
start_time: The start time of the workflow.
"""
runner_name = await get_runner_name(unit)
await _wait_until_runner_is_used_up(runner_name, unit)
# Wait for the workflow log to contain the conclusion
sleep(60)

await _assert_workflow_run_conclusion(
runner_name=runner_name, conclusion=conclusion, workflow=workflow, start_time=start_time
)


async def _wait_for_workflow_to_start(unit: Unit, workflow: Workflow):
Expand Down Expand Up @@ -189,34 +124,6 @@ async def _cancel_workflow_run(unit: Unit, workflow: Workflow):
run.cancel()


async def dispatch_workflow(
app: Application, branch: Branch, github_repository: Repository, conclusion: str
):
"""Dispatch a workflow on a branch for the runner to run.
The function assumes that there is only one runner running in the unit.
Args:
app: The charm to dispatch the workflow for.
branch: The branch to dispatch the workflow on.
github_repository: The github repository to dispatch the workflow on.
conclusion: The expected workflow run conclusion.
"""
start_time = datetime.now(timezone.utc)

workflow = github_repository.get_workflow(id_or_file_name=DISPATCH_TEST_WORKFLOW_FILENAME)
if conclusion == "failure":
workflow = github_repository.get_workflow(
id_or_file_name=DISPATCH_FAILURE_TEST_WORKFLOW_FILENAME
)

# The `create_dispatch` returns True on success.
assert workflow.create_dispatch(branch, {"runner": app.name})
await _wait_for_workflow_to_complete(
unit=app.units[0], workflow=workflow, conclusion=conclusion, start_time=start_time
)


async def assert_events_after_reconciliation(
app: Application, github_repository: Repository, post_job_status: PostJobStatus
):
Expand Down
86 changes: 86 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
import pytest
import pytest_asyncio
import yaml
from git import Repo
from github import Github, GithubException
from github.Branch import Branch
from github.Repository import Repository
from juju.application import Application
from juju.client._definitions import FullStatus, UnitStatus
from juju.model import Model
from pytest_operator.plugin import OpsTest

from tests.integration.helpers import (
deploy_github_runner_charm,
ensure_charm_has_runner,
reconcile,
wait_for,
)
from tests.status_name import ACTIVE

Expand Down Expand Up @@ -246,6 +249,64 @@ async def app_runner(
return application


@pytest_asyncio.fixture(scope="module", name="app_no_wait")
async def app_no_wait_fixture(
model: Model,
charm_file: str,
app_name: str,
path: str,
token: str,
http_proxy: str,
https_proxy: str,
no_proxy: str,
) -> AsyncIterator[Application]:
"""GitHub runner charm application without waiting for active."""
app: Application = await deploy_github_runner_charm(
model=model,
charm_file=charm_file,
app_name=app_name,
path=path,
token=token,
runner_storage="memory",
http_proxy=http_proxy,
https_proxy=https_proxy,
no_proxy=no_proxy,
reconcile_interval=60,
wait_idle=False,
)
await app.set_config({"virtual-machines": "1"})
return app


@pytest_asyncio.fixture(scope="module", name="tmate_ssh_server_app")
async def tmate_ssh_server_app_fixture(
model: Model, app_no_wait: Application
) -> AsyncIterator[Application]:
"""tmate-ssh-server charm application related to GitHub-Runner app charm."""
tmate_app: Application = await model.deploy("tmate-ssh-server", channel="edge")
await app_no_wait.relate("debug-ssh", f"{tmate_app.name}:debug-ssh")
await model.wait_for_idle(status=ACTIVE, timeout=60 * 30)

return tmate_app


@pytest_asyncio.fixture(scope="module", name="tmate_ssh_server_unit_ip")
async def tmate_ssh_server_unit_ip_fixture(
model: Model,
tmate_ssh_server_app: Application,
) -> AsyncIterator[str]:
"""tmate-ssh-server charm unit ip."""
status: FullStatus = await model.get_status([tmate_ssh_server_app.name])
try:
unit_status: UnitStatus = next(
iter(status.applications[tmate_ssh_server_app.name].units.values())
)
assert unit_status.public_address, "Invalid unit address"
return unit_status.public_address
except StopIteration as exc:
raise StopIteration("Invalid unit status") from exc


@pytest.fixture(scope="module")
def github_client(token: str) -> Github:
"""Returns the github client."""
Expand Down Expand Up @@ -357,6 +418,31 @@ async def app_juju_storage(
return application


@pytest_asyncio.fixture(scope="module", name="test_github_branch")
async def test_github_branch_fixture(github_repository: Repository) -> AsyncIterator[Branch]:
"""Create a new branch for testing, from latest commit in current branch."""
test_branch = f"test-{secrets.token_hex(4)}"
branch_ref = github_repository.create_git_ref(
ref=f"refs/heads/{test_branch}", sha=Repo().head.commit.hexsha
)

def get_branch():
"""Get newly created branch."""
try:
branch = github_repository.get_branch(test_branch)
except GithubException as err:
if err.status == 404:
return False
raise
return branch

await wait_for(get_branch)

yield get_branch()

branch_ref.delete()


@pytest_asyncio.fixture(scope="module", name="app_with_grafana_agent")
async def app_with_grafana_agent_integrated_fixture(
model: Model, app_no_runner: Application
Expand Down
Loading

0 comments on commit e6b935c

Please sign in to comment.