Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: ssh-debug integration test #193

Merged
merged 44 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
144fb45
feat: initial ssh-debug integration
yanksyoon Dec 20, 2023
1c273f0
fix: linting and test fixes
yanksyoon Dec 20, 2023
c79969e
test: add debug info tests
yanksyoon Dec 20, 2023
ce95067
fix: relation data & tests
yanksyoon Dec 27, 2023
41d391b
test: fix unit test
yanksyoon Dec 29, 2023
b61cf74
Merge branch 'main' into feat/ssh-integration
yanksyoon Jan 4, 2024
f479b2e
Merge branch 'main' into feat/ssh-integration
yanksyoon Jan 5, 2024
950909f
feat: flush runners on relation changed
yanksyoon Jan 5, 2024
6e4cafd
Merge branch 'feat/ssh-integration' of https://github.com/canonical/g…
yanksyoon Jan 5, 2024
9301231
fix: lint
yanksyoon Jan 5, 2024
ffa7348
Merge branch 'main' into feat/ssh-integration
yanksyoon Jan 11, 2024
2317203
Merge branch 'main' into feat/ssh-integration
yanksyoon Jan 15, 2024
6993b5f
test: validate SHA fingerprint format
yanksyoon Jan 15, 2024
93c2195
fix: flush only idle runners
yanksyoon Jan 15, 2024
a20e709
fix: remove remote unit filter
yanksyoon Jan 15, 2024
612e84d
fix: use underscore for env vars
yanksyoon Jan 15, 2024
1f734d1
Merge branch 'main' into feat/ssh-integration
yanksyoon Jan 15, 2024
32d313d
Merge branch 'main' into feat/ssh-integration
yanksyoon Jan 16, 2024
c242ca2
Merge branch 'main' of https://github.com/canonical/github-runner-ope…
yanksyoon Jan 16, 2024
ede0da3
test: tmate action test
yanksyoon Jan 16, 2024
9a65604
test: tmate action test
yanksyoon Jan 16, 2024
5e289ad
test: add ssh_debug test to workflow
yanksyoon Jan 16, 2024
2876419
test: rename relation
yanksyoon Jan 16, 2024
11fe9a1
fix: json serializable state
yanksyoon Jan 16, 2024
b3f9af6
fix: optional state serialization
yanksyoon Jan 16, 2024
b2bcf28
fix: use machine address
yanksyoon Jan 16, 2024
e28f3aa
fix: test branch naming
yanksyoon Jan 17, 2024
079a5b8
Merge branch 'main' into test/ssh-integration
yanksyoon Jan 17, 2024
aa929bc
Merge branch 'test/ssh-integration' of https://github.com/canonical/g…
yanksyoon Jan 17, 2024
85a2089
Merge branch 'main' into test/ssh-integration
yanksyoon Jan 17, 2024
88812d7
test: fix iterator error
yanksyoon Jan 17, 2024
e7572a3
Merge branch 'test/ssh-integration' of https://github.com/canonical/g…
yanksyoon Jan 17, 2024
6d99d8d
test: trigger w/ longer timeout
yanksyoon Jan 17, 2024
0e07706
test: trigger
yanksyoon Jan 18, 2024
e430336
Merge branch 'main' of https://github.com/canonical/github-runner-ope…
yanksyoon Jan 18, 2024
e5972c8
chore: move test dependencies
yanksyoon Jan 18, 2024
ed7098e
test: refactor tests
yanksyoon Jan 18, 2024
6fa625d
test: refactor tests
yanksyoon Jan 19, 2024
157f32c
fix: lint
yanksyoon Jan 19, 2024
c4e3d40
fix: lint
yanksyoon Jan 19, 2024
cb0b32b
test: move imports helpers - charm_metrics_helpers - modules
yanksyoon Jan 19, 2024
52b6618
Update tests/integration/helpers.py
yanksyoon Jan 19, 2024
cc9a671
test: lint fix
yanksyoon Jan 19, 2024
eb4d6db
Merge branch 'main' into test/ssh-integration
yanksyoon Jan 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 55 additions & 16 deletions tests/integration/charm_metrics_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,23 @@

import json
import logging
import typing
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 github.WorkflowJob import WorkflowJob
from github.WorkflowRun import WorkflowRun
from juju.application import Application
from juju.unit import Unit

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 get_runner_name, get_runner_names, run_in_unit

