444 lines
18 KiB
Python
444 lines
18 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_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)},
|
|
)
|
|
|
|
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()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|