Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions src-docs/runner.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Single instance of GitHub self-hosted runner.

Attrs: app_name (str): Name of the charm. path (GitHubPath): Path to GitHub repo or org. proxies (ProxySetting): HTTP proxy setting for juju charm. name (str): Name of the runner instance. exist (bool): Whether the runner instance exists on LXD. online (bool): Whether GitHub marks this runner as online. busy (bool): Whether GitHub marks this runner as busy.

<a href="../src/runner.py#L83"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/runner.py#L84"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down Expand Up @@ -46,7 +46,7 @@ Construct the runner instance.

---

<a href="../src/runner.py#L111"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/runner.py#L112"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `create`

Expand Down Expand Up @@ -78,7 +78,7 @@ Create the runner instance on LXD and register it on GitHub.

---

<a href="../src/runner.py#L154"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/runner.py#L155"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `remove`

Expand Down
95 changes: 82 additions & 13 deletions src/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional, Sequence
from urllib.parse import urljoin, urlsplit, urlunsplit

import yaml

Expand Down Expand Up @@ -499,12 +500,13 @@ def _install_binary(self, binary: Path) -> None:
self._apt_install(["docker.io", "npm", "python3-pip", "shellcheck", "jq", "wget"])
self._wget_install(
[
WgetExecutable(
url="https://github.com/mikefarah/yq/releases/download/v4.34.1/yq_linux_amd64",
yq_executable := WgetExecutable(
url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64",
cmd="yq",
)
]
)
self._verify_yq_checksum(yq_executable)

# Add the user to docker group.
self.instance.execute(["/usr/sbin/usermod", "-aG", "docker", "ubuntu"])
Expand Down Expand Up @@ -751,6 +753,28 @@ def _apt_install(self, packages: Iterable[str]) -> None:

self.instance.execute(["/usr/bin/apt-get", "clean"])

def _wget_download(self, url: str, path: str) -> None:
"""Download the file to path.

Args:
url: URL to download from.
path: Path to store the file.
"""
if self.instance is None:
raise RunnerError("Runner operation called prior to runner creation.")

logger.info("Downloading %s via wget to %s...", url, path)
wget_cmd = ["/usr/bin/wget", url, "-O", path]
if self.config.proxies.get("http", None) or self.config.proxies.get("https", None):
wget_cmd += ["-e", "use_proxy=on"]
if self.config.proxies.get("http", None):
wget_cmd += ["-e", f"http_proxy={self.config.proxies['http']}"]
if self.config.proxies.get("https", None):
wget_cmd += ["-e", f"https_proxy={self.config.proxies['https']}"]
if self.config.proxies.get("no_proxy", None):
wget_cmd += ["-e", f"no_proxy={self.config.proxies['no_proxy']}"]
self.instance.execute(wget_cmd)

def _wget_install(self, executables: Iterable[WgetExecutable]) -> None:
"""Installs the given binaries.

Expand All @@ -765,15 +789,60 @@ def _wget_install(self, executables: Iterable[WgetExecutable]) -> None:

for executable in executables:
executable_path = f"/usr/bin/{executable.cmd}"
logger.info("Downloading %s via wget to %s...", executable.url, executable_path)
wget_cmd = ["/usr/bin/wget", executable.url, "-O", executable_path]
if self.config.proxies.get("http", None) or self.config.proxies.get("https", None):
wget_cmd += ["-e", "use_proxy=on"]
if self.config.proxies.get("http", None):
wget_cmd += ["-e", f"http_proxy={self.config.proxies['http']}"]
if self.config.proxies.get("https", None):
wget_cmd += ["-e", f"https_proxy={self.config.proxies['https']}"]
if self.config.proxies.get("no_proxy", None):
wget_cmd += ["-e", f"no_proxy={self.config.proxies['no_proxy']}"]
self.instance.execute(wget_cmd)
self._wget_download(executable.url, executable_path)
self.instance.execute(["/usr/bin/chmod", "+x", executable_path])

def _execute(self, cmd: list[str], cwd: str | None = None) -> tuple[str, str]:
"""Execute command in runner instance.

Args:
cmd: Commands to be executed.
cwd: Working directory to execute the commands.

Returns:
Tuple containing the stdout, stderr.
"""
if self.instance is None:
raise RunnerError("Runner operation called prior to runner creation.")

return_code, stdout, stderr = self.instance.execute(cmd, cwd)
stdout_str = stdout.read().decode("utf-8")
stderr_str = stderr.read().decode("utf-8")

if return_code != 0:
raise RunnerError(
(
f"Command {' '.join(cmd)} failed with code {return_code} in runner "
f"{self.config.name}: {stderr_str}"
)
)

return stdout_str, stderr_str

def _verify_yq_checksum(self, yq_executable: WgetExecutable) -> None:
"""Verify the checksum is correct for the yq executable.

Args:
yq_executable: Information on yq executable to check.
"""
if self.instance is None:
raise RunnerError("Runner operation called prior to runner creation.")

split_url = urlsplit(yq_executable.url)
# Extract the base download path
base_path, executable_name = split_url.path.rsplit("/", 1)
base_path_parts = split_url._replace(path=base_path)
base_url = urlunsplit(base_path_parts)

self._wget_download(urljoin(base_url, "checksums"), "checksums")
self._wget_download(urljoin(base_url, "extract-checksum.sh"), "extract-checksum.sh")

stdout, _ = self._execute(["bash", "extract-checksum.sh", "SHA-256", executable_name])
expected_checksum = stdout.rsplit(maxsplit=1)[1]

yq_path, _ = self._execute(["which", yq_executable.cmd])
stdout, _ = self._execute(["sha256sum", yq_path])
calculated_checksum = stdout.split(maxsplit=1)[0]

if expected_checksum != calculated_checksum:
raise RunnerError("Checksum mismatch for yq executable")
4 changes: 2 additions & 2 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ def mocks(monkeypatch, tmp_path, exec_command):
monkeypatch.setattr("firewall.Firewall.refresh_firewall", unittest.mock.MagicMock())
monkeypatch.setattr("runner.execute_command", exec_command)
monkeypatch.setattr("runner.shared_fs", unittest.mock.MagicMock())
monkeypatch.setattr("runner.time", unittest.mock.MagicMock())
monkeypatch.setattr("runner.Runner._verify_yq_checksum", unittest.mock.MagicMock())
monkeypatch.setattr("metrics.execute_command", exec_command)
monkeypatch.setattr("metrics.METRICS_LOG_PATH", Path(tmp_path / "metrics.log"))
monkeypatch.setattr("metrics.LOGROTATE_CONFIG", Path(tmp_path / "github-runner-metrics"))

monkeypatch.setattr("runner.time", unittest.mock.MagicMock())
monkeypatch.setattr("runner_manager.GhApi", MockGhapiClient)
monkeypatch.setattr("runner_manager.jinja2", unittest.mock.MagicMock())
monkeypatch.setattr("runner_manager.LxdClient", MockLxdClient)
Expand Down