anyscale 0.26.69__py3-none-any.whl → 0.26.71__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 +126 -3
- anyscale/_private/anyscale_client/common.py +51 -2
- anyscale/_private/anyscale_client/fake_anyscale_client.py +103 -11
- anyscale/client/README.md +43 -4
- anyscale/client/openapi_client/__init__.py +30 -4
- anyscale/client/openapi_client/api/default_api.py +1769 -27
- anyscale/client/openapi_client/models/__init__.py +30 -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_multi_version_update_weights_update_model.py +152 -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/apply_version_weight_update_model.py +181 -0
- anyscale/client/openapi_client/models/backend_server_api_product_models_catalog_client_models_table_metadata.py +546 -0
- anyscale/client/openapi_client/models/backend_server_api_product_models_data_catalogs_table_metadata.py +178 -0
- anyscale/client/openapi_client/models/baseimagesenum.py +139 -1
- anyscale/client/openapi_client/models/catalog_metadata.py +150 -0
- 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/column_info.py +265 -0
- anyscale/client/openapi_client/models/compute_node_type.py +29 -1
- anyscale/client/openapi_client/models/connection_metadata.py +206 -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 +59 -3
- anyscale/client/openapi_client/models/data_catalog.py +45 -31
- anyscale/client/openapi_client/models/data_catalog_connection.py +74 -58
- anyscale/client/openapi_client/models/{ha_job_event_level.py → data_catalog_object_type.py} +7 -8
- anyscale/client/openapi_client/models/data_catalog_schema.py +324 -0
- anyscale/client/openapi_client/models/data_catalog_table.py +437 -0
- anyscale/client/openapi_client/models/data_catalog_volume.py +437 -0
- anyscale/client/openapi_client/models/datacatalogschema_list_response.py +147 -0
- anyscale/client/openapi_client/models/datacatalogtable_list_response.py +147 -0
- anyscale/client/openapi_client/models/datacatalogvolume_list_response.py +147 -0
- 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_serve_deployment.py +27 -1
- anyscale/client/openapi_client/models/decorated_service_event_api_model.py +3 -3
- anyscale/client/openapi_client/models/decoratedproductionservicev2_versionapimodel_response.py +121 -0
- anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +33 -5
- anyscale/client/openapi_client/models/describe_machine_pool_requests_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/machine_type_partition_filter.py +152 -0
- anyscale/client/openapi_client/models/partition_info.py +30 -1
- anyscale/client/openapi_client/models/physical_resources.py +178 -0
- 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/schema_metadata.py +150 -0
- anyscale/client/openapi_client/models/service_event_fields.py +318 -0
- anyscale/client/openapi_client/models/sso_config.py +18 -18
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +139 -1
- anyscale/client/openapi_client/models/table_data_preview.py +209 -0
- 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/unified_origin_filter.py +113 -0
- anyscale/client/openapi_client/models/unifiedevent_list_response.py +147 -0
- anyscale/client/openapi_client/models/volume_metadata.py +150 -0
- anyscale/client/openapi_client/models/worker_node_type.py +29 -1
- anyscale/client/openapi_client/models/workspace_event_fields.py +122 -0
- anyscale/client/openapi_client/models/workspace_template_version.py +58 -1
- anyscale/client/openapi_client/models/workspace_template_version_data_object.py +58 -1
- anyscale/cloud/models.py +2 -2
- anyscale/commands/cloud_commands.py +133 -2
- anyscale/commands/job_commands.py +121 -1
- anyscale/commands/job_queue_commands.py +99 -2
- anyscale/commands/service_commands.py +267 -67
- anyscale/commands/setup_k8s.py +546 -31
- anyscale/commands/util.py +104 -1
- anyscale/commands/workspace_commands.py +123 -5
- anyscale/commands/workspace_commands_v2.py +17 -1
- anyscale/compute_config/_private/compute_config_sdk.py +25 -12
- anyscale/compute_config/models.py +15 -0
- anyscale/controllers/cloud_controller.py +15 -2
- anyscale/controllers/job_controller.py +12 -0
- anyscale/controllers/kubernetes_verifier.py +80 -66
- anyscale/controllers/workspace_controller.py +67 -5
- anyscale/job/_private/job_sdk.py +50 -2
- anyscale/job/commands.py +3 -0
- anyscale/job/models.py +16 -0
- anyscale/job_queue/__init__.py +37 -1
- anyscale/job_queue/_private/job_queue_sdk.py +28 -1
- anyscale/job_queue/commands.py +61 -1
- anyscale/sdk/anyscale_client/__init__.py +1 -0
- anyscale/sdk/anyscale_client/api/default_api.py +12 -2
- anyscale/sdk/anyscale_client/models/__init__.py +1 -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 +139 -1
- anyscale/sdk/anyscale_client/models/compute_node_type.py +29 -1
- anyscale/sdk/anyscale_client/models/physical_resources.py +178 -0
- anyscale/sdk/anyscale_client/models/rollout_strategy.py +2 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +139 -1
- anyscale/sdk/anyscale_client/models/worker_node_type.py +29 -1
- anyscale/service/__init__.py +51 -3
- anyscale/service/_private/service_sdk.py +481 -58
- anyscale/service/commands.py +90 -4
- anyscale/service/models.py +56 -0
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/version.py +1 -1
- anyscale/workspace/_private/workspace_sdk.py +1 -0
- anyscale/workspace/models.py +19 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/METADATA +1 -1
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/RECORD +112 -85
- anyscale/client/openapi_client/models/o_auth_connection_response.py +0 -229
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/WHEEL +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/LICENSE +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/NOTICE +0 -0
- {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,28 @@
|
|
|
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
|
)
|
|
13
20
|
from anyscale.client.openapi_client.models.list_response_metadata import (
|
|
14
21
|
ListResponseMetadata,
|
|
15
22
|
)
|
|
23
|
+
from anyscale.client.openapi_client.models.resource_tag_resource_type import (
|
|
24
|
+
ResourceTagResourceType,
|
|
25
|
+
)
|
|
16
26
|
from anyscale.compute_config.models import ComputeConfig
|
|
17
27
|
from anyscale.sdk.anyscale_client.models import (
|
|
18
28
|
AccessConfig,
|
|
@@ -21,6 +31,7 @@ from anyscale.sdk.anyscale_client.models import (
|
|
|
21
31
|
ProductionServiceV2VersionModel,
|
|
22
32
|
Protocols,
|
|
23
33
|
RayGCSExternalStorageConfig as APIRayGCSExternalStorageConfig,
|
|
34
|
+
RolloutStrategy,
|
|
24
35
|
ServiceConfig as ExternalAPIServiceConfig,
|
|
25
36
|
ServiceEventCurrentState,
|
|
26
37
|
ServiceSortField,
|
|
@@ -124,16 +135,21 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
124
135
|
canary_percent: Optional[int],
|
|
125
136
|
):
|
|
126
137
|
"""Log user-facing information about a deployed service."""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
service.
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
if not service.primary_version:
|
|
139
|
+
version_info = "versions: {versions}".format(
|
|
140
|
+
versions=[v.version for v in service.versions]
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
version_info = "version ID: {version_id}".format(
|
|
144
|
+
version_id=self._get_user_facing_service_version_id(
|
|
145
|
+
service.canary_version
|
|
146
|
+
if service.canary_version is not None
|
|
147
|
+
else service.primary_version
|
|
148
|
+
)
|
|
132
149
|
)
|
|
133
|
-
)
|
|
134
150
|
details = (
|
|
135
151
|
"("
|
|
136
|
-
+
|
|
152
|
+
+ version_info
|
|
137
153
|
+ (
|
|
138
154
|
")"
|
|
139
155
|
if canary_percent is None
|
|
@@ -270,6 +286,7 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
270
286
|
)
|
|
271
287
|
return ApplyProductionServiceV2Model(
|
|
272
288
|
name=name,
|
|
289
|
+
version=config.version_name,
|
|
273
290
|
project_id=project_id,
|
|
274
291
|
ray_serve_config=self._build_ray_serve_config(config),
|
|
275
292
|
build_id=existing_config.build_id,
|
|
@@ -282,16 +299,19 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
282
299
|
),
|
|
283
300
|
ray_gcs_external_storage_config=existing_config.ray_gcs_external_storage_config,
|
|
284
301
|
tracing_config=existing_config.tracing_config,
|
|
302
|
+
tags=getattr(config, "tags", None),
|
|
285
303
|
)
|
|
286
304
|
|
|
287
305
|
def _build_apply_service_model_for_rollout( # noqa: PLR0912
|
|
288
306
|
self,
|
|
289
307
|
name: str,
|
|
290
308
|
config: ServiceConfig,
|
|
309
|
+
rollout_strategy: RolloutStrategy,
|
|
291
310
|
*,
|
|
292
311
|
canary_percent: Optional[int] = None,
|
|
293
312
|
max_surge_percent: Optional[int] = None,
|
|
294
313
|
existing_service: Optional[DecoratedProductionServiceV2APIModel] = None,
|
|
314
|
+
traffic_percent: Optional[int] = None,
|
|
295
315
|
) -> ApplyProductionServiceV2Model:
|
|
296
316
|
"""Build the ApplyProductionServiceV2Model for a rolling update."""
|
|
297
317
|
|
|
@@ -399,13 +419,14 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
399
419
|
|
|
400
420
|
return ApplyProductionServiceV2Model(
|
|
401
421
|
name=name,
|
|
422
|
+
version=config.version_name,
|
|
402
423
|
project_id=project_id,
|
|
403
424
|
ray_serve_config=self._build_ray_serve_config(config),
|
|
404
425
|
build_id=build_id,
|
|
405
426
|
compute_config_id=compute_config_id,
|
|
406
427
|
canary_percent=canary_percent,
|
|
407
428
|
max_surge_percent=max_surge_percent,
|
|
408
|
-
rollout_strategy=
|
|
429
|
+
rollout_strategy=rollout_strategy,
|
|
409
430
|
config=ExternalAPIServiceConfig(
|
|
410
431
|
access=AccessConfig(use_bearer_token=config.query_auth_token_enabled),
|
|
411
432
|
protocols=Protocols(grpc=self._build_grpc_protocol_config(config)),
|
|
@@ -413,36 +434,327 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
413
434
|
),
|
|
414
435
|
ray_gcs_external_storage_config=ray_gcs_external_storage_config,
|
|
415
436
|
tracing_config=tracing_config,
|
|
437
|
+
traffic_percent=traffic_percent,
|
|
438
|
+
tags=getattr(config, "tags", None),
|
|
416
439
|
)
|
|
417
440
|
|
|
418
|
-
def
|
|
441
|
+
def _build_apply_service_model_for_multi_version(
|
|
419
442
|
self,
|
|
420
|
-
|
|
443
|
+
name: str,
|
|
444
|
+
cloud: Optional[str],
|
|
445
|
+
project: Optional[str],
|
|
446
|
+
configs: List[ServiceConfig],
|
|
447
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel],
|
|
448
|
+
versions: List[Dict[str, Any]],
|
|
449
|
+
) -> ApplyProductionServiceV2Model:
|
|
450
|
+
cloud_id = self.client.get_cloud_id(cloud_name=cloud)
|
|
451
|
+
project_id = self.client.get_project_id(parent_cloud_id=cloud_id, name=project)
|
|
452
|
+
|
|
453
|
+
# implement a map between version name and config file
|
|
454
|
+
# This way, we can iterate over the version config and corresponding config file to the _build_apply_service_model_for_rollout function
|
|
455
|
+
# Also, maybe add an assertion to confirm the version name is the same as the config file name.
|
|
456
|
+
# 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.
|
|
457
|
+
version_service_map = self._get_service_version_map(configs, versions, existing_service) # type: ignore
|
|
458
|
+
existing_versions: Dict[str, DecoratedProductionServiceV2VersionAPIModel] = {}
|
|
459
|
+
if existing_service is not None:
|
|
460
|
+
existing_versions = {
|
|
461
|
+
v.version: v
|
|
462
|
+
for v in self.client.get_service_versions(
|
|
463
|
+
existing_service.id, read_all_versions=True
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
service_versions: List[ApplyProductionServiceV2Model] = []
|
|
468
|
+
for _, (config, version) in version_service_map.items():
|
|
469
|
+
version_name = version["name"]
|
|
470
|
+
traffic_percent = version["traffic_percent"]
|
|
471
|
+
if traffic_percent == 0:
|
|
472
|
+
self.logger.warning(
|
|
473
|
+
f"Setting traffic for version {version_name} to 0 will terminate it."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# 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.
|
|
477
|
+
if config is None:
|
|
478
|
+
if version_name not in existing_versions:
|
|
479
|
+
raise ValueError(
|
|
480
|
+
f"Version {version_name} does not exist in the existing service."
|
|
481
|
+
)
|
|
482
|
+
existing_version = existing_versions[version_name]
|
|
483
|
+
|
|
484
|
+
service_version = ApplyProductionServiceV2Model(
|
|
485
|
+
name=name,
|
|
486
|
+
project_id=project_id,
|
|
487
|
+
ray_serve_config=existing_version.ray_serve_config,
|
|
488
|
+
build_id=existing_version.build_id,
|
|
489
|
+
compute_config_id=existing_version.compute_config_id,
|
|
490
|
+
rollout_strategy=RolloutStrategy.MULTI_VERSION,
|
|
491
|
+
# TODO(doyoung): "config" is a service level config, so I'm assuming this is not necessary when this model is
|
|
492
|
+
# representing a service version. If not, may need to add and store 'config' field in the version model, ProductionServiceV2VersionModel,
|
|
493
|
+
# from the backend when the service version first gets created so it can be read from here for reconstruction.
|
|
494
|
+
config=None,
|
|
495
|
+
ray_gcs_external_storage_config=existing_version.ray_gcs_external_storage_config,
|
|
496
|
+
tracing_config=existing_version.tracing_config,
|
|
497
|
+
version=version_name,
|
|
498
|
+
traffic_percent=traffic_percent,
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
service_version = self._build_apply_service_model_for_rollout(
|
|
502
|
+
name,
|
|
503
|
+
config,
|
|
504
|
+
RolloutStrategy.MULTI_VERSION,
|
|
505
|
+
existing_service=existing_service,
|
|
506
|
+
traffic_percent=traffic_percent,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
service_versions.append(service_version)
|
|
510
|
+
|
|
511
|
+
assert len(service_versions) > 0, "service_versions should not be empty"
|
|
512
|
+
return ApplyProductionServiceMultiVersionV2Model(
|
|
513
|
+
rollout_strategy=RolloutStrategy.MULTI_VERSION,
|
|
514
|
+
cloud_id=cloud_id,
|
|
515
|
+
project_id=project_id,
|
|
516
|
+
service_versions=service_versions,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
def _validate_versions(
|
|
520
|
+
self, versions: str
|
|
521
|
+
) -> List[Dict[str, Any]]: # noqa: C901, PLR0912
|
|
522
|
+
# Parse versions JSON string to list of dictionaries
|
|
523
|
+
try:
|
|
524
|
+
versions = json.loads(versions)
|
|
525
|
+
except json.JSONDecodeError as e:
|
|
526
|
+
raise ValueError(f"Invalid JSON format for --versions: {e}")
|
|
527
|
+
|
|
528
|
+
for version in versions:
|
|
529
|
+
# traffic_percent must be specified in all versions by the users.
|
|
530
|
+
# If some version does not specify traffic_percent, raise an error.
|
|
531
|
+
if "name" not in version:
|
|
532
|
+
raise ValueError("Name is required for each version.")
|
|
533
|
+
if "traffic_percent" not in version:
|
|
534
|
+
raise ValueError("Traffic percent is required for each version.")
|
|
535
|
+
|
|
536
|
+
# Verify the key names are valid. Only either of name, traffic_percent, or capacity_percent are allowed.
|
|
537
|
+
for key in version:
|
|
538
|
+
if key not in [
|
|
539
|
+
"name",
|
|
540
|
+
"traffic_percent",
|
|
541
|
+
]:
|
|
542
|
+
raise ValueError(f"Invalid key in versions: {key}")
|
|
543
|
+
|
|
544
|
+
if isinstance(versions, dict):
|
|
545
|
+
# Convert object form {"v1": 25, "v2": 75} to full list form
|
|
546
|
+
versions = [ # type: ignore
|
|
547
|
+
{"name": name, "traffic_percent": percent}
|
|
548
|
+
for name, percent in versions.items()
|
|
549
|
+
]
|
|
550
|
+
elif not isinstance(versions, list):
|
|
551
|
+
raise ValueError(
|
|
552
|
+
"--versions must be a JSON array of objects or a JSON object in text format."
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Verify sum of each traffic_percent from each version is 100.
|
|
556
|
+
traffic_percent_sum = sum(version["traffic_percent"] for version in versions)
|
|
557
|
+
if traffic_percent_sum != 100:
|
|
558
|
+
raise ValueError("Sum of traffic percent for all versions must be 100.")
|
|
559
|
+
|
|
560
|
+
return versions # type: ignore
|
|
561
|
+
|
|
562
|
+
def _read_and_validate_configs(
|
|
563
|
+
self, configs: List[ServiceConfig]
|
|
564
|
+
) -> Tuple[str, Optional[str], Optional[str]]:
|
|
565
|
+
"""The service name, cloud, and project must be identical for all versions."""
|
|
566
|
+
|
|
567
|
+
# If all the name field from configs are identical, use the first one as the name.
|
|
568
|
+
# If all the name field from configs are not provided, set to None, use the default name.
|
|
569
|
+
if all(config.name is None for config in configs):
|
|
570
|
+
name = self._get_default_name()
|
|
571
|
+
elif all(config.name == configs[0].name for config in configs):
|
|
572
|
+
name = configs[0].name
|
|
573
|
+
else:
|
|
574
|
+
raise ValueError("All provided names in config files must be identical.")
|
|
575
|
+
|
|
576
|
+
# If all the cloud field from configs are identical, use the first one as the cloud.
|
|
577
|
+
# If all the cloud field from configs are all set to None, set cloud to None. Default cloud will be used from backend.
|
|
578
|
+
if all(config.cloud is None for config in configs):
|
|
579
|
+
cloud = None
|
|
580
|
+
elif all(config.cloud == configs[0].cloud for config in configs):
|
|
581
|
+
cloud = configs[0].cloud
|
|
582
|
+
else:
|
|
583
|
+
raise ValueError("All provided clouds in config files must be identical.")
|
|
584
|
+
|
|
585
|
+
# If all the project field from configs are identical, use the first one as the project.
|
|
586
|
+
# If all the project field from configs are all set to None, set project to None. Default project will be used from backend.
|
|
587
|
+
if all(config.project is None for config in configs):
|
|
588
|
+
project = None
|
|
589
|
+
elif all(config.project == configs[0].project for config in configs):
|
|
590
|
+
project = configs[0].project
|
|
591
|
+
else:
|
|
592
|
+
raise ValueError("All provided projects in config files must be identical.")
|
|
593
|
+
|
|
594
|
+
return name, cloud, project
|
|
595
|
+
|
|
596
|
+
def _get_service_version_map(
|
|
597
|
+
self,
|
|
598
|
+
configs: List[ServiceConfig],
|
|
599
|
+
versions: Optional[List[Dict[str, Any]]],
|
|
600
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel],
|
|
601
|
+
) -> Dict[str, Tuple[ServiceConfig, Dict[str, Any]]]:
|
|
602
|
+
service_config_to_version_config = {}
|
|
603
|
+
if versions is not None:
|
|
604
|
+
if len(configs) > len(versions):
|
|
605
|
+
raise ValueError(
|
|
606
|
+
"Number of config files passed must be less than or equal to the number of version configs when multiple versions are being deployed."
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
for service_config in configs:
|
|
610
|
+
for version_config in versions:
|
|
611
|
+
if service_config.version_name == version_config["name"]:
|
|
612
|
+
service_config_to_version_config[version_config["name"]] = (
|
|
613
|
+
service_config,
|
|
614
|
+
version_config,
|
|
615
|
+
)
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
# version name in the ServiceConfig must have a matching version name in the version config.
|
|
619
|
+
# 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.
|
|
620
|
+
if service_config.version_name not in service_config_to_version_config:
|
|
621
|
+
raise ValueError(
|
|
622
|
+
f"version_name {service_config.version_name!r} in service config {service_config} does not match any of the version names provided with --versions."
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# version name in version config doesn't need to have a matching version name in service config. If there's not a matching, this
|
|
626
|
+
# means the version is already deployed, and the user is likely to be only updating the traffic/capacity percent.
|
|
627
|
+
for version_config in versions:
|
|
628
|
+
version_name = version_config["name"]
|
|
629
|
+
if version_name not in service_config_to_version_config:
|
|
630
|
+
if existing_service is None:
|
|
631
|
+
raise ValueError(
|
|
632
|
+
f"--config-file corresponding to version {version_name!r} must be provided if the Service is not already deployed."
|
|
633
|
+
)
|
|
634
|
+
# TODO(doyoung): Perhaps add a validaiton logic to confirm if the version name already exists in the existing service through existing_service.versions.
|
|
635
|
+
service_config_to_version_config[version_name] = (
|
|
636
|
+
None,
|
|
637
|
+
version_config,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
return service_config_to_version_config
|
|
641
|
+
|
|
642
|
+
def _get_rollout_strategy( # noqa: PLR0912
|
|
643
|
+
self,
|
|
644
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel],
|
|
645
|
+
version_weights: Optional[List[Dict[str, Any]]],
|
|
646
|
+
in_place: bool,
|
|
647
|
+
configs: List[ServiceConfig],
|
|
648
|
+
) -> RolloutStrategy:
|
|
649
|
+
"""Determine the rollout strategy to use.
|
|
650
|
+
|
|
651
|
+
If `in_place` is True, return RolloutStrategy.IN_PLACE.
|
|
652
|
+
If `version_weights` is None or if the service is going from 0 or 1 versions to 1 version, return RolloutStrategy.ROLLOUT.
|
|
653
|
+
Otherwise, return RolloutStrategy.MULTI_VERSION.
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
active_versions = len(existing_service.versions) if existing_service else 0
|
|
657
|
+
|
|
658
|
+
if in_place:
|
|
659
|
+
if version_weights is not None:
|
|
660
|
+
raise ValueError(
|
|
661
|
+
"In-place updates are not supported for multi-version services."
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
return RolloutStrategy.IN_PLACE
|
|
665
|
+
elif version_weights is None or (
|
|
666
|
+
len(version_weights) == 1 and active_versions <= 1
|
|
667
|
+
):
|
|
668
|
+
if existing_service is None and len(configs) == 0:
|
|
669
|
+
raise ValueError(
|
|
670
|
+
"A config file must be provided when deploying a new service."
|
|
671
|
+
)
|
|
672
|
+
if version_weights is not None:
|
|
673
|
+
self.logger.warning(
|
|
674
|
+
"Defaulting to ROLLOUT strategy for single version service. Ignoring --versions."
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
return RolloutStrategy.ROLLOUT
|
|
678
|
+
else:
|
|
679
|
+
return RolloutStrategy.MULTI_VERSION
|
|
680
|
+
|
|
681
|
+
def deploy( # noqa: PLR0912, C901
|
|
682
|
+
self,
|
|
683
|
+
configs: Union[ServiceConfig, List[ServiceConfig]],
|
|
421
684
|
*,
|
|
422
685
|
in_place: bool = False,
|
|
423
686
|
canary_percent: Optional[int] = None,
|
|
424
687
|
max_surge_percent: Optional[int] = None,
|
|
688
|
+
versions: Optional[str] = None,
|
|
689
|
+
name: Optional[str] = None,
|
|
690
|
+
cloud: Optional[str] = None,
|
|
691
|
+
project: Optional[str] = None,
|
|
425
692
|
) -> str:
|
|
426
|
-
|
|
427
|
-
raise TypeError("in_place must be a bool.")
|
|
693
|
+
"""Deploy or update a service.
|
|
428
694
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
695
|
+
Args:
|
|
696
|
+
configs: Service configuration(s) to deploy. Can be a single ServiceConfig or a list of ServiceConfig objects for multi-version deployments.
|
|
697
|
+
versions: JSON string specifying version configurations for multi-version deployments.
|
|
698
|
+
name: Name of the service. If not provided, defaults to workspace cluster name.
|
|
699
|
+
cloud: Cloud provider to deploy to.
|
|
700
|
+
project: Project to deploy the service in.
|
|
434
701
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
702
|
+
Returns:
|
|
703
|
+
The service ID.
|
|
704
|
+
"""
|
|
705
|
+
if versions is not None:
|
|
706
|
+
versions = self._validate_versions(versions) # type: ignore
|
|
440
707
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
708
|
+
if isinstance(configs, ServiceConfig):
|
|
709
|
+
if versions is not None:
|
|
710
|
+
raise ValueError(
|
|
711
|
+
"To deploy with --versions, a list of ServiceConfig must be provided."
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
if not isinstance(in_place, bool):
|
|
715
|
+
raise TypeError("in_place must be a bool.")
|
|
716
|
+
|
|
717
|
+
if canary_percent is not None:
|
|
718
|
+
if not isinstance(canary_percent, int):
|
|
719
|
+
raise TypeError("canary_percent must be an int.")
|
|
720
|
+
if canary_percent < 0 or canary_percent > 100:
|
|
721
|
+
raise ValueError("canary_percent must be between 0 and 100.")
|
|
722
|
+
|
|
723
|
+
if max_surge_percent is not None:
|
|
724
|
+
if not isinstance(max_surge_percent, int):
|
|
725
|
+
raise TypeError("max_surge_percent must be an int.")
|
|
726
|
+
if max_surge_percent < 0 or max_surge_percent > 100:
|
|
727
|
+
raise ValueError("max_surge_percent must be between 0 and 100.")
|
|
728
|
+
|
|
729
|
+
name = configs.name
|
|
730
|
+
cloud = configs.cloud
|
|
731
|
+
project = configs.project
|
|
732
|
+
configs = [configs]
|
|
733
|
+
|
|
734
|
+
elif isinstance(configs, List):
|
|
735
|
+
if versions is None:
|
|
736
|
+
raise ValueError(
|
|
737
|
+
"--versions must be provided when deploying with multiple config files."
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
if len(configs) == 0:
|
|
741
|
+
if name is None:
|
|
742
|
+
raise ValueError(
|
|
743
|
+
"When using --versions, a service name must be provided if no config files are given."
|
|
744
|
+
)
|
|
745
|
+
else:
|
|
746
|
+
name, cloud, project = self._read_and_validate_configs(configs)
|
|
747
|
+
|
|
748
|
+
else:
|
|
749
|
+
ValueError(
|
|
750
|
+
"Single object of ServiceConfig or list of ServiceConfig must be provided."
|
|
445
751
|
)
|
|
752
|
+
|
|
753
|
+
if name is None:
|
|
754
|
+
name = self._get_default_name()
|
|
755
|
+
|
|
756
|
+
existing_service: Optional[DecoratedProductionServiceV2APIModel] = (
|
|
757
|
+
self.client.get_service(name=name, cloud=cloud, project=project)
|
|
446
758
|
)
|
|
447
759
|
if existing_service is None:
|
|
448
760
|
self.logger.info(f"Starting new service '{name}'.")
|
|
@@ -451,37 +763,63 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
451
763
|
else:
|
|
452
764
|
self.logger.info(f"Updating existing service '{name}'.")
|
|
453
765
|
|
|
454
|
-
# passed canary_percent is ignored when creating or restarting a service
|
|
766
|
+
# passed canary_percent is ignored when creating or restarting a service or multiple versions are being deployed.
|
|
455
767
|
is_new_or_restarting = (
|
|
456
768
|
existing_service is None
|
|
457
769
|
or existing_service.current_state == ServiceEventCurrentState.TERMINATED
|
|
458
770
|
)
|
|
459
|
-
if canary_percent is not None and
|
|
771
|
+
if canary_percent is not None and (
|
|
772
|
+
is_new_or_restarting or versions is not None
|
|
773
|
+
):
|
|
460
774
|
canary_percent = None
|
|
461
775
|
self.logger.warning(
|
|
462
|
-
"canary_percent is ignored when creating or restarting a service."
|
|
776
|
+
"canary_percent is ignored when creating or restarting a service or multiple versions are being deployed."
|
|
463
777
|
)
|
|
464
778
|
|
|
465
|
-
|
|
779
|
+
rollout_strategy = self._get_rollout_strategy(
|
|
780
|
+
existing_service, versions, in_place, configs # type: ignore
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
service: DecoratedProductionServiceV2APIModel
|
|
784
|
+
# TODO(doyoung): Support multi version deployment for in_place update.
|
|
785
|
+
if rollout_strategy == RolloutStrategy.IN_PLACE:
|
|
786
|
+
if versions is not None:
|
|
787
|
+
raise ValueError(
|
|
788
|
+
"versions cannot be specified when doing an in_place update."
|
|
789
|
+
)
|
|
790
|
+
assert len(configs) == 1
|
|
466
791
|
model = self._build_apply_service_model_for_in_place_update(
|
|
467
792
|
name,
|
|
468
|
-
|
|
793
|
+
configs[0],
|
|
469
794
|
canary_percent=canary_percent,
|
|
470
795
|
max_surge_percent=max_surge_percent,
|
|
471
796
|
existing_service=existing_service,
|
|
472
797
|
)
|
|
798
|
+
service = self.client.rollout_service(model)
|
|
799
|
+
elif rollout_strategy == RolloutStrategy.MULTI_VERSION:
|
|
800
|
+
model = self._build_apply_service_model_for_multi_version(
|
|
801
|
+
name,
|
|
802
|
+
cloud,
|
|
803
|
+
project,
|
|
804
|
+
configs,
|
|
805
|
+
existing_service=existing_service,
|
|
806
|
+
versions=versions, # type: ignore
|
|
807
|
+
)
|
|
808
|
+
service = self.client.rollout_service_multi_version(model)
|
|
473
809
|
else:
|
|
810
|
+
# rolling out a single version in new service without specifying versions.
|
|
811
|
+
assert len(configs) == 1
|
|
474
812
|
model = self._build_apply_service_model_for_rollout(
|
|
475
813
|
name,
|
|
476
|
-
|
|
814
|
+
configs[0],
|
|
815
|
+
RolloutStrategy.ROLLOUT,
|
|
477
816
|
canary_percent=canary_percent,
|
|
478
817
|
max_surge_percent=max_surge_percent,
|
|
479
818
|
existing_service=existing_service,
|
|
480
819
|
)
|
|
820
|
+
service = self.client.rollout_service(model)
|
|
481
821
|
|
|
482
|
-
service = self.client.rollout_service(model)
|
|
483
822
|
self._log_deployed_service_info(service, canary_percent=canary_percent)
|
|
484
|
-
|
|
485
823
|
return service.id
|
|
486
824
|
|
|
487
825
|
def _resolve_to_service_model(
|
|
@@ -619,14 +957,18 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
619
957
|
sampling_ratio=model.tracing_config.sampling_ratio,
|
|
620
958
|
)
|
|
621
959
|
|
|
960
|
+
version_name = model.version or self._get_user_facing_service_version_id(model)
|
|
961
|
+
|
|
622
962
|
return ServiceVersionStatus(
|
|
623
963
|
id=self._get_user_facing_service_version_id(model),
|
|
964
|
+
name=version_name,
|
|
624
965
|
created_at=model.created_at,
|
|
625
966
|
state=model.current_state,
|
|
626
967
|
# NOTE(edoakes): there is also a "current_weight" field but it does not match the UI.
|
|
627
968
|
weight=model.weight,
|
|
628
969
|
config=ServiceConfig(
|
|
629
970
|
name=service_name,
|
|
971
|
+
version_name=version_name,
|
|
630
972
|
applications=model.ray_serve_config["applications"],
|
|
631
973
|
image_uri=str(image_uri),
|
|
632
974
|
compute_config=compute_config,
|
|
@@ -670,35 +1012,63 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
670
1012
|
# which means that the per-version `query_auth_token_enabled` field will lie if
|
|
671
1013
|
# it's changed.
|
|
672
1014
|
query_auth_token_enabled = model.auth_token is not None
|
|
1015
|
+
all_versions = None
|
|
1016
|
+
primary_version = None
|
|
1017
|
+
canary_version = None
|
|
1018
|
+
project_name = None
|
|
673
1019
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1020
|
+
if model.is_multi_version:
|
|
1021
|
+
# For multi-version services, format all versions
|
|
1022
|
+
version_tasks = [
|
|
1023
|
+
asyncio.create_task(
|
|
1024
|
+
self._service_version_model_to_status_async(
|
|
1025
|
+
version,
|
|
1026
|
+
service_name=model.name,
|
|
1027
|
+
project_id=model.project_id,
|
|
1028
|
+
query_auth_token_enabled=query_auth_token_enabled,
|
|
1029
|
+
)
|
|
682
1030
|
)
|
|
683
|
-
|
|
1031
|
+
for version in model.versions
|
|
1032
|
+
]
|
|
1033
|
+
all_versions = await asyncio.gather(*version_tasks)
|
|
1034
|
+
|
|
1035
|
+
if (
|
|
1036
|
+
all_versions
|
|
1037
|
+
and all_versions[0]
|
|
1038
|
+
and isinstance(all_versions[0].config, ServiceConfig)
|
|
1039
|
+
):
|
|
1040
|
+
project_name = all_versions[0].config.project
|
|
684
1041
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1042
|
+
else:
|
|
1043
|
+
primary_version_task = None
|
|
1044
|
+
if model.primary_version is not None:
|
|
1045
|
+
primary_version_task = asyncio.create_task(
|
|
1046
|
+
self._service_version_model_to_status_async(
|
|
1047
|
+
model.primary_version,
|
|
1048
|
+
service_name=model.name,
|
|
1049
|
+
project_id=model.project_id,
|
|
1050
|
+
query_auth_token_enabled=query_auth_token_enabled,
|
|
1051
|
+
)
|
|
693
1052
|
)
|
|
694
|
-
)
|
|
695
1053
|
|
|
696
|
-
|
|
697
|
-
|
|
1054
|
+
canary_version_task = None
|
|
1055
|
+
if model.canary_version is not None:
|
|
1056
|
+
canary_version_task = asyncio.create_task(
|
|
1057
|
+
self._service_version_model_to_status_async(
|
|
1058
|
+
model.canary_version,
|
|
1059
|
+
service_name=model.name,
|
|
1060
|
+
project_id=model.project_id,
|
|
1061
|
+
query_auth_token_enabled=query_auth_token_enabled,
|
|
1062
|
+
)
|
|
1063
|
+
)
|
|
698
1064
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1065
|
+
primary_version = (
|
|
1066
|
+
await primary_version_task if primary_version_task else None
|
|
1067
|
+
)
|
|
1068
|
+
canary_version = await canary_version_task if canary_version_task else None
|
|
1069
|
+
|
|
1070
|
+
if primary_version and isinstance(primary_version.config, ServiceConfig):
|
|
1071
|
+
project_name = primary_version.config.project
|
|
702
1072
|
|
|
703
1073
|
return ServiceStatus(
|
|
704
1074
|
id=model.id,
|
|
@@ -710,6 +1080,7 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
710
1080
|
primary_version=primary_version,
|
|
711
1081
|
canary_version=canary_version,
|
|
712
1082
|
project=project_name,
|
|
1083
|
+
versions=all_versions,
|
|
713
1084
|
)
|
|
714
1085
|
|
|
715
1086
|
def status(
|
|
@@ -730,6 +1101,7 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
730
1101
|
cloud: Optional[str] = None,
|
|
731
1102
|
project: Optional[str] = None,
|
|
732
1103
|
include_archived: bool = False,
|
|
1104
|
+
tags_filter: Optional[Dict[str, List[str]]] = None,
|
|
733
1105
|
# Paging
|
|
734
1106
|
max_items: Optional[int] = None, # Controls total items yielded by iterator
|
|
735
1107
|
page_size: Optional[int] = None, # Controls items fetched per API call
|
|
@@ -770,6 +1142,18 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
770
1142
|
|
|
771
1143
|
normalised_states = _normalize_state_filter(state_filter)
|
|
772
1144
|
|
|
1145
|
+
# Convert tags dict into backend tag_filter list[str]
|
|
1146
|
+
backend_tag_filter: Optional[List[str]] = None
|
|
1147
|
+
if tags_filter:
|
|
1148
|
+
flattened: List[str] = []
|
|
1149
|
+
for key, values in tags_filter.items():
|
|
1150
|
+
if not values:
|
|
1151
|
+
continue
|
|
1152
|
+
for value in values:
|
|
1153
|
+
if key and value:
|
|
1154
|
+
flattened.append(f"{key}:{value}")
|
|
1155
|
+
backend_tag_filter = flattened if len(flattened) > 0 else None
|
|
1156
|
+
|
|
773
1157
|
def _fetch_page(
|
|
774
1158
|
token: Optional[str],
|
|
775
1159
|
) -> DecoratedlistserviceapimodelListResponse:
|
|
@@ -780,6 +1164,7 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
780
1164
|
cloud=cloud,
|
|
781
1165
|
project=project,
|
|
782
1166
|
include_archived=include_archived,
|
|
1167
|
+
tag_filter=backend_tag_filter,
|
|
783
1168
|
count=page_size,
|
|
784
1169
|
paging_token=token,
|
|
785
1170
|
sort_field=sort_field,
|
|
@@ -873,6 +1258,44 @@ class PrivateServiceSDK(WorkloadSDK):
|
|
|
873
1258
|
model.primary_version, head, max_lines
|
|
874
1259
|
)
|
|
875
1260
|
|
|
1261
|
+
def add_tags(
|
|
1262
|
+
self,
|
|
1263
|
+
*,
|
|
1264
|
+
id: Optional[str] = None, # noqa: A002
|
|
1265
|
+
name: Optional[str] = None,
|
|
1266
|
+
cloud: Optional[str] = None,
|
|
1267
|
+
project: Optional[str] = None,
|
|
1268
|
+
tags: Dict[str, str],
|
|
1269
|
+
) -> None:
|
|
1270
|
+
if id is None:
|
|
1271
|
+
model = self._resolve_to_service_model(
|
|
1272
|
+
name=name, cloud=cloud, project=project
|
|
1273
|
+
)
|
|
1274
|
+
resource_id = model.id
|
|
1275
|
+
assert id is not None
|
|
1276
|
+
self.client.upsert_resource_tags(
|
|
1277
|
+
ResourceTagResourceType.SERVICE, resource_id, tags
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
def remove_tags(
|
|
1281
|
+
self,
|
|
1282
|
+
*,
|
|
1283
|
+
id: Optional[str] = None, # noqa: A002
|
|
1284
|
+
name: Optional[str] = None,
|
|
1285
|
+
cloud: Optional[str] = None,
|
|
1286
|
+
project: Optional[str] = None,
|
|
1287
|
+
keys: List[str],
|
|
1288
|
+
) -> None:
|
|
1289
|
+
if id is None:
|
|
1290
|
+
model = self._resolve_to_service_model(
|
|
1291
|
+
name=name, cloud=cloud, project=project
|
|
1292
|
+
)
|
|
1293
|
+
resource_id = model.id
|
|
1294
|
+
assert id is not None
|
|
1295
|
+
self.client.delete_resource_tags(
|
|
1296
|
+
ResourceTagResourceType.SERVICE, resource_id, keys
|
|
1297
|
+
)
|
|
1298
|
+
|
|
876
1299
|
|
|
877
1300
|
def _normalize_state_filter(
|
|
878
1301
|
states: Optional[Union[List[ServiceState], List[str]]],
|