hy-app-monitor/tests/test_gateway_release_clb.py
2026-04-27 18:42:56 +08:00

607 lines
25 KiB
Python

from __future__ import annotations
import json
import unittest
from unittest.mock import patch
from monitors.yumi.service_release import deploy_runtime_services_payload
from monitors.yumi.tencent_clb import call_clb, collect_service_bindings, prepare_gateway_release_clb_snapshot
class GatewayClbClientTests(unittest.TestCase):
@patch("monitors.yumi.tencent_clb.invoke_clb")
@patch("monitors.yumi.tencent_clb.build_clb_client")
def test_call_clb_accepts_top_level_payload_shape(
self,
build_clb_client_mock,
invoke_clb_mock,
) -> None:
class DummyRequest:
def from_json_string(self, text: str) -> None:
self.text = text
class DummyModels:
DescribeTargetsRequest = DummyRequest
class DummyResponse:
def to_json_string(self) -> str:
return json.dumps({"Listeners": [{"ListenerId": "lbl-http"}], "RequestId": "req-1"})
class DummyClient:
def DescribeTargets(self, request):
return request
build_clb_client_mock.return_value = (DummyClient(), "")
invoke_clb_mock.return_value = DummyResponse()
import tencentcloud.clb.v20180317 as clb_pkg
with patch.object(clb_pkg, "models", DummyModels(), create=True):
payload = call_clb("DescribeTargets", {"LoadBalancerId": "lb-test"})
self.assertEqual(payload["RequestId"], "req-1")
self.assertEqual(payload["Listeners"][0]["ListenerId"], "lbl-http")
class GatewayClbDiscoveryTests(unittest.TestCase):
@patch("monitors.yumi.tencent_clb.GATEWAY_CLB_ALLOWED_DOMAINS", ("jvapi.example.com",))
def test_collect_service_bindings_filters_by_allowed_domains(self) -> None:
listeners = [
{
"ListenerId": "lbl-http",
"Rules": [
{
"LocationId": "loc-a",
"Domain": "api.example.com",
"Url": "/",
"Targets": [
{
"InstanceId": "ins-gw-1",
"Port": 8080,
"Weight": 20,
"PrivateIpAddresses": ["10.0.0.1"],
}
],
},
{
"LocationId": "loc-b",
"Domain": "jvapi.example.com",
"Url": "/",
"Targets": [
{
"InstanceId": "ins-gw-1",
"Port": 8080,
"Weight": 10,
"PrivateIpAddresses": ["10.0.0.1"],
}
],
},
],
}
]
bindings = collect_service_bindings(
listeners,
{
"host": "gateway-1",
"service": "gateway",
"ip": "10.0.0.1",
"instanceId": "ins-gw-1",
"port": 8080,
},
)
self.assertEqual(
[(item["domain"], item["locationId"], item["weight"]) for item in bindings],
[("jvapi.example.com", "loc-b", 10)],
)
@patch("monitors.yumi.tencent_clb.GATEWAY_CLB_ALLOWED_DOMAINS", ())
@patch("monitors.yumi.tencent_clb.GATEWAY_CLB_LOAD_BALANCER_ID", "lb-test")
@patch("monitors.yumi.tencent_clb.GATEWAY_CLB_LISTENER_IDS", ("lbl-http", "lbl-https"))
@patch("monitors.yumi.tencent_clb.describe_gateway_targets")
def test_prepare_gateway_release_clb_snapshot_collects_all_matching_rules(
self,
describe_gateway_targets_mock,
) -> None:
describe_gateway_targets_mock.return_value = {
"Listeners": [
{
"ListenerId": "lbl-http",
"Rules": [
{
"LocationId": "loc-a",
"Domain": "api.example.com",
"Url": "/",
"Targets": [
{
"InstanceId": "ins-gw-1",
"Port": 8080,
"Weight": 20,
"PrivateIpAddresses": ["10.0.0.1"],
}
],
},
{
"LocationId": "loc-b",
"Domain": "admin.example.com",
"Url": "/",
"Targets": [
{
"InstanceId": "ins-gw-1",
"Port": 8080,
"Weight": 10,
"PrivateIpAddresses": ["10.0.0.1"],
}
],
},
],
},
{
"ListenerId": "lbl-https",
"Rules": [
{
"LocationId": "loc-c",
"Domain": "api.example.com",
"Url": "/",
"Targets": [
{
"InstanceId": "ins-gw-2",
"Port": 8080,
"Weight": 15,
"PrivateIpAddresses": ["10.0.0.2"],
}
],
}
],
},
]
}
snapshot = prepare_gateway_release_clb_snapshot(
{
"host": "gateway-1",
"service": "gateway",
"ip": "10.0.0.1",
"instanceId": "ins-gw-1",
"port": 8080,
}
)
self.assertEqual(snapshot["loadBalancerId"], "lb-test")
self.assertEqual(snapshot["listenerIds"], ["lbl-http", "lbl-https"])
self.assertEqual(
{(item["listenerId"], item["locationId"], item["weight"]) for item in snapshot["bindings"]},
{("lbl-http", "loc-a", 20), ("lbl-http", "loc-b", 10)},
)
@patch("monitors.yumi.tencent_clb.GATEWAY_CLB_ALLOWED_DOMAINS", ())
def test_collect_service_bindings_ignores_other_targets(self) -> None:
listeners = [
{
"ListenerId": "lbl-http",
"Rules": [
{
"LocationId": "loc-a",
"Targets": [
{
"InstanceId": "ins-gw-1",
"Port": 8080,
"Weight": 20,
"PrivateIpAddresses": ["10.0.0.1"],
},
{
"InstanceId": "ins-gw-2",
"Port": 8080,
"Weight": 20,
"PrivateIpAddresses": ["10.0.0.2"],
},
],
}
],
}
]
bindings = collect_service_bindings(
listeners,
{
"host": "gateway-1",
"service": "gateway",
"ip": "10.0.0.1",
"instanceId": "ins-gw-1",
"port": 8080,
},
)
self.assertEqual(len(bindings), 1)
self.assertEqual(bindings[0]["instanceId"], "ins-gw-1")
class GatewayReleaseFlowTests(unittest.TestCase):
@patch("monitors.yumi.service_release.build_service_images")
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["gateway", "auth"])
@patch("monitors.yumi.service_release.host_image_plan")
@patch("monitors.yumi.service_release.update_local_service_image")
@patch("monitors.yumi.service_release.sync_remote_host_compose_template")
@patch("monitors.yumi.service_release.prepull_host_images")
@patch("monitors.yumi.service_release.prepare_gateway_release_clb_snapshot")
@patch("monitors.yumi.service_release.drain_gateway_release_target")
@patch("monitors.yumi.service_release.restart_service_instance")
@patch("monitors.yumi.service_release.wait_http_ready")
@patch("monitors.yumi.service_release.wait_java_registration_ready")
@patch("monitors.yumi.service_release.wait_gateway_release_target_healthy")
@patch("monitors.yumi.service_release.restore_gateway_release_target")
def test_gateway_update_drains_before_restart_and_restores_after_health(
self,
restore_gateway_release_target_mock,
wait_gateway_release_target_healthy_mock,
wait_java_registration_ready_mock,
wait_http_ready_mock,
restart_service_instance_mock,
drain_gateway_release_target_mock,
prepare_gateway_release_clb_snapshot_mock,
prepull_host_images_mock,
sync_remote_host_compose_template_mock,
update_local_service_image_mock,
host_image_plan_mock,
buildable_service_names_mock,
service_records_by_name_mock,
build_service_images_mock,
) -> None:
del buildable_service_names_mock
gateway_record = {
"host": "gateway-1",
"ip": "10.0.0.1",
"instanceId": "ins-gw-1",
"port": 8080,
"path": "/health",
"service": "gateway",
"kind": "java",
}
service_records_by_name_mock.return_value = {"gateway": [gateway_record]}
build_service_images_mock.return_value = {
"serviceImages": {"gateway": "registry/gateway:new"},
"builds": [],
}
host_image_plan_mock.return_value = [
{
"host": "gateway-1",
"ip": "10.0.0.1",
"services": ["gateway"],
"images": ["registry/gateway:new"],
}
]
update_local_service_image_mock.return_value = {"host": "gateway-1", "service": "gateway"}
sync_remote_host_compose_template_mock.return_value = {
"path": "/tmp/docker-compose.yml",
"changed": False,
"source": "local",
"backupPath": "",
}
prepull_host_images_mock.return_value = {"status": "SUCCESS", "output": "ok"}
prepare_gateway_release_clb_snapshot_mock.return_value = {
"loadBalancerId": "lb-test",
"listenerIds": ["lbl-http"],
"bindings": [{"listenerId": "lbl-http", "locationId": "loc-a", "domain": "", "url": "", "port": 8080, "weight": 10}],
"bindingSummary": "lbl-http / loc-a -> 10",
}
drain_gateway_release_target_mock.return_value = {"summary": "lbl-http / loc-a -> 0"}
restart_service_instance_mock.return_value = {"status": "SUCCESS", "output": "restarted"}
wait_http_ready_mock.return_value = {"ok": True, "statusCode": 200, "latencyMs": 12, "detail": "ok"}
wait_java_registration_ready_mock.return_value = {
"checked": True,
"healthy": True,
"enabled": True,
"source": "instance/list",
}
wait_gateway_release_target_healthy_mock.return_value = [{"listenerId": "lbl-http", "locationId": "loc-a", "healthStatus": True}]
restore_gateway_release_target_mock.return_value = {"summary": "lbl-http / loc-a -> 10"}
call_order: list[str] = []
prepare_gateway_release_clb_snapshot_mock.side_effect = lambda service: call_order.append("prepare") or {
"loadBalancerId": "lb-test",
"listenerIds": ["lbl-http"],
"bindings": [{"listenerId": "lbl-http", "locationId": "loc-a", "domain": "", "url": "", "port": 8080, "weight": 10}],
"bindingSummary": "lbl-http / loc-a -> 10",
}
drain_gateway_release_target_mock.side_effect = lambda snapshot: call_order.append("drain") or {"summary": "lbl-http / loc-a -> 0"}
restart_service_instance_mock.side_effect = lambda service: call_order.append("restart") or {"status": "SUCCESS", "output": "restarted"}
wait_http_ready_mock.side_effect = lambda service, timeout_seconds: call_order.append("http") or {
"ok": True,
"statusCode": 200,
"latencyMs": 12,
"detail": "ok",
}
wait_java_registration_ready_mock.side_effect = lambda service, timeout_seconds: call_order.append("nacos") or {
"checked": True,
"healthy": True,
"enabled": True,
"source": "instance/list",
}
wait_gateway_release_target_healthy_mock.side_effect = lambda snapshot, timeout_seconds: call_order.append("clb-health") or [
{"listenerId": "lbl-http", "locationId": "loc-a", "healthStatus": True}
]
restore_gateway_release_target_mock.side_effect = lambda snapshot: call_order.append("restore") or {
"summary": "lbl-http / loc-a -> 10"
}
result = deploy_runtime_services_payload(
["gateway"],
java_git_ref="main",
go_git_ref="main",
image_tag="20260423",
skip_maven_build=True,
)
self.assertTrue(result["ok"])
self.assertEqual(call_order, ["prepare", "drain", "restart", "http", "nacos", "clb-health", "restore"])
self.assertEqual(result["steps"][0]["clb"]["restoredSummary"], "lbl-http / loc-a -> 10")
@patch("monitors.yumi.service_release.build_service_images")
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["gateway"])
@patch("monitors.yumi.service_release.host_image_plan")
@patch("monitors.yumi.service_release.update_local_service_image")
@patch("monitors.yumi.service_release.sync_remote_host_compose_template")
@patch("monitors.yumi.service_release.prepull_host_images")
@patch("monitors.yumi.service_release.prepare_gateway_release_clb_snapshot")
@patch("monitors.yumi.service_release.drain_gateway_release_target")
@patch("monitors.yumi.service_release.restart_service_instance", side_effect=RuntimeError("restart failed"))
@patch("monitors.yumi.service_release.restore_gateway_release_target")
def test_gateway_update_keeps_target_drained_when_restart_fails(
self,
restore_gateway_release_target_mock,
restart_service_instance_mock,
drain_gateway_release_target_mock,
prepare_gateway_release_clb_snapshot_mock,
prepull_host_images_mock,
sync_remote_host_compose_template_mock,
update_local_service_image_mock,
host_image_plan_mock,
buildable_service_names_mock,
service_records_by_name_mock,
build_service_images_mock,
) -> None:
del buildable_service_names_mock, restart_service_instance_mock
gateway_record = {
"host": "gateway-1",
"ip": "10.0.0.1",
"instanceId": "ins-gw-1",
"port": 8080,
"path": "/health",
"service": "gateway",
"kind": "java",
}
service_records_by_name_mock.return_value = {"gateway": [gateway_record]}
build_service_images_mock.return_value = {
"serviceImages": {"gateway": "registry/gateway:new"},
"builds": [],
}
host_image_plan_mock.return_value = [
{
"host": "gateway-1",
"ip": "10.0.0.1",
"services": ["gateway"],
"images": ["registry/gateway:new"],
}
]
update_local_service_image_mock.return_value = {"host": "gateway-1", "service": "gateway"}
sync_remote_host_compose_template_mock.return_value = {
"path": "/tmp/docker-compose.yml",
"changed": False,
"source": "local",
"backupPath": "",
}
prepull_host_images_mock.return_value = {"status": "SUCCESS", "output": "ok"}
prepare_gateway_release_clb_snapshot_mock.return_value = {
"loadBalancerId": "lb-test",
"listenerIds": ["lbl-http"],
"bindings": [{"listenerId": "lbl-http", "locationId": "loc-a", "domain": "", "url": "", "port": 8080, "weight": 10}],
"bindingSummary": "lbl-http / loc-a -> 10",
}
drain_gateway_release_target_mock.return_value = {"summary": "lbl-http / loc-a -> 0"}
result = deploy_runtime_services_payload(
["gateway"],
java_git_ref="main",
go_git_ref="main",
image_tag="20260423",
skip_maven_build=True,
)
self.assertFalse(result["ok"])
self.assertIn("restart failed", result["error"])
restore_gateway_release_target_mock.assert_not_called()
self.assertEqual(result["steps"][0]["clb"]["drainedSummary"], "lbl-http / loc-a -> 0")
@patch("monitors.yumi.service_release.build_service_images")
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["auth"])
@patch("monitors.yumi.service_release.host_image_plan")
@patch("monitors.yumi.service_release.update_local_service_image")
@patch("monitors.yumi.service_release.sync_remote_host_compose_template")
@patch("monitors.yumi.service_release.prepull_host_images")
@patch("monitors.yumi.service_release.prepare_gateway_release_clb_snapshot")
@patch("monitors.yumi.service_release.restart_service_instance")
@patch("monitors.yumi.service_release.wait_http_ready")
@patch("monitors.yumi.service_release.wait_java_registration_ready")
def test_non_gateway_update_skips_clb_protection(
self,
wait_java_registration_ready_mock,
wait_http_ready_mock,
restart_service_instance_mock,
prepare_gateway_release_clb_snapshot_mock,
prepull_host_images_mock,
sync_remote_host_compose_template_mock,
update_local_service_image_mock,
host_image_plan_mock,
buildable_service_names_mock,
service_records_by_name_mock,
build_service_images_mock,
) -> None:
del buildable_service_names_mock
auth_record = {
"host": "app-1",
"ip": "10.0.0.3",
"instanceId": "ins-app-1",
"port": 1000,
"path": "/actuator/health",
"service": "auth",
"kind": "java",
}
service_records_by_name_mock.return_value = {"auth": [auth_record]}
build_service_images_mock.return_value = {
"serviceImages": {"auth": "registry/auth:new"},
"builds": [],
}
host_image_plan_mock.return_value = [
{
"host": "app-1",
"ip": "10.0.0.3",
"services": ["auth"],
"images": ["registry/auth:new"],
}
]
update_local_service_image_mock.return_value = {"host": "app-1", "service": "auth"}
sync_remote_host_compose_template_mock.return_value = {
"path": "/tmp/docker-compose.yml",
"changed": False,
"source": "local",
"backupPath": "",
}
prepull_host_images_mock.return_value = {"status": "SUCCESS", "output": "ok"}
restart_service_instance_mock.return_value = {"status": "SUCCESS", "output": "restarted"}
wait_http_ready_mock.return_value = {"ok": True, "statusCode": 200, "latencyMs": 11, "detail": "ok"}
wait_java_registration_ready_mock.return_value = {
"checked": True,
"healthy": True,
"enabled": True,
"source": "instance/list",
}
result = deploy_runtime_services_payload(
["auth"],
java_git_ref="main",
go_git_ref="main",
image_tag="20260423",
skip_maven_build=True,
)
self.assertTrue(result["ok"])
prepare_gateway_release_clb_snapshot_mock.assert_not_called()
@patch("monitors.yumi.service_release.build_service_images")
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["other"])
@patch("monitors.yumi.service_release.host_image_plan")
@patch("monitors.yumi.service_release.update_local_service_image")
@patch("monitors.yumi.service_release.sync_remote_host_compose_template")
@patch("monitors.yumi.service_release.prepull_host_images")
@patch("monitors.yumi.service_release.drain_java_registration")
@patch("monitors.yumi.service_release.restart_service_instance")
@patch("monitors.yumi.service_release.wait_http_ready")
@patch("monitors.yumi.service_release.restore_java_registration")
def test_other_update_drains_nacos_before_restart(
self,
restore_java_registration_mock,
wait_http_ready_mock,
restart_service_instance_mock,
drain_java_registration_mock,
prepull_host_images_mock,
sync_remote_host_compose_template_mock,
update_local_service_image_mock,
host_image_plan_mock,
buildable_service_names_mock,
service_records_by_name_mock,
build_service_images_mock,
) -> None:
del buildable_service_names_mock
other_record = {
"host": "app-1",
"ip": "10.0.0.3",
"instanceId": "ins-app-1",
"port": 2400,
"path": "/actuator/health",
"service": "other",
"kind": "java",
}
service_records_by_name_mock.return_value = {"other": [other_record]}
build_service_images_mock.return_value = {
"serviceImages": {"other": "registry/other:new"},
"builds": [],
}
host_image_plan_mock.return_value = [
{
"host": "app-1",
"ip": "10.0.0.3",
"services": ["other"],
"images": ["registry/other:new"],
}
]
update_local_service_image_mock.return_value = {"host": "app-1", "service": "other"}
sync_remote_host_compose_template_mock.return_value = {
"path": "/tmp/docker-compose.yml",
"changed": False,
"source": "local",
"backupPath": "",
}
prepull_host_images_mock.return_value = {"status": "SUCCESS", "output": "ok"}
wait_http_ready_mock.return_value = {"ok": True, "statusCode": 200, "latencyMs": 11, "detail": "ok"}
drain_java_registration_mock.return_value = {
"checked": True,
"enabled": False,
"source": "instance/list",
"drainSeconds": 8.0,
}
restore_java_registration_mock.return_value = {
"checked": True,
"healthy": True,
"enabled": True,
"source": "instance/list",
"restored": True,
}
call_order: list[str] = []
drain_java_registration_mock.side_effect = lambda service: call_order.append("drain") or {
"checked": True,
"enabled": False,
"source": "instance/list",
"drainSeconds": 8.0,
}
restart_service_instance_mock.side_effect = lambda service: call_order.append("restart") or {
"status": "SUCCESS",
"output": "restarted",
}
wait_http_ready_mock.side_effect = lambda service, timeout_seconds: call_order.append("http") or {
"ok": True,
"statusCode": 200,
"latencyMs": 11,
"detail": "ok",
}
restore_java_registration_mock.side_effect = lambda service: call_order.append("restore") or {
"checked": True,
"healthy": True,
"enabled": True,
"source": "instance/list",
"restored": True,
}
result = deploy_runtime_services_payload(
["other"],
java_git_ref="main",
go_git_ref="main",
image_tag="20260423",
skip_maven_build=True,
)
self.assertTrue(result["ok"])
self.assertEqual(call_order, ["drain", "restart", "http", "restore"])
self.assertEqual(result["steps"][0]["nacosDrain"]["enabled"], False)
self.assertEqual(result["steps"][0]["nacos"]["restored"], True)
if __name__ == "__main__":
unittest.main()