Skip to content

Commit 197153b

Browse files
authored
Merge branch 'main' into fix/caching-issue-lab-finish
2 parents 204e207 + 70ed442 commit 197153b

14 files changed

Lines changed: 1766 additions & 12 deletions

File tree

api/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ dependencies = [
3131
"watchfiles==1.0.5",
3232
"werkzeug==3.1.3",
3333
"google-cloud-storage==2.18.2",
34+
"azure-identity==1.25.1",
35+
"azure-mgmt-compute==34.1.0",
36+
"azure-mgmt-network==30.1.0",
37+
"azure-mgmt-resource==24.0.0",
38+
"azure-mgmt-authorization==4.0.0",
3439
"azure-storage-blob==12.28.0",
3540
"cashews==7.5.0",
3641
"paramiko==4.0.0",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""Tests for Azure provider creation in team_provider_endpoints."""
2+
3+
import asyncio
4+
from unittest.mock import AsyncMock, MagicMock, patch
5+
6+
from transformerlab.shared.models.models import ProviderType
7+
8+
9+
def test_create_azure_provider_injects_resource_group_and_team_id():
10+
"""create_provider_for_team must inject azure_resource_group and team_id into config for Azure providers."""
11+
from transformerlab.services.compute_provider.team_provider_endpoints import create_provider_for_team
12+
from pydantic import BaseModel
13+
14+
class FakeConfig(BaseModel):
15+
azure_subscription_id: str = "sub-123"
16+
azure_tenant_id: str = "tenant-456"
17+
azure_client_id: str = "client-789"
18+
azure_client_secret: str = "secret"
19+
azure_location: str = "eastus"
20+
21+
def model_dump(self, **kwargs):
22+
return {
23+
"azure_subscription_id": self.azure_subscription_id,
24+
"azure_tenant_id": self.azure_tenant_id,
25+
"azure_client_id": self.azure_client_id,
26+
"azure_client_secret": self.azure_client_secret,
27+
"azure_location": self.azure_location,
28+
}
29+
30+
captured_config = {}
31+
32+
async def fake_create_team_provider(session, team_id, name, provider_type, config, created_by_user_id):
33+
captured_config.update(config)
34+
mock_provider = MagicMock()
35+
mock_provider.type = "azure"
36+
mock_provider.id = "prov-1"
37+
mock_provider.name = name
38+
mock_provider.config = config
39+
mock_provider.is_default = False
40+
mock_provider.disabled = False
41+
mock_provider.supported_accelerators = []
42+
mock_provider.team_id = team_id
43+
mock_provider.created_by_user_id = "user-1"
44+
mock_provider.created_at = None
45+
mock_provider.updated_at = None
46+
return mock_provider
47+
48+
provider_data = MagicMock()
49+
provider_data.type = ProviderType.AZURE
50+
provider_data.name = "my-azure"
51+
provider_data.config = FakeConfig()
52+
53+
with (
54+
patch(
55+
"transformerlab.services.compute_provider.team_provider_endpoints.create_team_provider",
56+
side_effect=fake_create_team_provider,
57+
),
58+
patch(
59+
"transformerlab.services.compute_provider.team_provider_endpoints.list_team_providers",
60+
new_callable=AsyncMock,
61+
return_value=[],
62+
),
63+
patch(
64+
"transformerlab.services.compute_provider.team_provider_endpoints.cache.invalidate",
65+
new_callable=AsyncMock,
66+
),
67+
):
68+
asyncio.run(
69+
create_provider_for_team(
70+
session=MagicMock(),
71+
team_id="team-xyz",
72+
user=MagicMock(id="user-1"),
73+
provider_data=provider_data,
74+
force_refresh=False,
75+
)
76+
)
77+
78+
assert captured_config["azure_resource_group"] == "transformerlab-team-xyz"
79+
assert captured_config["team_id"] == "team-xyz"
80+
81+
82+
def test_azure_is_in_allowed_provider_types():
83+
"""Azure must be in allowed_provider_types — verifies it no longer raises 400."""
84+
from transformerlab.services.compute_provider.team_provider_endpoints import create_provider_for_team
85+
from fastapi import HTTPException
86+
from pydantic import BaseModel
87+
88+
class FakeConfig(BaseModel):
89+
azure_subscription_id: str = "sub-123"
90+
91+
def model_dump(self, **kwargs):
92+
return {"azure_subscription_id": self.azure_subscription_id}
93+
94+
provider_data = MagicMock()
95+
provider_data.type = ProviderType.AZURE
96+
provider_data.name = "my-azure"
97+
provider_data.config = FakeConfig()
98+
99+
did_raise_type_error = False
100+
with (
101+
patch(
102+
"transformerlab.services.compute_provider.team_provider_endpoints.list_team_providers",
103+
new_callable=AsyncMock,
104+
return_value=[],
105+
),
106+
patch(
107+
"transformerlab.services.compute_provider.team_provider_endpoints.create_team_provider",
108+
new_callable=AsyncMock,
109+
side_effect=Exception("stop here"),
110+
),
111+
patch(
112+
"transformerlab.services.compute_provider.team_provider_endpoints.cache.invalidate",
113+
new_callable=AsyncMock,
114+
),
115+
):
116+
try:
117+
asyncio.run(
118+
create_provider_for_team(
119+
session=MagicMock(),
120+
team_id="team-xyz",
121+
user=MagicMock(id="user-1"),
122+
provider_data=provider_data,
123+
force_refresh=False,
124+
)
125+
)
126+
except HTTPException as e:
127+
if e.status_code == 400 and "Invalid provider type" in str(e.detail):
128+
did_raise_type_error = True
129+
except Exception:
130+
pass
131+
132+
assert not did_raise_type_error, "Azure should be in allowed_provider_types"

api/test/api/test_remote_job_status_service.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,81 @@ async def fake_update_status(job_id, status, experiment_id=None):
522522
assert ("status", "STOPPED") in calls
523523

524524

525+
@pytest.mark.asyncio
526+
async def test_check_job_via_provider_azure_unknown_stopping_debounced(monkeypatch):
527+
"""AZURE STOPPING + UNKNOWN should debounce before STOPPED."""
528+
monkeypatch.setattr(remote_job_status_service, "EMPTY_PROVIDER_JOBS_TERMINAL_THRESHOLD", 2)
529+
from transformerlab.compute_providers.models import ClusterStatus, ClusterState
530+
531+
job = _make_job(status="STOPPING")
532+
record = _make_provider_record("azure")
533+
534+
cluster_status = MagicMock(spec=ClusterStatus)
535+
cluster_status.state = ClusterState.UNKNOWN
536+
cluster_status.status_message = "unknown"
537+
538+
instance = MagicMock()
539+
instance.get_cluster_status = MagicMock(return_value=cluster_status)
540+
541+
calls = []
542+
543+
async def fake_update_kv(job_id, key, value, exp_id):
544+
calls.append(("kv", key, value))
545+
546+
monkeypatch.setattr(
547+
remote_job_status_service.job_service,
548+
"job_update_job_data_insert_key_value",
549+
fake_update_kv,
550+
)
551+
monkeypatch.setattr(
552+
remote_job_status_service.job_service,
553+
"job_update_status",
554+
AsyncMock(side_effect=AssertionError("should not be called on first poll")),
555+
)
556+
557+
result = await _check_job_via_provider(job, "exp-1", record, instance)
558+
assert result is False
559+
assert ("kv", "azure_stopping_unknown_polls", 1) in calls
560+
561+
job["job_data"]["azure_stopping_unknown_polls"] = 1
562+
monkeypatch.setattr(remote_job_status_service.job_service, "job_update_status", AsyncMock())
563+
result = await _check_job_via_provider(job, "exp-1", record, instance)
564+
assert result is True
565+
566+
567+
@pytest.mark.asyncio
568+
async def test_check_job_via_provider_azure_resets_unknown_stopping_counter(monkeypatch):
569+
"""AZURE clears debounce counter once cluster state is no longer UNKNOWN."""
570+
from transformerlab.compute_providers.models import ClusterStatus, ClusterState
571+
572+
job = _make_job(status="STOPPING")
573+
job["job_data"]["azure_stopping_unknown_polls"] = 2
574+
record = _make_provider_record("azure")
575+
576+
cluster_status = MagicMock(spec=ClusterStatus)
577+
cluster_status.state = ClusterState.DOWN
578+
cluster_status.status_message = "ResourceNotFound"
579+
580+
instance = MagicMock()
581+
instance.get_cluster_status = MagicMock(return_value=cluster_status)
582+
583+
calls = []
584+
585+
async def fake_update_kv(job_id, key, value, exp_id):
586+
calls.append(("kv", key, value))
587+
588+
monkeypatch.setattr(
589+
remote_job_status_service.job_service,
590+
"job_update_job_data_insert_key_value",
591+
fake_update_kv,
592+
)
593+
monkeypatch.setattr(remote_job_status_service.job_service, "job_update_status", AsyncMock())
594+
595+
result = await _check_job_via_provider(job, "exp-1", record, instance)
596+
assert result is True
597+
assert ("kv", "azure_stopping_unknown_polls", 0) in calls
598+
599+
525600
@pytest.mark.asyncio
526601
async def test_check_job_via_provider_local_still_running(monkeypatch):
527602
"""LOCAL provider cluster still UP → returns False."""

0 commit comments

Comments
 (0)