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

docs: ssh debug integration docs #195

Merged
merged 39 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
39 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
c59e89c
docs: ssh-debug integration docs
yanksyoon Jan 17, 2024
50f7564
docs: add docstring to testing option values
yanksyoon Jan 17, 2024
3f4c16e
docs: integration test workflow value comment
yanksyoon Jan 17, 2024
4e605d2
Merge branch 'main' of https://github.com/canonical/github-runner-ope…
yanksyoon Jan 19, 2024
9af8ead
Merge branch 'main' into docs/ssh-debug-integration
yanksyoon Jan 19, 2024
8032108
docs: update master to main
yanksyoon Jan 20, 2024
cc18b0f
docs: improve wording
yanksyoon Jan 22, 2024
d061298
docs: parser help messages
yanksyoon Jan 22, 2024
8044dc7
Update tests/conftest.py
yanksyoon Jan 22, 2024
5c68588
Merge branch 'main' into docs/ssh-debug-integration
yanksyoon Jan 22, 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
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", "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", "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", "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", "test_self_hosted_runner", "test_charm_with_proxy", "test_charm_with_juju_storage", "test_debug_ssh"]'
17 changes: 17 additions & 0 deletions .github/workflows/workflow_dispatch_ssh_debug.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Workflow Dispatch Tests (ssh-debug/tmate)

on:
# Manually dispatched workflow action
workflow_dispatch:
inputs:
runner:
description: 'Self hosted gh runner'
required: true

jobs:
workflow-dispatch-tests:
runs-on: [self-hosted, linux, x64, "${{ inputs.runner }}"]
steps:
- name: Setup tmate session
uses: canonical/action-tmate@chore/env_var_change
timeout-minutes: 1
9 changes: 9 additions & 0 deletions docs/explanation/ssh-debug.md
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SSH Debug

To enhance the security of the runner and the infrastructure behind the runner, only user ssh-keys
registered on [Authorized Keys](https://github.com/tmate-io/tmate-ssh-server/pull/93) are allowed
by default on [tmate-ssh-server charm](https://charmhub.io/tmate-ssh-server/).

Authorized keys are registered via [action-tmate](https://github.com/canonical/action-tmate/)'s
`limit-access-to-actor` feature. This feature uses GitHub users's SSH key to launch an instance
of tmate session with `-a` option, which adds the user's SSH key to `~/.ssh/authorized_keys`.
56 changes: 56 additions & 0 deletions docs/how-to/debug-with-ssh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# How to debug with ssh

The charm exposes an integration `debug-ssh` interface which can be used with
[tmate-ssh-server charm](https://charmhub.io/tmate-ssh-server/) to pre-configure runners with
environment variables to be picked up by [action-tmate](https://github.com/canonical/action-tmate/)
for automatic configuration.

## Prerequisites

To enhance the security of self-hosted runners and its infrastracture, only authorized connections
can be established. Hence, action-tmate users must have
[ssh-key registered](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account)
on the GitHub account.

For users using Canonical's self-hosted runners, access to tmate-ssh-server is provided only within
Canonical VPN.
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved

## Deploying

Use the following command to deploy and integrate github-runner with tmate-ssh-server.

```shell
juju deploy tmate-ssh-server
juju integrate tmate-ssh-server github-runner
```

Idle runners will be flushed and restarted. Busy runners will be configured automatically on next
spawn.

## Using the action

Create a workflow that looks like the following within your workflow to enable action-tmate.

```yaml
name: SSH Debug workflow example

on: [pull_request]

jobs:
build:
runs-on: [self-hosted]
steps:
- uses: actions/checkout@v3
- name: Setup tmate session
uses: canonical/action-tmate@master
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved
```

The output of the action looks like the following.

```
<workflow setup logs redacted>
SSH: ssh -p 10022 <token>@<ip>
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved
or: ssh -i <path-to-private-SSH-key> -p10022 <token>@<ip>
```

Read more about [action-tmate's usage here](https://github.com/canonical/action-tmate).
11 changes: 11 additions & 0 deletions docs/reference/integrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Integrations

### debug-ssh

_Interface_: debug-ssh
_Supported charms_: [tmate-ssh-server](https://charmhub.io/tmate-ssh-server)

Debug-ssh integration provides necessary information for runners to provide ssh reverse-proxy
applications to setup inside the runner.

Example debug-ssh integrate command: `juju integrate github-runner tmate-ssh-server`
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
57 changes: 57 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,
)


Expand Down Expand Up @@ -245,6 +248,35 @@ async def app_runner(
return application


@pytest_asyncio.fixture(scope="module", name="tmate_ssh_server_app")
async def tmate_ssh_server_app_fixture(
model: Model, app: 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 model.wait_for_idle(raise_on_error=False, 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 @@ -354,3 +386,28 @@ async def app_juju_storage(
reconcile_interval=60,
)
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()
43 changes: 42 additions & 1 deletion 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 @@ -353,3 +355,42 @@ async def deploy_github_runner_charm(

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()
1 change: 1 addition & 0 deletions tests/integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GitPython>3,<4
74 changes: 74 additions & 0 deletions tests/integration/test_debug_ssh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

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

import requests
from github.Branch import Branch
from github.Repository import Repository
from github.Workflow import Workflow
from github.WorkflowRun import WorkflowRun

from tests.integration.helpers import wait_for


async def test_ssh_debug(
github_repository: Repository,
test_github_branch: Branch,
app_name: str,
token: str,
tmate_ssh_server_unit_ip: str,
):
"""
arrange: given an integrated GitHub-Runner charm and tmate-ssh-server charm.
act: when canonical/action-tmate is triggered.
assert: the ssh connection info from action-log and tmate-ssh-server matches.
"""
# trigger tmate action
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:
last_run: WorkflowRun = next(workflow.get_runs())
except StopIteration:
return None
if last_run.head_branch != test_github_branch:
return None
return last_run

await wait_for(latest_workflow_run)
lastest_run = typing.cast(WorkflowRun, latest_workflow_run())

def is_workflow_complete():
"""Return if the workflow is complete."""
lastest_run.update()
return lastest_run.status == "completed"

await wait_for(is_workflow_complete)

response = requests.get(
lastest_run.logs_url,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
},
)
zip_data = BytesIO(response.content)
with zipfile.ZipFile(zip_data, "r") as zip_ref:
tmate_log_filename = next(
iter([name for name in zip_ref.namelist() if "Setup tmate session" in name])
)
logs = str(zip_ref.read(tmate_log_filename), encoding="utf-8")

# ensure ssh connection info printed in logs.
assert tmate_ssh_server_unit_ip in logs, "Tmate ssh server IP not found in action logs."
assert "10022" in logs, "Tmate ssh server connection port not found in action logs."
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ deps =
# Type error problem with newer version of macaroonbakery
macaroonbakery==1.3.2
-r{toxinidir}/requirements.txt
-r{[vars]tst_path}integration/requirements.txt
commands =
pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}

Expand Down
Loading