Skip to content

Commit a7f5704

Browse files
authored
PTFE-753 attach gcp runners to github (#362)
1 parent 0b4e7fa commit a7f5704

File tree

8 files changed

+213
-24
lines changed

8 files changed

+213
-24
lines changed

.trunk/configs/.shellcheckrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
enable=all
2+
source-path=SCRIPTDIR
3+
disable=SC2154
4+
5+
# If you're having issues with shellcheck following source, disable the errors via:
6+
# disable=SC1090
7+
# disable=SC1091

.trunk/trunk.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ lint:
2020
paths:
2121
- tests
2222
enabled:
23+
24+
2325
2426
2527

runner_manager/backend/docker.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,12 @@ def _labels(self, runner: Runner) -> Dict[str, str | None]:
5353
labels.update(self.instance_config.labels)
5454
return labels
5555

56-
def _environment(self, runner: Runner) -> Dict[str, str | None]:
57-
"""Return environment variables for the container."""
58-
environment: Dict[str, str | None] = self.instance_config.environment
59-
environment.update(
60-
{
61-
"RUNNER_NAME": runner.name,
62-
"RUNNER_LABELS": ", ".join([label.name for label in runner.labels]),
63-
"RUNNER_TOKEN": runner.token,
64-
"RUNNER_ORG": runner.organization,
65-
"RUNNER_GROUP": runner.runner_group_name,
66-
}
67-
)
68-
return environment
69-
7056
def create(self, runner: Runner):
7157
if self.instance_config.context:
7258
self._build(self.instance_config.context, self.instance_config.image)
7359

7460
labels = self._labels(runner)
75-
environment = self._environment(runner)
61+
environment = self.instance_config.runner_env(runner).dict()
7662
log.info(f"Creating container for runner {runner.name}, labels: {labels}")
7763
container: Container = self.client.containers.run(
7864
self.instance_config.image,

runner_manager/bin/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from pathlib import Path
2+
3+
startup_sh: Path = Path(__file__).parent / "startup.sh"

runner_manager/bin/startup.sh

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env bash
2+
3+
RUNNER_NAME=${RUNNER_NAME:-$(hostname)}
4+
RUNNER_ORG=${RUNNER_ORG:-"org"}
5+
RUNNER_LABELS=${RUNNER_LABELS:-"runner"}
6+
RUNNER_TOKEN=${RUNNER_TOKEN:-"token"}
7+
RUNNER_GROUP=${RUNNER_GROUP:-"default"}
8+
RUNNER_WORKDIR=${RUNNER_WORKDIR:-"_work"}
9+
RUNNER_DOWNLOAD_URL=${RUNNER_DOWNLOAD_URL:-"https://github.com/actions/runner/releases/download/v2.308.0/actions-runner-linux-x64-2.308.0.tar.gz"}
10+
RUNNER_FILE=${RUNNER_FILE:-$(basename "${RUNNER_DOWNLOAD_URL}")}
11+
LSB_RELEASE_CS=${LSB_RELEASE_CS:-$(lsb_release -cs))}
12+
13+
source /etc/os-release
14+
LINUX_OS=${ID}
15+
LINUX_OS_VERSION=$(echo "${VERSION_ID}" | sed -E 's/^([0-9]+)\..*$/\1/')
16+
DOCKER_SERVICE_START="yes"
17+
18+
SSH_KEYS=${SSH_KEYS:-""}
19+
20+
sudo groupadd -f docker
21+
sudo useradd -m actions
22+
sudo usermod -aG docker,root actions
23+
sudo bash -c "echo 'actions ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"
24+
sudo -H -u actions bash -c 'mkdir -p /home/actions/.ssh'
25+
sudo -H -u actions bash -c 'echo "${SSH_KEYS}" >> /home/actions/.ssh/authorized_keys'
26+
27+
if [[ ${LINUX_OS} == "ubuntu" ]]; then
28+
sudo apt-get -y update
29+
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install apt-transport-https \
30+
ca-certificates \
31+
curl \
32+
gnupg \
33+
lsb-release
34+
elif [[ ${LINUX_OS} == "centos" ]] || [[ ${LINUX_OS} == "rocky" ]] || [[ ${LINUX_OS} == "almalinux" ]]; then
35+
sudo yum install -y bind-utils yum-utils
36+
elif [[ ${LINUX_OS} == "rhel" ]]; then
37+
sudo bash -c 'cat <<EOF > /etc/systemd/system/redhat_registration.service
38+
[Unit]
39+
Description=Redhat registration
40+
After=network-online.target
41+
42+
[Service]
43+
Type=oneshot
44+
RemainAfterExit=true
45+
TimeoutStartSec=300
46+
ExecStart=/sbin/subscription-manager register --username={{ redhat_username }} --password={{ redhat_password }} --auto-attach
47+
TimeoutStopSec=300
48+
ExecStop=-/sbin/subscription-manager unregister
49+
50+
[Install]
51+
WantedBy=multi-user.target
52+
EOF'
53+
sudo chmod 600 /etc/systemd/system/redhat_registration.service
54+
sudo systemctl daemon-reload
55+
sudo systemctl enable redhat_registration.service
56+
sudo systemctl start redhat_registration.service
57+
else
58+
echo "OS not managed by the runner-manager"
59+
exit 1
60+
fi
61+
62+
if [[ ! ${RUNNER_LABELS} =~ "no-docker" ]]; then
63+
64+
if [[ ${LINUX_OS} == "ubuntu" ]]; then
65+
sudo apt-get -y update
66+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /tmp/docker.gpg
67+
sudo cat /tmp/docker.gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg || true
68+
echo \
69+
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
70+
${LSB_RELEASE_CS} stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
71+
sudo apt-get update --yes --force-yes
72+
sudo apt-get install --yes --force-yes docker-ce docker-ce-cli containerd.io
73+
elif [[ ${LINUX_OS} == "centos" ]] || [[ ${LINUX_OS} == "rocky" ]] || [[ ${LINUX_OS} == "almalinux" ]]; then
74+
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
75+
sudo yum install -y epel-release docker-ce docker-ce-cli containerd.io
76+
elif [[ ${LINUX_OS} == "rhel" ]]; then
77+
if [[ ${LINUX_OS_VERSION} == "7" ]]; then
78+
# Enable repos to install docker
79+
sudo mkdir /etc/docker/
80+
# TODO: make dns config a setting for the runner
81+
sudo bash -c 'cat > /etc/docker/daemon.json << EOF
82+
{
83+
"dns": ["10.100.1.1", "10.100.1.2", "10.100.1.3"]
84+
}
85+
EOF'
86+
87+
sudo subscription-manager repos --enable=rhel-7-server-extras-rpms --enable=rhel-7-server-optional-rpms
88+
sudo yum install -y docker
89+
elif [[ ${LINUX_OS_VERSION} == "8" || ${LINUX_OS_VERSION} == "9" ]]; then
90+
sudo dnf install -y podman-docker podman
91+
DOCKER_SERVICE_START="no"
92+
else
93+
echo "RHEL version not managed by the runner-manager"
94+
exit 1
95+
fi
96+
fi
97+
98+
if [[ ${DOCKER_SERVICE_START} == "yes" ]]; then
99+
sudo systemctl start docker
100+
fi
101+
fi
102+
103+
# Login as actions user so that all the following commands are executed as actions user
104+
sudo su - actions
105+
mkdir -p /home/actions/actions-runner
106+
cd /home/actions/actions-runner || exit
107+
# Download the runner package
108+
curl -L "${RUNNER_DOWNLOAD_URL}" -o "/tmp/${RUNNER_FILE}"
109+
tar xzf /tmp/"${RUNNER_FILE}"
110+
# install dependencies
111+
sudo ./bin/installdependencies.sh
112+
echo "[Unit]
113+
Description={{Description}}
114+
After=network.target
115+
116+
[Service]
117+
ExecStart=/bin/bash {{RunnerRoot}}/runsvc.sh
118+
User={{User}}
119+
WorkingDirectory={{RunnerRoot}}
120+
KillMode=process
121+
KillSignal=SIGTERM
122+
TimeoutStopSec=5min
123+
124+
[Install]
125+
WantedBy=multi-user.target" >/home/actions/actions-runner/bin/actions.runner.service.template
126+
127+
./config.sh \
128+
--url "https://github.com/${RUNNER_ORG}" \
129+
--token "${RUNNER_TOKEN}" \
130+
--name "${RUNNER_NAME}" \
131+
--work "${RUNNER_WORKDIR}" \
132+
--labels "${RUNNER_LABELS}" \
133+
--runnergroup "${RUNNER_GROUP}" \
134+
--replace \
135+
--unattended \
136+
--ephemeral
137+
138+
if command -v systemctl; then
139+
sudo ./svc.sh install
140+
sudo ./svc.sh start
141+
else
142+
nohup /home/actions/actions-runner/run.sh 2>/home/actions/actions-runner/logs &
143+
fi

runner_manager/models/backend.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
from enum import Enum
2+
from pathlib import Path
23
from typing import Dict, List, Optional
34

4-
from google.cloud.compute import AttachedDisk, Instance, NetworkInterface
5+
from google.cloud.compute import (
6+
AttachedDisk,
7+
Instance,
8+
Items,
9+
Metadata,
10+
NetworkInterface,
11+
)
512
from pydantic import BaseModel
613

14+
from runner_manager.bin import startup_sh
715
from runner_manager.models.runner import Runner
816

917

@@ -20,9 +28,29 @@ class BackendConfig(BaseModel):
2028
"""Base class for backend configuration."""
2129

2230

31+
class RunnerEnv(BaseModel):
32+
"""Base class for required runner instance environment variables."""
33+
34+
RUNNER_NAME: Optional[str] = None
35+
RUNNER_LABELS: Optional[str] = None
36+
RUNNER_TOKEN: Optional[str] = None
37+
RUNNER_ORG: Optional[str] = None
38+
RUNNER_GROUP: Optional[str] = None
39+
40+
2341
class InstanceConfig(BaseModel):
2442
"""Base class for backend instance configuration."""
2543

44+
def runner_env(self, runner: Runner) -> RunnerEnv:
45+
46+
return RunnerEnv(
47+
RUNNER_NAME=runner.name,
48+
RUNNER_LABELS=", ".join([label.name for label in runner.labels]),
49+
RUNNER_TOKEN=runner.token,
50+
RUNNER_ORG=runner.organization,
51+
RUNNER_GROUP=runner.runner_group_name,
52+
)
53+
2654

2755
class DockerInstanceConfig(InstanceConfig):
2856
"""Configuration for Docker backend instance."""
@@ -35,13 +63,6 @@ class DockerInstanceConfig(InstanceConfig):
3563
detach: bool = True
3664
remove: bool = False
3765
labels: Dict[str, str] = {}
38-
environment: Dict[str, str | None] = {
39-
"RUNNER_NAME": None,
40-
"RUNNER_LABELS": None,
41-
"RUNNER_TOKEN": None,
42-
"RUNNER_ORG": None,
43-
"RUNNER_GROUP": "default",
44-
}
4566

4667

4768
class DockerConfig(BackendConfig):
@@ -63,6 +84,7 @@ class GCPInstanceConfig(InstanceConfig):
6384
image_family: str = "ubuntu-2004-lts"
6485
image_project: str = "ubuntu-os-cloud"
6586
machine_type: str = "e2-small"
87+
startup_script: str = startup_sh.as_posix()
6688
network: str = "global/networks/default"
6789
labels: Optional[Dict[str, str]] = {}
6890
image: Optional[str] = None
@@ -73,12 +95,24 @@ class GCPInstanceConfig(InstanceConfig):
7395
class Config:
7496
arbitrary_types_allowed = True
7597

98+
def runner_env(self, runner: Runner) -> List[Items]:
99+
items: List[Items] = []
100+
env: RunnerEnv = super().runner_env(runner)
101+
for key, value in env.dict().items():
102+
items.append(Items(key=key, value=value))
103+
# Adding startup script to install and configure runner
104+
startup_script = Path(self.startup_script).read_text()
105+
items.append(Items(key="startup-script", value=startup_script))
106+
return items
107+
76108
def configure_instance(self, runner: Runner) -> Instance:
77109
"""Configure instance."""
110+
items: List[Items] = self.runner_env(runner)
78111
return Instance(
79112
name=runner.name,
80113
disks=self.disks,
81114
machine_type=self.machine_type,
82115
network_interfaces=self.network_interfaces,
83116
labels=self.labels,
117+
metadata=Metadata(items=items),
84118
)

tests/unit/backend/test_gcp.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import os
22
from typing import List
33

4+
from google.cloud.compute import Items
45
from pytest import fixture, mark, raises
56
from redis_om import NotFoundError
67

78
from runner_manager.backend.gcloud import GCPBackend
9+
from runner_manager.bin import startup_sh
810
from runner_manager.models.backend import Backends, GCPConfig, GCPInstanceConfig
911
from runner_manager.models.runner import Runner
1012
from runner_manager.models.runner_group import RunnerGroup
@@ -43,6 +45,17 @@ def gcp_runner(runner: Runner, gcp_group: RunnerGroup) -> Runner:
4345
return runner
4446

4547

48+
def test_gcp_instance(runner: Runner):
49+
instance: GCPInstanceConfig = GCPInstanceConfig()
50+
items: List[Items] = instance.runner_env(runner)
51+
startup: bool = False
52+
for item in items:
53+
if item.key == "startup-script":
54+
startup = True
55+
assert item.value == startup_sh.read_text()
56+
assert startup is True
57+
58+
4659
@mark.skipif(
4760
not os.getenv("GOOGLE_APPLICATION_CREDENTIALS"), reason="GCP credentials not found"
4861
)

tests/unit/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from runner_manager import Runner, RunnerGroup
1212
from runner_manager.clients.github import GitHub
13+
from runner_manager.models.runner import RunnerLabel
1314

1415
hypothesis_settings.register_profile(
1516
"unit",
@@ -49,7 +50,7 @@ def runner(settings) -> Runner:
4950
runner_group_id=1,
5051
status="online",
5152
busy=False,
52-
labels=[],
53+
labels=[RunnerLabel(name="label")],
5354
manager=settings.name,
5455
)
5556
assert runner.Meta.global_key_prefix == settings.name

0 commit comments

Comments
 (0)