Skip to content

Commit

Permalink
Merge ed7098e into 0c28442
Browse files Browse the repository at this point in the history
  • Loading branch information
yanksyoon authored Jan 18, 2024
2 parents 0c28442 + ed7098e commit 430a77c
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 11 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
54 changes: 49 additions & 5 deletions tests/integration/charm_metrics_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@

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

Expand Down Expand Up @@ -51,6 +54,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 +99,11 @@ 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


async def _wait_for_workflow_to_complete(
Expand Down Expand Up @@ -190,7 +226,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_TEST_WORKFLOW_FILENAME,
):
"""Dispatch a workflow on a branch for the runner to run.
Expand All @@ -201,10 +241,13 @@ 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.
Returns:
A completed workflow.
"""
start_time = datetime.now(timezone.utc)

workflow = github_repository.get_workflow(id_or_file_name=DISPATCH_TEST_WORKFLOW_FILENAME)
workflow = github_repository.get_workflow(id_or_file_name=workflow_id_or_name)
if conclusion == "failure":
workflow = github_repository.get_workflow(
id_or_file_name=DISPATCH_FAILURE_TEST_WORKFLOW_FILENAME
Expand All @@ -215,6 +258,7 @@ async def dispatch_workflow(
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
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
49 changes: 47 additions & 2 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

"""Utilities for integration test."""

import inspect
import json
import subprocess
import time
from asyncio import sleep
from typing import Any
from typing import Any, Awaitable, Callable, Union

import juju.version
import yaml
Expand Down Expand Up @@ -305,6 +307,7 @@ async def deploy_github_runner_charm(
https_proxy: str,
no_proxy: str,
reconcile_interval: int,
wait_idle: bool = True,
) -> Application:
"""Deploy github-runner charm.
Expand All @@ -318,6 +321,7 @@ async def deploy_github_runner_charm(
https_proxy: HTTPS proxy for the application to use.
no_proxy: No proxy configuration for the application.
reconcile_interval: Time between reconcile for the application.
wait_idle: wait for model to become idle.
"""
subprocess.run(["sudo", "modprobe", "br_netfilter"])

Expand Down Expand Up @@ -351,5 +355,46 @@ async def deploy_github_runner_charm(
storage=storage,
)

await model.wait_for_idle(status=ACTIVE, timeout=60 * 30)
if wait_idle:
await model.wait_for_idle(status=ACTIVE, timeout=60 * 30)

return application


async def wait_for(
func: Callable[[], Union[Awaitable, Any]],
timeout: int = 300,
check_interval: int = 10,
) -> Any:
"""Wait for function execution to become truthy.
Args:
func: A callback function to wait to return a truthy value.
timeout: Time in seconds to wait for function result to become truthy.
check_interval: Time in seconds to wait between ready checks.
Raises:
TimeoutError: if the callback function did not return a truthy value within timeout.
Returns:
The result of the function if any.
"""
deadline = time.time() + timeout
is_awaitable = inspect.iscoroutinefunction(func)
while time.time() < deadline:
if is_awaitable:
if result := await func():
return result
else:
if result := func():
return result
time.sleep(check_interval)

# final check before raising TimeoutError.
if is_awaitable:
if result := await func():
return result
else:
if result := func():
return result
raise TimeoutError()
3 changes: 3 additions & 0 deletions tests/integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GitPython>3,<4
pyyaml
pygithub
Loading

0 comments on commit 430a77c

Please sign in to comment.