Skip to content

Commit 04886ed

Browse files
ethanwharriscarmoccaawaelchli
authored
[App] Refactor cloud dispatch and update to new API (#16456)
Co-authored-by: Carlos Mocholí <[email protected]> Co-authored-by: Adrian Wälchli <[email protected]>
1 parent b30e948 commit 04886ed

File tree

10 files changed

+833
-668
lines changed

10 files changed

+833
-668
lines changed

requirements/app/base.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
lightning-cloud>=0.5.12, <=0.5.16
1+
lightning-cloud>=0.5.19
22
packaging
33
typing-extensions>=4.0.0, <=4.4.0
44
deepdiff>=5.7.0, <6.2.4

src/lightning_app/core/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def get_lightning_cloud_url() -> str:
4545
# Project under which the resources need to run in cloud. If this env is not set,
4646
# cloud runner will try to get the default project from the cloud
4747
LIGHTNING_CLOUD_PROJECT_ID = os.getenv("LIGHTNING_CLOUD_PROJECT_ID")
48+
LIGHTNING_CLOUD_PRINT_SPECS = os.getenv("LIGHTNING_CLOUD_PRINT_SPECS")
4849
LIGHTNING_DIR = os.getenv("LIGHTNING_DIR", str(Path.home() / ".lightning"))
4950
LIGHTNING_CREDENTIAL_PATH = os.getenv("LIGHTNING_CREDENTIAL_PATH", str(Path(LIGHTNING_DIR) / "credentials.json"))
5051
DOT_IGNORE_FILENAME = ".lightningignore"

src/lightning_app/runners/cloud.py

Lines changed: 584 additions & 470 deletions
Large diffs are not rendered by default.

src/lightning_app/source_code/copytree.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import partial
44
from pathlib import Path
55
from shutil import copy2, copystat, Error
6-
from typing import Callable, List, Optional, Set, Union
6+
from typing import Callable, List, Optional, Set, Tuple, Union
77

88
from lightning_app.core.constants import DOT_IGNORE_FILENAME
99
from lightning_app.utilities.app_helpers import Logger
@@ -122,7 +122,7 @@ def _filter_ignored(src: Path, patterns: Set[str], current_dir: Path, entries: L
122122
return [entry for entry in entries if str(relative_dir / entry.name) not in ignored_names]
123123

124124

125-
def _parse_lightningignore(lines: List[str]) -> Set[str]:
125+
def _parse_lightningignore(lines: Tuple[str]) -> Set[str]:
126126
"""Creates a set that removes empty lines and comments."""
127127
lines = [ln.strip() for ln in lines]
128128
# removes first `/` character for posix and `\\` for windows

src/lightning_app/source_code/local.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from shutil import rmtree
55
from typing import List, Optional
66

7+
from lightning_app.core.constants import DOT_IGNORE_FILENAME
78
from lightning_app.source_code.copytree import _copytree, _IGNORE_FUNCTION
89
from lightning_app.source_code.hashing import _get_hash
910
from lightning_app.source_code.tar import _tar_path
@@ -27,6 +28,14 @@ def __init__(self, path: Path, ignore_functions: Optional[List[_IGNORE_FUNCTION]
2728
if not self.cache_location.exists():
2829
self.cache_location.mkdir(parents=True, exist_ok=True)
2930

31+
# Create a default dotignore if it doesn't exist
32+
if not (path / DOT_IGNORE_FILENAME).is_file():
33+
with open(path / DOT_IGNORE_FILENAME, "w") as f:
34+
f.write("venv/\n")
35+
if (path / "bin" / "activate").is_file() or (path / "pyvenv.cfg").is_file():
36+
# the user is developing inside venv
37+
f.write("bin/\ninclude/\nlib/\npyvenv.cfg\n")
38+
3039
# clean old cache entries
3140
self._prune_cache()
3241

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import random
2+
3+
from lightning_cloud.openapi import V1ClusterType, V1ProjectClusterBinding
4+
from lightning_cloud.openapi.rest import ApiException
5+
6+
from lightning_app.utilities.network import LightningClient
7+
8+
9+
def _ensure_cluster_project_binding(client: LightningClient, project_id: str, cluster_id: str) -> None:
10+
cluster_bindings = client.projects_service_list_project_cluster_bindings(project_id=project_id)
11+
12+
for cluster_binding in cluster_bindings.clusters:
13+
if cluster_binding.cluster_id != cluster_id:
14+
continue
15+
if cluster_binding.project_id == project_id:
16+
return
17+
18+
client.projects_service_create_project_cluster_binding(
19+
project_id=project_id,
20+
body=V1ProjectClusterBinding(cluster_id=cluster_id, project_id=project_id),
21+
)
22+
23+
24+
def _get_default_cluster(client: LightningClient, project_id: str) -> str:
25+
"""This utility implements a minimal version of the cluster selection logic used in the cloud.
26+
27+
TODO: This should be requested directly from the platform.
28+
"""
29+
cluster_bindings = client.projects_service_list_project_cluster_bindings(project_id=project_id).clusters
30+
31+
if not cluster_bindings:
32+
raise ValueError(f"No clusters are bound to the project {project_id}.")
33+
34+
if len(cluster_bindings) == 1:
35+
return cluster_bindings[0].cluster_id
36+
37+
clusters = []
38+
for cluster_binding in cluster_bindings:
39+
try:
40+
clusters.append(client.cluster_service_get_cluster(cluster_binding.cluster_id))
41+
except ApiException:
42+
# If we failed to get the cluster, ignore it
43+
continue
44+
45+
# Filter global clusters
46+
clusters = [cluster for cluster in clusters if cluster.spec.cluster_type == V1ClusterType.GLOBAL]
47+
48+
if len(clusters) == 0:
49+
raise RuntimeError(f"No clusters found on `{client.api_client.configuration.host}`.")
50+
51+
return random.choice(clusters).id

tests/integrations_app/public/test_commands_and_api.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,11 @@ def test_commands_and_api_example_cloud() -> None:
1818
admin_page,
1919
_,
2020
fetch_logs,
21-
_,
21+
app_name,
2222
):
23-
# 1: Collect the app_id
24-
app_id = admin_page.url.split("/")[-1]
25-
26-
# 2: Connect to the App and send the first & second command with the client
23+
# Connect to the App and send the first & second command with the client
2724
# Requires to be run within the same process.
28-
cmd_1 = f"python -m lightning connect {app_id}"
25+
cmd_1 = f"python -m lightning connect {app_name}"
2926
cmd_2 = "python -m lightning command with client --name=this"
3027
cmd_3 = "python -m lightning command without client --name=is"
3128
cmd_4 = "lightning disconnect"
@@ -35,32 +32,32 @@ def test_commands_and_api_example_cloud() -> None:
3532
# This prevents some flakyness in the CI. Couldn't reproduce it locally.
3633
sleep(5)
3734

38-
# 5: Send a request to the Rest API directly.
35+
# Send a request to the Rest API directly.
3936
client = LightningClient()
4037
project = _get_project(client)
4138

4239
lit_apps = [
43-
app
44-
for app in client.lightningapp_instance_service_list_lightningapp_instances(
45-
project_id=project.project_id
40+
lit_app
41+
for lit_app in client.lightningapp_instance_service_list_lightningapp_instances(
42+
project_id=project.project_id,
4643
).lightningapps
47-
if app.id == app_id
44+
if lit_app.name == app_name
4845
]
4946
app = lit_apps[0]
5047

5148
base_url = app.status.url
5249
resp = requests.post(base_url + "/user/command_without_client?name=awesome")
5350
assert resp.status_code == 200, resp.json()
5451

55-
# 6: Validate the logs.
52+
# Validate the logs.
5653
has_logs = False
5754
while not has_logs:
5855
for log in fetch_logs():
5956
if "['this', 'is', 'awesome']" in log:
6057
has_logs = True
6158
sleep(1)
6259

63-
# 7: Send a request to the Rest API directly.
60+
# Send a request to the Rest API directly.
6461
resp = requests.get(base_url + "/pure_function")
6562
assert resp.status_code == 200
6663
assert resp.json() == "Hello World !"

tests/tests_app/cli/test_cloud_cli.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import pytest
1010
from click.testing import CliRunner
1111
from lightning_cloud.openapi import (
12-
V1LightningappV2,
12+
V1CloudSpace,
13+
V1ListCloudSpacesResponse,
1314
V1ListLightningappInstancesResponse,
14-
V1ListLightningappsV2Response,
1515
V1ListMembershipsResponse,
1616
V1Membership,
1717
)
@@ -37,8 +37,8 @@ class FakeResponse:
3737

3838

3939
class FakeLightningClient:
40-
def lightningapp_v2_service_list_lightningapps_v2(self, *args, **kwargs):
41-
return V1ListLightningappsV2Response(lightningapps=[])
40+
def cloud_space_service_list_cloud_spaces(self, *args, **kwargs):
41+
return V1ListCloudSpacesResponse(cloudspaces=[])
4242

4343
def lightningapp_instance_service_list_lightningapp_instances(self, *args, **kwargs):
4444
return V1ListLightningappInstancesResponse(lightningapps=[])
@@ -105,14 +105,14 @@ def __init__(self, *args, create_response, **kwargs):
105105
super().__init__()
106106
self.create_response = create_response
107107

108-
def lightningapp_v2_service_create_lightningapp_v2(self, *args, **kwargs):
109-
return V1LightningappV2(id="my_app", name="app")
108+
def cloud_space_service_create_cloud_space(self, *args, **kwargs):
109+
return V1CloudSpace(id="my_app", name="app")
110110

111-
def lightningapp_v2_service_create_lightningapp_release(self, project_id, app_id, body):
111+
def cloud_space_service_create_lightning_run(self, project_id, cloudspace_id, body):
112112
assert project_id == "test-project-id"
113113
return self.create_response
114114

115-
def lightningapp_v2_service_create_lightningapp_release_instance(self, project_id, app_id, id, body):
115+
def cloud_space_service_create_lightning_run_instance(self, project_id, cloudspace_id, id, body):
116116
assert project_id == "test-project-id"
117117
return self.create_response
118118

@@ -123,7 +123,7 @@ def lightningapp_v2_service_create_lightningapp_release_instance(self, project_i
123123
def test_start_app(create_response, monkeypatch):
124124

125125
monkeypatch.setattr(cloud, "V1LightningappInstanceState", MagicMock())
126-
monkeypatch.setattr(cloud, "Body8", MagicMock())
126+
monkeypatch.setattr(cloud, "CloudspaceIdRunsBody", MagicMock())
127127
monkeypatch.setattr(cloud, "V1Flowserver", MagicMock())
128128
monkeypatch.setattr(cloud, "V1LightningappInstanceSpec", MagicMock())
129129
monkeypatch.setattr(
@@ -167,7 +167,7 @@ def run():
167167
flow_servers=ANY,
168168
)
169169

170-
cloud.Body8.assert_called_once()
170+
cloud.CloudspaceIdRunsBody.assert_called_once()
171171

172172

173173
class HttpHeaderDict(dict):
@@ -186,7 +186,7 @@ def __init__(self, *args, message, **kwargs):
186186
super().__init__()
187187
self.message = message
188188

189-
def lightningapp_v2_service_list_lightningapps_v2(self, *args, **kwargs):
189+
def cloud_space_service_list_cloud_spaces(self, *args, **kwargs):
190190
raise ApiException(
191191
http_resp=HttpHeaderDict(
192192
data=self.message,
@@ -207,7 +207,7 @@ def lightningapp_v2_service_list_lightningapps_v2(self, *args, **kwargs):
207207
def test_start_app_exception(message, monkeypatch, caplog):
208208

209209
monkeypatch.setattr(cloud, "V1LightningappInstanceState", MagicMock())
210-
monkeypatch.setattr(cloud, "Body8", MagicMock())
210+
monkeypatch.setattr(cloud, "CloudspaceIdRunsBody", MagicMock())
211211
monkeypatch.setattr(cloud, "V1Flowserver", MagicMock())
212212
monkeypatch.setattr(cloud, "V1LightningappInstanceSpec", MagicMock())
213213
monkeypatch.setattr(cloud, "LocalSourceCodeDir", MagicMock())

0 commit comments

Comments
 (0)