mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-09-26 07:05:34 +00:00
394 lines
16 KiB
Python
394 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2023 Greptime Team
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Coverage for the pure parts of the Aliyun ECS runner provision/teardown scripts."""
|
|
|
|
import base64
|
|
import importlib.util
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
from contextlib import redirect_stderr
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
SCRIPTS_DIR = Path(__file__).parents[2] / ".github/scripts"
|
|
|
|
|
|
def load_module(name: str, filename: str):
|
|
spec = importlib.util.spec_from_file_location(name, SCRIPTS_DIR / filename)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
provision = load_module(
|
|
"aliyun_ecs_runner_provision_under_test", "aliyun-ecs-runner-provision.py"
|
|
)
|
|
teardown = load_module(
|
|
"aliyun_ecs_runner_teardown_under_test", "aliyun-ecs-runner-teardown.py"
|
|
)
|
|
|
|
|
|
class GitHubApiResponseTest(unittest.TestCase):
|
|
def test_success_response_body(self):
|
|
cases = ((200, b'{"runners": []}', {"runners": []}), (204, b"", {}))
|
|
for status, body, expected in cases:
|
|
with self.subTest(status=status):
|
|
response = Mock(status=status)
|
|
response.read.return_value = body
|
|
response.__enter__ = Mock(return_value=response)
|
|
response.__exit__ = Mock(return_value=False)
|
|
with patch.object(provision.urllib.request, "urlopen", return_value=response):
|
|
self.assertEqual(provision.github_api("token", "DELETE", "/test"), expected)
|
|
|
|
def test_invalid_json_still_fails(self):
|
|
response = Mock(status=200)
|
|
response.read.return_value = b"not json"
|
|
response.__enter__ = Mock(return_value=response)
|
|
response.__exit__ = Mock(return_value=False)
|
|
with patch.object(provision.urllib.request, "urlopen", return_value=response):
|
|
with self.assertRaises(ValueError):
|
|
provision.github_api("token", "GET", "/test")
|
|
|
|
|
|
class ProvisionNamingTest(unittest.TestCase):
|
|
def test_runner_name_and_label_derive_from_run_id(self) -> None:
|
|
self.assertEqual(provision.runner_name_for_run("12345"), "qreg-ecs-12345")
|
|
self.assertEqual(provision.runner_label_for_run("12345"), "query-regression-ecs-12345")
|
|
|
|
|
|
class ProvisionResourcesTest(unittest.TestCase):
|
|
@staticmethod
|
|
def config(**overrides):
|
|
values = {
|
|
"region_id": "region", "vswitch_id": "vswitch", "security_group_id": "sg",
|
|
"image_id": "image", "instance_type": "ecs.u1-c1m1.2xlarge", "repo": "owner/repo",
|
|
"run_id": "12345", "github_token": "token",
|
|
}
|
|
return provision.ProvisionConfig(**(values | overrides))
|
|
|
|
def test_sdk_request_defaults_and_overrides(self):
|
|
models = SimpleNamespace(
|
|
RunInstancesRequest=SimpleNamespace,
|
|
RunInstancesRequestSystemDisk=SimpleNamespace,
|
|
RunInstancesRequestTag=SimpleNamespace,
|
|
)
|
|
for overrides, disk, ttl in (
|
|
({}, "80", None),
|
|
({"system_disk_gib": 500, "ttl_hours": 8}, "500", "8"),
|
|
):
|
|
with (
|
|
self.subTest(overrides=overrides),
|
|
patch.dict(
|
|
sys.modules,
|
|
{"alibabacloud_ecs20140526": SimpleNamespace(models=models)},
|
|
),
|
|
):
|
|
client = Mock()
|
|
client.run_instances.return_value = SimpleNamespace(
|
|
body=SimpleNamespace(
|
|
instance_id_sets=SimpleNamespace(instance_id_set=["i-test"])
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
provision.run_instance(
|
|
client, self.config(**overrides), "userdata"
|
|
),
|
|
"i-test",
|
|
)
|
|
request = client.run_instances.call_args.args[0]
|
|
self.assertEqual(request.system_disk.size, disk)
|
|
self.assertEqual(request.system_disk.category, "cloud_essd")
|
|
self.assertEqual(request.instance_type, "ecs.u1-c1m1.2xlarge")
|
|
tags = {tag.key: tag.value for tag in request.tag}
|
|
expected = {
|
|
provision.MANAGED_BY_TAG_KEY: provision.MANAGED_BY_TAG_VALUE,
|
|
provision.RUN_TAG_KEY: "12345",
|
|
}
|
|
if ttl is not None:
|
|
expected[provision.TTL_TAG_KEY] = ttl
|
|
self.assertEqual(tags, expected)
|
|
|
|
def test_invalid_resource_bounds(self):
|
|
for overrides in (
|
|
{"system_disk_gib": 19},
|
|
{"system_disk_gib": 2049},
|
|
{"ttl_hours": 0},
|
|
{"ttl_hours": 169},
|
|
):
|
|
with self.subTest(overrides=overrides), self.assertRaises(ValueError):
|
|
self.config(**overrides)
|
|
for disk in (20, 2048):
|
|
self.assertEqual(self.config(system_disk_gib=disk).system_disk_gib, disk)
|
|
for ttl in (1, 168):
|
|
self.assertEqual(self.config(ttl_hours=ttl).ttl_hours, ttl)
|
|
|
|
def test_cli_environment_defaults_and_overrides(self):
|
|
args = [
|
|
"provision",
|
|
"--region-id",
|
|
"region",
|
|
"--vswitch-id",
|
|
"vswitch",
|
|
"--security-group-id",
|
|
"sg",
|
|
"--image-id",
|
|
"image",
|
|
"--instance-type",
|
|
"ecs.u1-c1m1.2xlarge",
|
|
"--repo",
|
|
"owner/repo",
|
|
"--run-id",
|
|
"12345",
|
|
"--github-token",
|
|
"token",
|
|
]
|
|
cases = [
|
|
({}, [], 80, None, False),
|
|
(
|
|
{"ALIYUN_ECS_SYSTEM_DISK_GIB": "500", "ALIYUN_ECS_TTL_HOURS": "8"},
|
|
[],
|
|
500,
|
|
8,
|
|
False,
|
|
),
|
|
(
|
|
{"ALIYUN_ECS_SYSTEM_DISK_GIB": "500", "ALIYUN_ECS_TTL_HOURS": "8"},
|
|
["--system-disk-gib", "600", "--ttl-hours", "12"],
|
|
600,
|
|
12,
|
|
False,
|
|
),
|
|
({"ALIYUN_ECS_TTL_HOURS": ""}, [], 80, None, False),
|
|
({"ALIYUN_ECS_ENABLE_DOCKER": "true"}, [], 80, None, True),
|
|
({"ALIYUN_ECS_ENABLE_DOCKER": "true"}, ["--enable-docker", "false"], 80, None, False),
|
|
({}, ["--enable-docker", "true"], 80, None, True),
|
|
]
|
|
for env, extra, disk, ttl, docker in cases:
|
|
with (
|
|
self.subTest(env=env, extra=extra),
|
|
patch.dict(os.environ, env, clear=True),
|
|
patch.object(sys, "argv", args + extra),
|
|
patch.object(provision, "provision", return_value=0) as run,
|
|
):
|
|
self.assertEqual(provision.main(), 0)
|
|
config = run.call_args.args[0]
|
|
self.assertEqual(
|
|
(config.system_disk_gib, config.ttl_hours, config.enable_docker), (disk, ttl, docker)
|
|
)
|
|
|
|
|
|
class ProvisionUserDataTest(unittest.TestCase):
|
|
def render(self) -> str:
|
|
return provision.render_user_data(
|
|
runner_name="qreg-ecs-12345",
|
|
runner_label="query-regression-ecs-12345",
|
|
runner_token="TOKEN",
|
|
repo="GreptimeTeam/greptimedb",
|
|
)
|
|
|
|
def test_user_data_creates_cache_paths_on_the_system_disk(self) -> None:
|
|
script = self.render()
|
|
self.assertNotIn("DISK_SERIAL", script)
|
|
self.assertNotIn("mount --bind", script)
|
|
self.assertNotIn("mkfs.ext4", script)
|
|
for destination in provision.CACHE_PATHS:
|
|
self.assertIn(f'"{destination}"', script)
|
|
|
|
def test_user_data_wires_runner_registration(self) -> None:
|
|
script = self.render()
|
|
self.assertIn("RUNNER_NAME=qreg-ecs-12345", script)
|
|
self.assertIn("RUNNER_LABELS=query-regression-ecs-12345", script)
|
|
self.assertIn("RUNNER_TOKEN=TOKEN", script)
|
|
self.assertIn("REPO_URL=https://github.com/GreptimeTeam/greptimedb", script)
|
|
self.assertIn("PATH=/opt/cargo/bin:", script)
|
|
self.assertIn("systemctl restart --no-block ephemeral-github-runner.service", script)
|
|
|
|
def test_docker_setup_is_opt_in_and_precedes_runner(self):
|
|
args = ("runner-name", "label", "token", "owner/repo")
|
|
default = provision.render_user_data(*args)
|
|
self.assertEqual(default, provision.render_user_data(*args, enable_docker=False))
|
|
self.assertNotIn("systemctl start docker", default)
|
|
self.assertNotIn("usermod", default)
|
|
enabled = provision.render_user_data(*args, runner_uid="2001", enable_docker=True)
|
|
start = enabled.index("# Reuse Docker CE")
|
|
end = enabled.index("cat > /etc/ephemeral-github-runner.env")
|
|
setup = enabled[start:end]
|
|
self.assertEqual(enabled[:start] + enabled[end:],
|
|
provision.render_user_data(*args, runner_uid="2001"))
|
|
self.assertLess(end, enabled.index("systemctl restart --no-block ephemeral-github-runner.service"))
|
|
for forbidden in ("apt-get", "sudo", "setfacl"):
|
|
self.assertNotIn(forbidden, setup)
|
|
mocks = '''
|
|
docker() { :; }
|
|
jq() { :; }
|
|
systemctl() { echo "systemctl $*"; return "$FAIL_START"; }
|
|
id() { [[ "$*" == "-nu 2001" ]] || return 1; echo custom-runner; }
|
|
usermod() { echo "usermod $*"; }
|
|
runuser() { echo "runuser $*"; }
|
|
'''
|
|
for fail in ("0", "1"):
|
|
with self.subTest(fail_start=fail):
|
|
result = subprocess.run(["bash", "-euc", mocks + setup],
|
|
env=os.environ | {"FAIL_START": fail},
|
|
capture_output=True, text=True)
|
|
self.assertEqual(result.returncode, int(fail), result.stderr)
|
|
calls = result.stdout.splitlines()
|
|
self.assertEqual(calls, ["docker", "jq", "systemctl start docker"] + (
|
|
["usermod -aG docker custom-runner", "runuser -u custom-runner -- docker info"]
|
|
if fail == "0" else []))
|
|
|
|
def test_encode_user_data_round_trips(self) -> None:
|
|
script = self.render()
|
|
self.assertEqual(
|
|
base64.b64decode(provision.encode_user_data(script)).decode("utf-8"), script
|
|
)
|
|
|
|
def test_user_data_enables_swap_and_masks_oomd(self) -> None:
|
|
script = self.render()
|
|
self.assertIn("systemctl mask systemd-oomd.socket systemd-oomd.service", script)
|
|
self.assertIn("systemctl mask unattended-upgrades.service apt-daily.timer apt-daily-upgrade.timer", script)
|
|
self.assertIn('APT::Periodic::Unattended-Upgrade "0"', script)
|
|
self.assertIn(f'fallocate --length {provision.SWAP_SIZE_GIB}G "{provision.SWAP_FILE}"', script)
|
|
self.assertIn(f'swapon "{provision.SWAP_FILE}"', script)
|
|
self.assertIn("sysctl --write vm.swappiness=10", script)
|
|
self.assertIn("OOMPolicy=continue", script)
|
|
self.assertNotIn("OOMScoreAdjust", script)
|
|
self.assertLess(script.index("swapon"), script.index("systemctl restart --no-block ephemeral-github-runner.service"))
|
|
|
|
|
|
class TeardownExpiryTest(unittest.TestCase):
|
|
NOW = datetime(2026, 8, 17, 6, 0, tzinfo=timezone.utc)
|
|
TTL = timedelta(hours=4)
|
|
|
|
def test_parse_creation_time_formats(self) -> None:
|
|
self.assertEqual(
|
|
teardown.parse_creation_time("2026-08-17T01:02:03Z"),
|
|
datetime(2026, 8, 17, 1, 2, 3, tzinfo=timezone.utc),
|
|
)
|
|
self.assertEqual(
|
|
teardown.parse_creation_time("2026-08-17T01:02Z"),
|
|
datetime(2026, 8, 17, 1, 2, tzinfo=timezone.utc),
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
teardown.parse_creation_time("not-a-time")
|
|
|
|
def test_expired_instance_names_selects_only_old_instances(self) -> None:
|
|
instances = [
|
|
("i-old", "qreg-ecs-1", "2026-08-17T01:00Z", None), # 5h old: expired
|
|
("i-edge", "qreg-ecs-2", "2026-08-17T02:00Z", None), # exactly TTL: expired
|
|
("i-fresh", "qreg-ecs-3", "2026-08-17T05:30Z", None), # 30m old: kept
|
|
]
|
|
self.assertEqual(
|
|
teardown.expired_instance_names(instances, self.NOW, self.TTL),
|
|
[("i-old", "qreg-ecs-1"), ("i-edge", "qreg-ecs-2")],
|
|
)
|
|
|
|
def test_tagged_ttl_overrides_fallback_and_preserves_legacy(self):
|
|
instances = [
|
|
("i-legacy", "legacy", "2026-08-17T01:00Z", None),
|
|
("i-long", "long", "2026-08-17T01:00Z", "8"),
|
|
("i-short", "short", "2026-08-17T03:00Z", "2"),
|
|
("i-edge", "edge", "2026-08-16T22:00Z", "8"),
|
|
("i-not-yet", "not-yet", "2026-08-16T22:00:01Z", "8"),
|
|
]
|
|
self.assertEqual(
|
|
teardown.expired_instance_names(instances, self.NOW, self.TTL),
|
|
[("i-legacy", "legacy"), ("i-short", "short"), ("i-edge", "edge")],
|
|
)
|
|
|
|
def test_malformed_ttl_never_falls_back_to_earlier_deletion(self):
|
|
for ttl in ("", "bad", "-1", "0", "169", "8.5", "nan"):
|
|
with self.subTest(ttl=ttl), redirect_stderr(io.StringIO()) as logs:
|
|
self.assertEqual(
|
|
teardown.expired_instance_names(
|
|
[("i-live", "live", "2026-08-16T01:00Z", ttl)],
|
|
self.NOW,
|
|
self.TTL,
|
|
),
|
|
[],
|
|
)
|
|
self.assertIn("Skipping i-live", logs.getvalue())
|
|
|
|
def test_list_instances_preserves_tags_and_pagination(self):
|
|
models = SimpleNamespace(
|
|
DescribeInstancesRequest=SimpleNamespace,
|
|
DescribeInstancesRequestTag=SimpleNamespace,
|
|
)
|
|
client = Mock()
|
|
|
|
def response(instance, token):
|
|
return SimpleNamespace(
|
|
body=SimpleNamespace(
|
|
instances=SimpleNamespace(instance=[instance]), next_token=token
|
|
)
|
|
)
|
|
|
|
client.describe_instances.side_effect = [
|
|
response(
|
|
SimpleNamespace(
|
|
instance_id="i-1",
|
|
instance_name="one",
|
|
creation_time="2026-08-17T01:00Z",
|
|
tags=SimpleNamespace(
|
|
tag=[
|
|
SimpleNamespace(
|
|
tag_key=provision.TTL_TAG_KEY, tag_value="8"
|
|
)
|
|
]
|
|
),
|
|
),
|
|
"next",
|
|
),
|
|
response(
|
|
SimpleNamespace(
|
|
instance_id="i-2",
|
|
instance_name="two",
|
|
creation_time="2026-08-17T02:00Z",
|
|
tags=None,
|
|
),
|
|
None,
|
|
),
|
|
]
|
|
with patch.dict(
|
|
sys.modules, {"alibabacloud_ecs20140526": SimpleNamespace(models=models)}
|
|
):
|
|
self.assertEqual(
|
|
teardown.list_managed_instances(client, "region"),
|
|
[
|
|
("i-1", "one", "2026-08-17T01:00Z", "8"),
|
|
("i-2", "two", "2026-08-17T02:00Z", None),
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
client.describe_instances.call_args_list[1].args[0].next_token, "next"
|
|
)
|
|
tag = client.describe_instances.call_args_list[0].args[0].tag[0]
|
|
self.assertEqual(
|
|
(tag.key, tag.value),
|
|
(provision.MANAGED_BY_TAG_KEY, provision.MANAGED_BY_TAG_VALUE),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|