TEST_WORKFLOW_NAMES = [
"Workflow Dispatch Tests",
Expand All @@ -51,6 +48,39 @@ async def _wait_until_runner_is_used_up(runner_name: str, unit: Unit):
assert False, "Timeout while waiting for the runner to be used up"


def _get_job_logs(job: WorkflowJob) -> str:
"""Retrieve a workflow's job logs.

Args:
job: The target job to fetch the logs from.

Returns:
The job logs.
"""
logs_url = job.logs_url()
logs = requests.get(logs_url).content.decode("utf-8")
return logs


def get_workflow_runs(
start_time: datetime, workflow: Workflow, runner_name: str, branch: Branch = None
) -> typing.Generator[WorkflowRun, None, None]:
"""Fetch the latest matching runs of a workflow for a given runner.

Args:
start_time: The start time of the workflow.
workflow: The target workflow to get the run for.
runner_name: The runner name the workflow job is assigned to.
branch: The branch the workflow is run on.
"""
for run in workflow.get_runs(created=f">={start_time.isoformat()}", branch=branch):
latest_job: WorkflowJob = run.jobs()[0]
logs = _get_job_logs(job=latest_job)

if JOB_LOG_START_MSG_TEMPLATE.format(runner_name=runner_name) in logs:
yield run


async def _assert_workflow_run_conclusion(
runner_name: str, conclusion: str, workflow: Workflow, start_time: datetime
):
Expand All @@ -63,11 +93,14 @@ async def _assert_workflow_run_conclusion(
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")
latest_job: WorkflowJob = run.jobs()[0]
logs = _get_job_logs(job=latest_job)

if JOB_LOG_START_MSG_TEMPLATE.format(runner_name=runner_name) in logs:
assert run.jobs()[0].conclusion == conclusion
assert latest_job.conclusion == conclusion, (
f"Job {latest_job.name} for {runner_name} expected {conclusion}, "
f"got {latest_job.conclusion}"
)


async def _wait_for_workflow_to_complete(
Expand Down Expand Up @@ -190,7 +223,11 @@ async def _cancel_workflow_run(unit: Unit, workflow: Workflow):


async def dispatch_workflow(
app: Application, branch: Branch, github_repository: Repository, conclusion: str
app: Application,
branch: Branch,
github_repository: Repository,
conclusion: str,
workflow_id_or_name: str,
):
"""Dispatch a workflow on a branch for the runner to run.

Expand All @@ -201,20 +238,22 @@ async def dispatch_workflow(
branch: The branch to dispatch the workflow on.
github_repository: The github repository to dispatch the workflow on.
conclusion: The expected workflow run conclusion.
workflow_id_or_name: The workflow filename in .github/workflows in main branch to run or
its id.

Returns:
A completed workflow.
"""
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
)
workflow = github_repository.get_workflow(id_or_file_name=workflow_id_or_name)

# 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
)
return workflow


async def assert_events_after_reconciliation(
Expand Down
17 changes: 12 additions & 5 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ async def app_runner(
return application


@pytest_asyncio.fixture(scope="module", name="tmate_ssh_server_app")
async def tmate_ssh_server_app_fixture(
@pytest_asyncio.fixture(scope="module", name="app_no_wait")
async def app_no_wait_fixture(
model: Model,
charm_file: str,
app_name: str,
Expand All @@ -260,8 +260,7 @@ async def tmate_ssh_server_app_fixture(
https_proxy: str,
no_proxy: str,
) -> AsyncIterator[Application]:
"""tmate-ssh-server charm application related to GitHub-Runner app charm."""
# 5 min reconcile period to register flushed runners after relation data changed event.
"""GitHub runner charm application without waiting for active."""
app: Application = await deploy_github_runner_charm(
model=model,
charm_file=charm_file,
Expand All @@ -276,8 +275,16 @@ async def tmate_ssh_server_app_fixture(
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.relate("debug-ssh", f"{tmate_app.name}:debug-ssh")
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
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
GitPython>3,<4
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved
pyyaml
pygithub
2 changes: 2 additions & 0 deletions tests/integration/test_charm_metrics_failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
)
from tests.integration.helpers import (
DISPATCH_CRASH_TEST_WORKFLOW_FILENAME,
DISPATCH_FAILURE_TEST_WORKFLOW_FILENAME,
ensure_charm_has_runner,
get_runner_name,
reconcile,
Expand Down Expand Up @@ -71,6 +72,7 @@ async def test_charm_issues_metrics_for_failed_repo_policy(
branch=forked_github_branch,
github_repository=forked_github_repository,
conclusion="failure",
workflow_id_or_name=DISPATCH_FAILURE_TEST_WORKFLOW_FILENAME,
)

# Set the number of virtual machines to 0 to speedup reconciliation
Expand Down
3 changes: 3 additions & 0 deletions tests/integration/test_charm_metrics_success.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
print_loop_device_info,
)
from tests.integration.helpers import (
DISPATCH_TEST_WORKFLOW_FILENAME,
ensure_charm_has_runner,
get_runner_name,
reconcile,
Expand Down Expand Up @@ -91,6 +92,7 @@ async def test_charm_issues_metrics_after_reconciliation(
branch=forked_github_branch,
github_repository=forked_github_repository,
conclusion="success",
workflow_id_or_name=DISPATCH_TEST_WORKFLOW_FILENAME,
)

# Set the number of virtual machines to 0 to speedup reconciliation
Expand Down Expand Up @@ -127,6 +129,7 @@ async def test_charm_remounts_shared_fs(
branch=forked_github_branch,
github_repository=forked_github_repository,
conclusion="success",
workflow_id_or_name=DISPATCH_TEST_WORKFLOW_FILENAME,
)

# unmount shared fs
Expand Down
83 changes: 29 additions & 54 deletions tests/integration/test_debug_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,28 @@

"""Integration tests for github-runner charm with ssh-debug integration."""
import logging
import typing
import zipfile
from io import BytesIO
from datetime import datetime, timezone

import requests
from github.Branch import Branch
from github.Repository import Repository
from github.Workflow import Workflow
from github.WorkflowRun import WorkflowRun
from juju.application import Application

from tests.integration.helpers import wait_for
from tests.integration.charm_metrics_helpers import (
_get_job_logs,
dispatch_workflow,
get_workflow_runs,
)
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved

logger = logging.getLogger(__name__)

SSH_DEBUG_WORKFLOW_FILE_NAME = "workflow_dispatch_ssh_debug.yaml"


async def test_ssh_debug(
app_no_wait: Application,
github_repository: Repository,
test_github_branch: Branch,
app_name: str,
token: str,
tmate_ssh_server_unit_ip: str,
):
"""
Expand All @@ -32,54 +34,27 @@ async def test_ssh_debug(
"""
# trigger tmate action
logger.info("Dispatching workflow_dispatch_ssh_debug.yaml workflow.")
workflow: Workflow = github_repository.get_workflow("workflow_dispatch_ssh_debug.yaml")
assert workflow.create_dispatch(
test_github_branch, inputs={"runner": app_name}
), "Failed to dispatch workflow"

# get action logs
def latest_workflow_run() -> typing.Optional[WorkflowRun]:
"""Get latest workflow run."""
try:
logger.info("Fetching latest workflow run on branch %s.", test_github_branch.name)
# The test branch is unique per test, hence there can only be one run per branch.
last_run: WorkflowRun = workflow.get_runs(branch=test_github_branch)[0]
except IndexError:
return None
return last_run

await wait_for(latest_workflow_run, timeout=60 * 10, check_interval=60)
lastest_run = typing.cast(WorkflowRun, latest_workflow_run())

def is_workflow_complete():
"""Return if the workflow is complete."""
lastest_run.update()
logger.info("Fetched latest workflow status %s.", lastest_run.status)
return lastest_run.status == "completed"

await wait_for(is_workflow_complete, timeout=60 * 45, check_interval=60)

response = requests.get(
lastest_run.logs_url,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
},
start_time = datetime.now(timezone.utc)

# expect failure since the ssh workflow will timeout
workflow = await dispatch_workflow(
app=app_no_wait,
branch=test_github_branch,
github_repository=github_repository,
conclusion="failure",
workflow_id_or_name=SSH_DEBUG_WORKFLOW_FILE_NAME,
)
zip_data = BytesIO(response.content)
with zipfile.ZipFile(zip_data, "r") as zip_ref:
logger.info("Files: %s", zip_ref.namelist())
tmate_log_filename = next(
iter(
[
name
for name in zip_ref.namelist()
if "workflow-dispatch-tests/3_Setup tmate session.txt" == name
]
)

latest_run: WorkflowRun = next(
get_workflow_runs(
start_time=start_time,
workflow=workflow,
runner_name=app_no_wait.name,
branch=test_github_branch,
)
logs = str(zip_ref.read(tmate_log_filename), encoding="utf-8")
)

logs = _get_job_logs(latest_run.jobs("latest")[0])

# ensure ssh connection info printed in logs.
logger.info("Logs: %s", logs)
Expand Down
2 changes: 0 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ deps =
juju2.9: juju==2.9.*
pytest-operator
pytest-asyncio
pyyaml
pygithub
# Type error problem with newer version of macaroonbakery
macaroonbakery==1.3.2
-r{toxinidir}/requirements.txt
Expand Down
Loading