anyscale 0.26.68__py3-none-any.whl → 0.26.70__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anyscale/_private/anyscale_client/anyscale_client.py +67 -1
- anyscale/_private/anyscale_client/common.py +20 -1
- anyscale/_private/anyscale_client/fake_anyscale_client.py +77 -10
- anyscale/client/README.md +16 -4
- anyscale/client/openapi_client/__init__.py +12 -4
- anyscale/client/openapi_client/api/default_api.py +588 -23
- anyscale/client/openapi_client/models/__init__.py +12 -4
- anyscale/client/openapi_client/models/api_key_info.py +29 -3
- anyscale/client/openapi_client/models/apply_autoscaling_config_update_model.py +350 -0
- anyscale/client/openapi_client/models/apply_production_service_multi_version_v2_model.py +207 -0
- anyscale/client/openapi_client/models/apply_production_service_v2_model.py +31 -3
- anyscale/client/openapi_client/models/baseimagesenum.py +70 -1
- anyscale/client/openapi_client/models/cloud_data_bucket_file_type.py +2 -1
- anyscale/client/openapi_client/models/{oauthconnectionresponse_response.py → clouddeployment_response.py} +11 -11
- anyscale/client/openapi_client/models/clusterdashboardnode_response.py +121 -0
- anyscale/client/openapi_client/models/create_experimental_workspace.py +29 -1
- anyscale/client/openapi_client/models/create_workspace_from_template.py +29 -1
- anyscale/client/openapi_client/models/create_workspace_template_version.py +31 -3
- anyscale/client/openapi_client/models/decorated_list_service_api_model.py +58 -1
- anyscale/client/openapi_client/models/decorated_production_service_v2_api_model.py +60 -3
- anyscale/client/openapi_client/models/decorated_service_event_api_model.py +3 -3
- anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +33 -5
- anyscale/client/openapi_client/models/describe_machine_pool_workloads_filters.py +33 -5
- anyscale/client/openapi_client/models/{service_event_level.py → entity_type.py} +9 -9
- anyscale/client/openapi_client/models/event_level.py +2 -1
- anyscale/client/openapi_client/models/job_event_fields.py +206 -0
- anyscale/client/openapi_client/models/lineage_graph_node.py +70 -42
- anyscale/client/openapi_client/models/lineage_workload.py +31 -3
- anyscale/client/openapi_client/models/machine_type_partition_filter.py +152 -0
- anyscale/client/openapi_client/models/partition_info.py +30 -1
- anyscale/client/openapi_client/models/production_job_event.py +3 -3
- anyscale/client/openapi_client/models/rollout_strategy.py +2 -1
- anyscale/client/openapi_client/models/service_event_fields.py +318 -0
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +70 -1
- anyscale/client/openapi_client/models/task_summary_config.py +29 -3
- anyscale/client/openapi_client/models/task_table_config.py +29 -3
- anyscale/client/openapi_client/models/unified_event.py +377 -0
- anyscale/client/openapi_client/models/{ha_job_event_level.py → unified_origin_filter.py} +21 -9
- anyscale/client/openapi_client/models/unifiedevent_list_response.py +147 -0
- anyscale/client/openapi_client/models/workspace_event_fields.py +122 -0
- anyscale/client/openapi_client/models/workspace_template_version.py +30 -1
- anyscale/client/openapi_client/models/workspace_template_version_data_object.py +30 -1
- anyscale/cloud/models.py +2 -2
- anyscale/commands/cloud_commands.py +148 -11
- anyscale/commands/command_examples.py +53 -0
- anyscale/commands/job_commands.py +1 -1
- anyscale/commands/service_commands.py +130 -67
- anyscale/commands/setup_k8s.py +615 -49
- anyscale/controllers/cloud_controller.py +19 -5
- anyscale/controllers/kubernetes_verifier.py +80 -66
- anyscale/job/_private/job_sdk.py +47 -1
- anyscale/job/commands.py +3 -0
- anyscale/sdk/anyscale_client/models/apply_production_service_v2_model.py +31 -3
- anyscale/sdk/anyscale_client/models/apply_service_model.py +31 -3
- anyscale/sdk/anyscale_client/models/baseimagesenum.py +70 -1
- anyscale/sdk/anyscale_client/models/rollout_strategy.py +2 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +70 -1
- anyscale/service/__init__.py +11 -3
- anyscale/service/_private/service_sdk.py +361 -35
- anyscale/service/commands.py +15 -3
- anyscale/service/models.py +12 -0
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/version.py +1 -1
- {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/METADATA +1 -1
- {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/RECORD +70 -62
- anyscale/client/openapi_client/models/o_auth_connection_response.py +0 -229
- {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/WHEEL +0 -0
- {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/licenses/LICENSE +0 -0
- {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/licenses/NOTICE +0 -0
- {anyscale-0.26.68.dist-info → anyscale-0.26.70.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import copy
|
|
3
|
-
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
4
5
|
|
|
5
6
|
from anyscale._private.models.model_base import ResultIterator
|
|
6
7
|
from anyscale._private.workload import WorkloadSDK
|
|
8
|
+
from anyscale.client.openapi_client.models.apply_production_service_multi_version_v2_model import (
|
|
9
|
+
ApplyProductionServiceMultiVersionV2Model,
|
|
10
|
+
)
|
|
7
11
|
from anyscale.client.openapi_client.models.decorated_production_service_v2_api_model import (
|
|
8
12
|
DecoratedProductionServiceV2APIModel,
|
|
9
13
|
)
|
|
14
|
+
from anyscale.client.openapi_client.models.decorated_production_service_v2_version_api_model import (
|
|
15
|
+
DecoratedProductionServiceV2VersionAPIModel,
|
|
16
|
+
)
|
|
10
17
|
from anyscale.client.openapi_client.models.decoratedlistserviceapimodel_list_response import (
|
|
11
18
|
DecoratedlistserviceapimodelListResponse,
|
|
12
19
|
)
|
|
@@ -21,6 +28,7 @@ from anyscale.sdk.anyscale_client.models import (
|
|
|
21
28
|
ProductionServiceV2VersionModel,
|
|
22
29
|
Protocols,
|
|
23
30
|
RayGCSExternalStorageConfig as APIRayGCSExternalStorageConfig,
|
|
31
|
+
RolloutStrategy,
|
|
24
32
|
ServiceConfig as ExternalAPIServiceConfig,
|
|
25
33
|
ServiceEventCurrentState,
|
|
26
34
|
ServiceSortField,
|
|
@@ -124,16 +132,21 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
124
132
|
canary_percent: Optional[int],
|
|
125
133
|
):
|
|
126
134
|
"""Log user-facing information about a deployed service."""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
service.
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
if not service.primary_version:
|
|
136
|
+
version_info = "versions: {versions}".format(
|
|
137
|
+
versions=[v.version for v in service.versions]
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
version_info = "version ID: {version_id}".format(
|
|
141
|
+
version_id=self._get_user_facing_service_version_id(
|
|
142
|
+
service.canary_version
|
|
143
|
+
if service.canary_version is not None
|
|
144
|
+
else service.primary_version
|
|
145
|
+
)
|
|
132
146
|
)
|
|
133
|
-
)
|
|
134
147
|
details = (
|
|
135
148
|
"("
|
|
136
|
-
+
|
|
149
|
+
+ version_info
|
|
137
150
|
+ (
|
|
138
151
|
")"
|
|
139
152
|
if canary_percent is None
|
|
@@ -270,6 +283,7 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
270
283
|
)
|
|
271
284
|
return ApplyProductionServiceV2Model(
|
|
272
285
|
name=name,
|
|
286
|
+
version=config.version_name,
|
|
273
287
|
project_id=project_id,
|
|
274
288
|
ray_serve_config=self._build_ray_serve_config(config),
|
|
275
289
|
build_id=existing_config.build_id,
|
|
@@ -288,10 +302,12 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
288
302
|
self,
|
|
289
303
|
name: str,
|
|
290
304
|
config: ServiceConfig,
|
|
305
|
+
rollout_strategy: RolloutStrategy,
|
|
291
306
|
*,
|
|
292
307
|
canary_percent: Optional[int] = None,
|
|
293
308
|
max_surge_percent: Optional[int] = None,
|
|
294
309
|
existing_service: Optional[DecoratedProductionServiceV2APIModel] = None,
|
|
310
|
+
traffic_percent: Optional[int] = None,
|
|
295
311
|
) -> ApplyProductionServiceV2Model:
|
|
296
312
|
"""Build the ApplyProductionServiceV2Model for a rolling update."""
|
|
297
313
|
|
|
@@ -399,13 +415,14 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
399
415
|
|
|
400
416
|
return ApplyProductionServiceV2Model(
|
|
401
417
|
name=name,
|
|
418
|
+
version=config.version_name,
|
|
402
419
|
project_id=project_id,
|
|
403
420
|
ray_serve_config=self._build_ray_serve_config(config),
|
|
404
421
|
build_id=build_id,
|
|
405
422
|
compute_config_id=compute_config_id,
|
|
406
423
|
canary_percent=canary_percent,
|
|
407
424
|
max_surge_percent=max_surge_percent,
|
|
408
|
-
rollout_strategy=
|
|
425
|
+
rollout_strategy=rollout_strategy,
|
|
409
426
|
config=ExternalAPIServiceConfig(
|
|
410
427
|
access=AccessConfig(use_bearer_token=config.query_auth_token_enabled),
|
|
411
428
|
protocols=Protocols(grpc=self._build_grpc_protocol_config(config)),
|
|
@@ -413,36 +430,319 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
413
430
|
),
|
|
414
431
|
ray_gcs_external_storage_config=ray_gcs_external_storage_config,
|
|
415
432
|
tracing_config=tracing_config,
|
|
433
|
+
traffic_percent=traffic_percent,
|
|
416
434
|
)
|
|
417
435
|
|
|
418
|
-
def
|
|
436
|
+
def _build_apply_service_model_for_multi_version(
|
|
419
437
|
self,
|
|
420
|
-
|
|
438
|
+
name: str,
|
|
439
|
+
cloud: Optional[str],
|
|
440
|
+
project: Optional[str],
|
|
441
|
+
configs: List[ServiceConfig],
|
|
442
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel],
|
|
443
|
+
versions: List[Dict[str, Any]],
|
|
444
|
+
) -> ApplyProductionServiceV2Model:
|
|
445
|
+
cloud_id = self.client.get_cloud_id(cloud_name=cloud)
|
|
446
|
+
project_id = self.client.get_project_id(parent_cloud_id=cloud_id, name=project)
|
|
447
|
+
|
|
448
|
+
# implement a map between version name and config file
|
|
449
|
+
# This way, we can iterate over the version config and corresponding config file to the _build_apply_service_model_for_rollout function
|
|
450
|
+
# Also, maybe add an assertion to confirm the version name is the same as the config file name.
|
|
451
|
+
# I also need to add a validation logic to confirm if all the version config passed(the version name) all has the corresponding config file.
|
|
452
|
+
version_service_map = self._get_service_version_map(configs, versions, existing_service) # type: ignore
|
|
453
|
+
existing_versions: Dict[str, DecoratedProductionServiceV2VersionAPIModel] = {}
|
|
454
|
+
if existing_service is not None:
|
|
455
|
+
existing_versions = {
|
|
456
|
+
v.version: v
|
|
457
|
+
for v in self.client.get_service_versions(existing_service.id)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
service_versions: List[ApplyProductionServiceV2Model] = []
|
|
461
|
+
for _, (config, version) in version_service_map.items():
|
|
462
|
+
version_name = version["name"]
|
|
463
|
+
traffic_percent = version["traffic_percent"]
|
|
464
|
+
# If config is None, it means the version is already deployed, and the user is likely to be only updating the traffic/capacity percent without serve config update.
|
|
465
|
+
if config is None:
|
|
466
|
+
if version_name not in existing_versions:
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"Version {version_name} does not exist in the existing service."
|
|
469
|
+
)
|
|
470
|
+
existing_version = existing_versions[version_name]
|
|
471
|
+
|
|
472
|
+
service_version = ApplyProductionServiceV2Model(
|
|
473
|
+
name=name,
|
|
474
|
+
project_id=project_id,
|
|
475
|
+
ray_serve_config=existing_version.ray_serve_config,
|
|
476
|
+
build_id=existing_version.build_id,
|
|
477
|
+
compute_config_id=existing_version.compute_config_id,
|
|
478
|
+
rollout_strategy=RolloutStrategy.MULTI_VERSION,
|
|
479
|
+
# TODO(doyoung): "config" is a service level config, so I'm assuming this is not necessary when this model is
|
|
480
|
+
# representing a service version. If not, may need to add and store 'config' field in the version model, ProductionServiceV2VersionModel,
|
|
481
|
+
# from the backend when the service version first gets created so it can be read from here for reconstruction.
|
|
482
|
+
config=None,
|
|
483
|
+
ray_gcs_external_storage_config=existing_version.ray_gcs_external_storage_config,
|
|
484
|
+
tracing_config=existing_version.tracing_config,
|
|
485
|
+
version=version_name,
|
|
486
|
+
traffic_percent=traffic_percent,
|
|
487
|
+
)
|
|
488
|
+
else:
|
|
489
|
+
service_version = self._build_apply_service_model_for_rollout(
|
|
490
|
+
name,
|
|
491
|
+
config,
|
|
492
|
+
RolloutStrategy.MULTI_VERSION,
|
|
493
|
+
existing_service=existing_service,
|
|
494
|
+
traffic_percent=traffic_percent,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
service_versions.append(service_version)
|
|
498
|
+
|
|
499
|
+
assert len(service_versions) > 0, "service_versions should not be empty"
|
|
500
|
+
return ApplyProductionServiceMultiVersionV2Model(
|
|
501
|
+
rollout_strategy=RolloutStrategy.MULTI_VERSION,
|
|
502
|
+
cloud_id=cloud_id,
|
|
503
|
+
project_id=project_id,
|
|
504
|
+
service_versions=service_versions,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def _validate_versions(
|
|
508
|
+
self, versions: str
|
|
509
|
+
) -> List[Dict[str, Any]]: # noqa: C901, PLR0912
|
|
510
|
+
# Parse versions JSON string to list of dictionaries
|
|
511
|
+
try:
|
|
512
|
+
versions = json.loads(versions)
|
|
513
|
+
except json.JSONDecodeError as e:
|
|
514
|
+
raise ValueError(f"Invalid JSON format for --versions: {e}")
|
|
515
|
+
|
|
516
|
+
for version in versions:
|
|
517
|
+
# traffic_percent must be specified in all versions by the users.
|
|
518
|
+
# If some version does not specify traffic_percent, raise an error.
|
|
519
|
+
if "name" not in version:
|
|
520
|
+
raise ValueError("Name is required for each version.")
|
|
521
|
+
if "traffic_percent" not in version:
|
|
522
|
+
raise ValueError("Traffic percent is required for each version.")
|
|
523
|
+
|
|
524
|
+
# Verify the key names are valid. Only either of name, traffic_percent, or capacity_percent are allowed.
|
|
525
|
+
for key in version:
|
|
526
|
+
if key not in [
|
|
527
|
+
"name",
|
|
528
|
+
"traffic_percent",
|
|
529
|
+
]:
|
|
530
|
+
raise ValueError(f"Invalid key in versions: {key}")
|
|
531
|
+
|
|
532
|
+
if isinstance(versions, dict):
|
|
533
|
+
# Convert object form {"v1": 25, "v2": 75} to full list form
|
|
534
|
+
versions = [ # type: ignore
|
|
535
|
+
{"name": name, "traffic_percent": percent}
|
|
536
|
+
for name, percent in versions.items()
|
|
537
|
+
]
|
|
538
|
+
elif not isinstance(versions, list):
|
|
539
|
+
raise ValueError(
|
|
540
|
+
"--versions must be a JSON array of objects or a JSON object in text format."
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Verify sum of each traffic_percent from each version is 100.
|
|
544
|
+
traffic_percent_sum = sum(version["traffic_percent"] for version in versions)
|
|
545
|
+
if traffic_percent_sum != 100:
|
|
546
|
+
raise ValueError("Sum of traffic percent for all versions must be 100.")
|
|
547
|
+
|
|
548
|
+
return versions # type: ignore
|
|
549
|
+
|
|
550
|
+
def _read_and_validate_configs(
|
|
551
|
+
self, configs: List[ServiceConfig]
|
|
552
|
+
) -> Tuple[str, Optional[str], Optional[str]]:
|
|
553
|
+
"""The service name, cloud, and project must be identical for all versions."""
|
|
554
|
+
|
|
555
|
+
# If all the name field from configs are identical, use the first one as the name.
|
|
556
|
+
# If all the name field from configs are not provided, set to None, use the default name.
|
|
557
|
+
if all(config.name is None for config in configs):
|
|
558
|
+
name = self._get_default_name()
|
|
559
|
+
elif all(config.name == configs[0].name for config in configs):
|
|
560
|
+
name = configs[0].name
|
|
561
|
+
else:
|
|
562
|
+
raise ValueError("All provided names in config files must be identical.")
|
|
563
|
+
|
|
564
|
+
# If all the cloud field from configs are identical, use the first one as the cloud.
|
|
565
|
+
# If all the cloud field from configs are all set to None, set cloud to None. Default cloud will be used from backend.
|
|
566
|
+
if all(config.cloud is None for config in configs):
|
|
567
|
+
cloud = None
|
|
568
|
+
elif all(config.cloud == configs[0].cloud for config in configs):
|
|
569
|
+
cloud = configs[0].cloud
|
|
570
|
+
else:
|
|
571
|
+
raise ValueError("All provided clouds in config files must be identical.")
|
|
572
|
+
|
|
573
|
+
# If all the project field from configs are identical, use the first one as the project.
|
|
574
|
+
# If all the project field from configs are all set to None, set project to None. Default project will be used from backend.
|
|
575
|
+
if all(config.project is None for config in configs):
|
|
576
|
+
project = None
|
|
577
|
+
elif all(config.project == configs[0].project for config in configs):
|
|
578
|
+
project = configs[0].project
|
|
579
|
+
else:
|
|
580
|
+
raise ValueError("All provided projects in config files must be identical.")
|
|
581
|
+
|
|
582
|
+
return name, cloud, project
|
|
583
|
+
|
|
584
|
+
def _get_service_version_map(
|
|
585
|
+
self,
|
|
586
|
+
configs: List[ServiceConfig],
|
|
587
|
+
versions: Optional[List[Dict[str, Any]]],
|
|
588
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel],
|
|
589
|
+
) -> Dict[str, Tuple[ServiceConfig, Dict[str, Any]]]:
|
|
590
|
+
service_config_to_version_config = {}
|
|
591
|
+
if versions is not None:
|
|
592
|
+
if len(configs) > len(versions):
|
|
593
|
+
raise ValueError(
|
|
594
|
+
"Number of config files passed must be less than or equal to the number of version configs when multiple versions are being deployed."
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
for service_config in configs:
|
|
598
|
+
for version_config in versions:
|
|
599
|
+
if service_config.version_name == version_config["name"]:
|
|
600
|
+
service_config_to_version_config[version_config["name"]] = (
|
|
601
|
+
service_config,
|
|
602
|
+
version_config,
|
|
603
|
+
)
|
|
604
|
+
break
|
|
605
|
+
|
|
606
|
+
# version name in the ServiceConfig must have a matching version name in the version config.
|
|
607
|
+
# If this point is reached, then, it means there's a config file passed with version name that doesn't exist in the version config.
|
|
608
|
+
if service_config.version_name not in service_config_to_version_config:
|
|
609
|
+
raise ValueError(
|
|
610
|
+
f"version_name {service_config.version_name!r} in service config {service_config} does not match any of the version names provided with --versions."
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
# version name in version config doesn't need to have a matching version name in service config. If there's not a matching, this
|
|
614
|
+
# means the version is already deployed, and the user is likely to be only updating the traffic/capacity percent.
|
|
615
|
+
for version_config in versions:
|
|
616
|
+
version_name = version_config["name"]
|
|
617
|
+
if version_name not in service_config_to_version_config:
|
|
618
|
+
if existing_service is None:
|
|
619
|
+
raise ValueError(
|
|
620
|
+
f"--config-file corresponding to version {version_name!r} must be provided if the Service is not already deployed."
|
|
621
|
+
)
|
|
622
|
+
# TODO(doyoung): Perhaps add a validaiton logic to confirm if the version name already exists in the existing service through existing_service.versions.
|
|
623
|
+
service_config_to_version_config[version_name] = (
|
|
624
|
+
None,
|
|
625
|
+
version_config,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
return service_config_to_version_config
|
|
629
|
+
|
|
630
|
+
def _get_rollout_strategy( # noqa: PLR0912
|
|
631
|
+
self,
|
|
632
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel],
|
|
633
|
+
version_weights: Optional[List[Dict[str, Any]]],
|
|
634
|
+
in_place: bool,
|
|
635
|
+
configs: List[ServiceConfig],
|
|
636
|
+
) -> RolloutStrategy:
|
|
637
|
+
"""Determine the rollout strategy to use.
|
|
638
|
+
|
|
639
|
+
If `in_place` is True, return RolloutStrategy.IN_PLACE.
|
|
640
|
+
If `version_weights` is None or if the service is going from 0 or 1 versions to 1 version, return RolloutStrategy.ROLLOUT.
|
|
641
|
+
Otherwise, return RolloutStrategy.MULTI_VERSION.
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
active_versions = len(existing_service.versions) if existing_service else 0
|
|
645
|
+
|
|
646
|
+
if in_place:
|
|
647
|
+
if version_weights is not None:
|
|
648
|
+
raise ValueError(
|
|
649
|
+
"In-place updates are not supported for multi-version services."
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
return RolloutStrategy.IN_PLACE
|
|
653
|
+
elif version_weights is None or (
|
|
654
|
+
len(version_weights) == 1 and active_versions <= 1
|
|
655
|
+
):
|
|
656
|
+
if existing_service is None and len(configs) == 0:
|
|
657
|
+
raise ValueError(
|
|
658
|
+
"A config file must be provided when deploying a new service."
|
|
659
|
+
)
|
|
660
|
+
if version_weights is not None:
|
|
661
|
+
self.logger.warning(
|
|
662
|
+
"Defaulting to ROLLOUT strategy for single version service. Ignoring --versions."
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
return RolloutStrategy.ROLLOUT
|
|
666
|
+
else:
|
|
667
|
+
return RolloutStrategy.MULTI_VERSION
|
|
668
|
+
|
|
669
|
+
def deploy( # noqa: PLR0912, C901
|
|
670
|
+
self,
|
|
671
|
+
configs: Union[ServiceConfig, List[ServiceConfig]],
|
|
421
672
|
*,
|
|
422
673
|
in_place: bool = False,
|
|
423
674
|
canary_percent: Optional[int] = None,
|
|
424
675
|
max_surge_percent: Optional[int] = None,
|
|
676
|
+
versions: Optional[str] = None,
|
|
677
|
+
name: Optional[str] = None,
|
|
678
|
+
cloud: Optional[str] = None,
|
|
679
|
+
project: Optional[str] = None,
|
|
425
680
|
) -> str:
|
|
426
|
-
|
|
427
|
-
raise TypeError("in_place must be a bool.")
|
|
681
|
+
"""Deploy or update a service.
|
|
428
682
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
683
|
+
Args:
|
|
684
|
+
configs: Service configuration(s) to deploy. Can be a single ServiceConfig or a list of ServiceConfig objects for multi-version deployments.
|
|
685
|
+
versions: JSON string specifying version configurations for multi-version deployments.
|
|
686
|
+
name: Name of the service. If not provided, defaults to workspace cluster name.
|
|
687
|
+
cloud: Cloud provider to deploy to.
|
|
688
|
+
project: Project to deploy the service in.
|
|
434
689
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
690
|
+
Returns:
|
|
691
|
+
The service ID.
|
|
692
|
+
"""
|
|
693
|
+
if versions is not None:
|
|
694
|
+
versions = self._validate_versions(versions) # type: ignore
|
|
440
695
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
696
|
+
if isinstance(configs, ServiceConfig):
|
|
697
|
+
if versions is not None:
|
|
698
|
+
raise ValueError(
|
|
699
|
+
"To deploy with --versions, a list of ServiceConfig must be provided."
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if not isinstance(in_place, bool):
|
|
703
|
+
raise TypeError("in_place must be a bool.")
|
|
704
|
+
|
|
705
|
+
if canary_percent is not None:
|
|
706
|
+
if not isinstance(canary_percent, int):
|
|
707
|
+
raise TypeError("canary_percent must be an int.")
|
|
708
|
+
if canary_percent < 0 or canary_percent > 100:
|
|
709
|
+
raise ValueError("canary_percent must be between 0 and 100.")
|
|
710
|
+
|
|
711
|
+
if max_surge_percent is not None:
|
|
712
|
+
if not isinstance(max_surge_percent, int):
|
|
713
|
+
raise TypeError("max_surge_percent must be an int.")
|
|
714
|
+
if max_surge_percent < 0 or max_surge_percent > 100:
|
|
715
|
+
raise ValueError("max_surge_percent must be between 0 and 100.")
|
|
716
|
+
|
|
717
|
+
name = configs.name
|
|
718
|
+
cloud = configs.cloud
|
|
719
|
+
project = configs.project
|
|
720
|
+
configs = [configs]
|
|
721
|
+
|
|
722
|
+
elif isinstance(configs, List):
|
|
723
|
+
if versions is None:
|
|
724
|
+
raise ValueError(
|
|
725
|
+
"--versions must be provided when deploying with multiple config files."
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
if len(configs) == 0:
|
|
729
|
+
if name is None:
|
|
730
|
+
raise ValueError(
|
|
731
|
+
"When using --versions, a service name must be provided if no config files are given."
|
|
732
|
+
)
|
|
733
|
+
else:
|
|
734
|
+
name, cloud, project = self._read_and_validate_configs(configs)
|
|
735
|
+
|
|
736
|
+
else:
|
|
737
|
+
ValueError(
|
|
738
|
+
"Single object of ServiceConfig or list of ServiceConfig must be provided."
|
|
445
739
|
)
|
|
740
|
+
|
|
741
|
+
if name is None:
|
|
742
|
+
name = self._get_default_name()
|
|
743
|
+
|
|
744
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel] = (
|
|
745
|
+
self.client.get_service(name=name, cloud=cloud, project=project)
|
|
446
746
|
)
|
|
447
747
|
if existing_service is None:
|
|
448
748
|
self.logger.info(f"Starting new service '{name}'.")
|
|
@@ -451,37 +751,63 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
451
751
|
else:
|
|
452
752
|
self.logger.info(f"Updating existing service '{name}'.")
|
|
453
753
|
|
|
454
|
-
# passed canary_percent is ignored when creating or restarting a service
|
|
754
|
+
# passed canary_percent is ignored when creating or restarting a service or multiple versions are being deployed.
|
|
455
755
|
is_new_or_restarting = (
|
|
456
756
|
existing_service is None
|
|
457
757
|
or existing_service.current_state == ServiceEventCurrentState.TERMINATED
|
|
458
758
|
)
|
|
459
|
-
if canary_percent is not None and
|
|
759
|
+
if canary_percent is not None and (
|
|
760
|
+
is_new_or_restarting or versions is not None
|
|
761
|
+
):
|
|
460
762
|
canary_percent = None
|
|
461
763
|
self.logger.warning(
|
|
462
|
-
"canary_percent is ignored when creating or restarting a service."
|
|
764
|
+
"canary_percent is ignored when creating or restarting a service or multiple versions are being deployed."
|
|
463
765
|
)
|
|
464
766
|
|
|
465
|
-
|
|
767
|
+
rollout_strategy = self._get_rollout_strategy(
|
|
768
|
+
existing_service, versions, in_place, configs # type: ignore
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
service: DecoratedProductionServiceV2APIModel
|
|
772
|
+
# TODO(doyoung): Support multi version deployment for in_place update.
|
|
773
|
+
if rollout_strategy == RolloutStrategy.IN_PLACE:
|
|
774
|
+
if versions is not None:
|
|
775
|
+
raise ValueError(
|
|
776
|
+
"versions cannot be specified when doing an in_place update."
|
|
777
|
+
)
|
|
778
|
+
assert len(configs) == 1
|
|
466
779
|
model = self._build_apply_service_model_for_in_place_update(
|
|
467
780
|
name,
|
|
468
|
-
|
|
781
|
+
configs[0],
|
|
469
782
|
canary_percent=canary_percent,
|
|
470
783
|
max_surge_percent=max_surge_percent,
|
|
471
784
|
existing_service=existing_service,
|
|
472
785
|
)
|
|
786
|
+
service = self.client.rollout_service(model)
|
|
787
|
+
elif rollout_strategy == RolloutStrategy.MULTI_VERSION:
|
|
788
|
+
model = self._build_apply_service_model_for_multi_version(
|
|
789
|
+
name,
|
|
790
|
+
cloud,
|
|
791
|
+
project,
|
|
792
|
+
configs,
|
|
793
|
+
existing_service=existing_service,
|
|
794
|
+
versions=versions, # type: ignore
|
|
795
|
+
)
|
|
796
|
+
service = self.client.rollout_service_multi_version(model)
|
|
473
797
|
else:
|
|
798
|
+
# rolling out a single version in new service without specifying versions.
|
|
799
|
+
assert len(configs) == 1
|
|
474
800
|
model = self._build_apply_service_model_for_rollout(
|
|
475
801
|
name,
|
|
476
|
-
|
|
802
|
+
configs[0],
|
|
803
|
+
RolloutStrategy.ROLLOUT,
|
|
477
804
|
canary_percent=canary_percent,
|
|
478
805
|
max_surge_percent=max_surge_percent,
|
|
479
806
|
existing_service=existing_service,
|
|
480
807
|
)
|
|
808
|
+
service = self.client.rollout_service(model)
|
|
481
809
|
|
|
482
|
-
service = self.client.rollout_service(model)
|
|
483
810
|
self._log_deployed_service_info(service, canary_percent=canary_percent)
|
|
484
|
-
|
|
485
811
|
return service.id
|
|
486
812
|
|
|
487
813
|
def _resolve_to_service_model(
|
anyscale/service/commands.py
CHANGED
|
@@ -32,10 +32,14 @@ anyscale.service.deploy(
|
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
34
|
_DEPLOY_ARG_DOCSTRINGS = {
|
|
35
|
-
"
|
|
35
|
+
"configs": "The config options defining the service.",
|
|
36
36
|
"in_place": "Perform an in-place upgrade without starting a new cluster. This can be used for faster iteration during development but is *not* currently recommended for production deploys. This *cannot* be used to change cluster-level options such as image and compute config (they will be ignored).",
|
|
37
37
|
"canary_percent": "The percentage of traffic to send to the canary version of the service (0-100). This can be used to manually shift traffic toward (or away from) the canary version. If not provided, traffic will be shifted incrementally toward the canary version until it reaches 100. Not supported when using --in-place. This is ignored when restarting a service or creating a new service.",
|
|
38
38
|
"max_surge_percent": "Amount of excess capacity allowed to be used while updating the service (0-100). Defaults to 100. Not supported when using --in-place.",
|
|
39
|
+
"versions": "Enable multi-version deployment by providing a JSON array of objects or a JSON object in text format. Defines the version name, traffic and capacity percents per version. Capacity defaults to traffic.",
|
|
40
|
+
"name": "Name of the service. When running in a workspace, this defaults to the workspace name.",
|
|
41
|
+
"cloud": "The Anyscale Cloud of this workload. If not provided, the organization default will be used (or, if running in a workspace, the cloud of the workspace).",
|
|
42
|
+
"project": "Named project to use for the service. If not provided, the default project for the cloud will be used (or, if running in a workspace, the project of the workspace).",
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
|
|
@@ -46,11 +50,15 @@ _DEPLOY_ARG_DOCSTRINGS = {
|
|
|
46
50
|
arg_docstrings=_DEPLOY_ARG_DOCSTRINGS,
|
|
47
51
|
)
|
|
48
52
|
def deploy(
|
|
49
|
-
|
|
53
|
+
configs: Union[ServiceConfig, List[ServiceConfig]],
|
|
50
54
|
*,
|
|
51
55
|
in_place: bool = False,
|
|
52
56
|
canary_percent: Optional[int] = None,
|
|
53
57
|
max_surge_percent: Optional[int] = None,
|
|
58
|
+
versions: Optional[str] = None,
|
|
59
|
+
name: Optional[str] = None,
|
|
60
|
+
cloud: Optional[str] = None,
|
|
61
|
+
project: Optional[str] = None,
|
|
54
62
|
_private_sdk: Optional[PrivateServiceSDK] = None,
|
|
55
63
|
) -> str:
|
|
56
64
|
"""Deploy a service.
|
|
@@ -62,10 +70,14 @@ def deploy(
|
|
|
62
70
|
Returns the id of the deployed service.
|
|
63
71
|
"""
|
|
64
72
|
return _private_sdk.deploy( # type: ignore
|
|
65
|
-
|
|
73
|
+
configs=configs,
|
|
66
74
|
in_place=in_place,
|
|
67
75
|
canary_percent=canary_percent,
|
|
68
76
|
max_surge_percent=max_surge_percent,
|
|
77
|
+
versions=versions,
|
|
78
|
+
name=name,
|
|
79
|
+
cloud=cloud,
|
|
80
|
+
project=project,
|
|
69
81
|
)
|
|
70
82
|
|
|
71
83
|
|
anyscale/service/models.py
CHANGED
|
@@ -231,6 +231,18 @@ tracing_config: # (Optional) Configuration options for tracing.
|
|
|
231
231
|
},
|
|
232
232
|
)
|
|
233
233
|
|
|
234
|
+
version_name: Optional[str] = field(
|
|
235
|
+
default=None, metadata={"docstring": "Unique name of the version."},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _validate_version_name(self, version_name: Optional[str]):
|
|
239
|
+
# Allow None if optional; enforce non-empty string if required
|
|
240
|
+
if version_name is None:
|
|
241
|
+
return None
|
|
242
|
+
if not isinstance(version_name, str) or not version_name.strip():
|
|
243
|
+
raise ValueError("version_name must be a non-empty string")
|
|
244
|
+
return version_name
|
|
245
|
+
|
|
234
246
|
def _validate_applications(self, applications: List[Dict[str, Any]]):
|
|
235
247
|
if not isinstance(applications, list):
|
|
236
248
|
raise TypeError("'applications' must be a list.")
|
anyscale/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.26.
|
|
1
|
+
__version__ = "0.26.70"
|