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.
Files changed (113) hide show
  1. anyscale/_private/anyscale_client/anyscale_client.py +126 -3
  2. anyscale/_private/anyscale_client/common.py +51 -2
  3. anyscale/_private/anyscale_client/fake_anyscale_client.py +103 -11
  4. anyscale/client/README.md +43 -4
  5. anyscale/client/openapi_client/__init__.py +30 -4
  6. anyscale/client/openapi_client/api/default_api.py +1769 -27
  7. anyscale/client/openapi_client/models/__init__.py +30 -4
  8. anyscale/client/openapi_client/models/api_key_info.py +29 -3
  9. anyscale/client/openapi_client/models/apply_autoscaling_config_update_model.py +350 -0
  10. anyscale/client/openapi_client/models/apply_multi_version_update_weights_update_model.py +152 -0
  11. anyscale/client/openapi_client/models/apply_production_service_multi_version_v2_model.py +207 -0
  12. anyscale/client/openapi_client/models/apply_production_service_v2_model.py +31 -3
  13. anyscale/client/openapi_client/models/apply_version_weight_update_model.py +181 -0
  14. anyscale/client/openapi_client/models/backend_server_api_product_models_catalog_client_models_table_metadata.py +546 -0
  15. anyscale/client/openapi_client/models/backend_server_api_product_models_data_catalogs_table_metadata.py +178 -0
  16. anyscale/client/openapi_client/models/baseimagesenum.py +139 -1
  17. anyscale/client/openapi_client/models/catalog_metadata.py +150 -0
  18. anyscale/client/openapi_client/models/cloud_data_bucket_file_type.py +2 -1
  19. anyscale/client/openapi_client/models/{oauthconnectionresponse_response.py → clouddeployment_response.py} +11 -11
  20. anyscale/client/openapi_client/models/column_info.py +265 -0
  21. anyscale/client/openapi_client/models/compute_node_type.py +29 -1
  22. anyscale/client/openapi_client/models/connection_metadata.py +206 -0
  23. anyscale/client/openapi_client/models/create_experimental_workspace.py +29 -1
  24. anyscale/client/openapi_client/models/create_workspace_from_template.py +29 -1
  25. anyscale/client/openapi_client/models/create_workspace_template_version.py +59 -3
  26. anyscale/client/openapi_client/models/data_catalog.py +45 -31
  27. anyscale/client/openapi_client/models/data_catalog_connection.py +74 -58
  28. anyscale/client/openapi_client/models/{ha_job_event_level.py → data_catalog_object_type.py} +7 -8
  29. anyscale/client/openapi_client/models/data_catalog_schema.py +324 -0
  30. anyscale/client/openapi_client/models/data_catalog_table.py +437 -0
  31. anyscale/client/openapi_client/models/data_catalog_volume.py +437 -0
  32. anyscale/client/openapi_client/models/datacatalogschema_list_response.py +147 -0
  33. anyscale/client/openapi_client/models/datacatalogtable_list_response.py +147 -0
  34. anyscale/client/openapi_client/models/datacatalogvolume_list_response.py +147 -0
  35. anyscale/client/openapi_client/models/decorated_list_service_api_model.py +58 -1
  36. anyscale/client/openapi_client/models/decorated_production_service_v2_api_model.py +60 -3
  37. anyscale/client/openapi_client/models/decorated_serve_deployment.py +27 -1
  38. anyscale/client/openapi_client/models/decorated_service_event_api_model.py +3 -3
  39. anyscale/client/openapi_client/models/decoratedproductionservicev2_versionapimodel_response.py +121 -0
  40. anyscale/client/openapi_client/models/describe_machine_pool_machines_filters.py +33 -5
  41. anyscale/client/openapi_client/models/describe_machine_pool_requests_filters.py +33 -5
  42. anyscale/client/openapi_client/models/describe_machine_pool_workloads_filters.py +33 -5
  43. anyscale/client/openapi_client/models/{service_event_level.py → entity_type.py} +9 -9
  44. anyscale/client/openapi_client/models/event_level.py +2 -1
  45. anyscale/client/openapi_client/models/job_event_fields.py +206 -0
  46. anyscale/client/openapi_client/models/machine_type_partition_filter.py +152 -0
  47. anyscale/client/openapi_client/models/partition_info.py +30 -1
  48. anyscale/client/openapi_client/models/physical_resources.py +178 -0
  49. anyscale/client/openapi_client/models/production_job_event.py +3 -3
  50. anyscale/client/openapi_client/models/rollout_strategy.py +2 -1
  51. anyscale/client/openapi_client/models/schema_metadata.py +150 -0
  52. anyscale/client/openapi_client/models/service_event_fields.py +318 -0
  53. anyscale/client/openapi_client/models/sso_config.py +18 -18
  54. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +139 -1
  55. anyscale/client/openapi_client/models/table_data_preview.py +209 -0
  56. anyscale/client/openapi_client/models/task_summary_config.py +29 -3
  57. anyscale/client/openapi_client/models/task_table_config.py +29 -3
  58. anyscale/client/openapi_client/models/unified_event.py +377 -0
  59. anyscale/client/openapi_client/models/unified_origin_filter.py +113 -0
  60. anyscale/client/openapi_client/models/unifiedevent_list_response.py +147 -0
  61. anyscale/client/openapi_client/models/volume_metadata.py +150 -0
  62. anyscale/client/openapi_client/models/worker_node_type.py +29 -1
  63. anyscale/client/openapi_client/models/workspace_event_fields.py +122 -0
  64. anyscale/client/openapi_client/models/workspace_template_version.py +58 -1
  65. anyscale/client/openapi_client/models/workspace_template_version_data_object.py +58 -1
  66. anyscale/cloud/models.py +2 -2
  67. anyscale/commands/cloud_commands.py +133 -2
  68. anyscale/commands/job_commands.py +121 -1
  69. anyscale/commands/job_queue_commands.py +99 -2
  70. anyscale/commands/service_commands.py +267 -67
  71. anyscale/commands/setup_k8s.py +546 -31
  72. anyscale/commands/util.py +104 -1
  73. anyscale/commands/workspace_commands.py +123 -5
  74. anyscale/commands/workspace_commands_v2.py +17 -1
  75. anyscale/compute_config/_private/compute_config_sdk.py +25 -12
  76. anyscale/compute_config/models.py +15 -0
  77. anyscale/controllers/cloud_controller.py +15 -2
  78. anyscale/controllers/job_controller.py +12 -0
  79. anyscale/controllers/kubernetes_verifier.py +80 -66
  80. anyscale/controllers/workspace_controller.py +67 -5
  81. anyscale/job/_private/job_sdk.py +50 -2
  82. anyscale/job/commands.py +3 -0
  83. anyscale/job/models.py +16 -0
  84. anyscale/job_queue/__init__.py +37 -1
  85. anyscale/job_queue/_private/job_queue_sdk.py +28 -1
  86. anyscale/job_queue/commands.py +61 -1
  87. anyscale/sdk/anyscale_client/__init__.py +1 -0
  88. anyscale/sdk/anyscale_client/api/default_api.py +12 -2
  89. anyscale/sdk/anyscale_client/models/__init__.py +1 -0
  90. anyscale/sdk/anyscale_client/models/apply_production_service_v2_model.py +31 -3
  91. anyscale/sdk/anyscale_client/models/apply_service_model.py +31 -3
  92. anyscale/sdk/anyscale_client/models/baseimagesenum.py +139 -1
  93. anyscale/sdk/anyscale_client/models/compute_node_type.py +29 -1
  94. anyscale/sdk/anyscale_client/models/physical_resources.py +178 -0
  95. anyscale/sdk/anyscale_client/models/rollout_strategy.py +2 -1
  96. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +139 -1
  97. anyscale/sdk/anyscale_client/models/worker_node_type.py +29 -1
  98. anyscale/service/__init__.py +51 -3
  99. anyscale/service/_private/service_sdk.py +481 -58
  100. anyscale/service/commands.py +90 -4
  101. anyscale/service/models.py +56 -0
  102. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  103. anyscale/version.py +1 -1
  104. anyscale/workspace/_private/workspace_sdk.py +1 -0
  105. anyscale/workspace/models.py +19 -0
  106. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/METADATA +1 -1
  107. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/RECORD +112 -85
  108. anyscale/client/openapi_client/models/o_auth_connection_response.py +0 -229
  109. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/WHEEL +0 -0
  110. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/entry_points.txt +0 -0
  111. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/LICENSE +0 -0
  112. {anyscale-0.26.69.dist-info → anyscale-0.26.71.dist-info}/licenses/NOTICE +0 -0
  113. {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
- from typing import Any, Dict, List, Optional, Union
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
- version_id_info = "version ID: {version_id}".format(
128
- version_id=self._get_user_facing_service_version_id(
129
- service.canary_version
130
- if service.canary_version is not None
131
- else service.primary_version
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
- + version_id_info
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="ROLLOUT",
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 deploy( # noqa: PLR0912
441
+ def _build_apply_service_model_for_multi_version(
419
442
  self,
420
- config: ServiceConfig,
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
- if not isinstance(in_place, bool):
427
- raise TypeError("in_place must be a bool.")
693
+ """Deploy or update a service.
428
694
 
429
- if canary_percent is not None:
430
- if not isinstance(canary_percent, int):
431
- raise TypeError("canary_percent must be an int.")
432
- if canary_percent < 0 or canary_percent > 100:
433
- raise ValueError("canary_percent must be between 0 and 100.")
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
- if max_surge_percent is not None:
436
- if not isinstance(max_surge_percent, int):
437
- raise TypeError("max_surge_percent must be an int.")
438
- if max_surge_percent < 0 or max_surge_percent > 100:
439
- raise ValueError("max_surge_percent must be between 0 and 100.")
702
+ Returns:
703
+ The service ID.
704
+ """
705
+ if versions is not None:
706
+ versions = self._validate_versions(versions) # type: ignore
440
707
 
441
- name = config.name or self._get_default_name()
442
- existing_service: Optional[DecoratedProductionServiceV2APIModel] = (
443
- self.client.get_service(
444
- name=name, cloud=config.cloud, project=config.project
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 is_new_or_restarting:
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
- if in_place:
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
- config,
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
- config,
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
- primary_version_task = None
675
- if model.primary_version is not None:
676
- primary_version_task = asyncio.create_task(
677
- self._service_version_model_to_status_async(
678
- model.primary_version,
679
- service_name=model.name,
680
- project_id=model.project_id,
681
- query_auth_token_enabled=query_auth_token_enabled,
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
- canary_version_task = None
686
- if model.canary_version is not None:
687
- canary_version_task = asyncio.create_task(
688
- self._service_version_model_to_status_async(
689
- model.canary_version,
690
- service_name=model.name,
691
- project_id=model.project_id,
692
- query_auth_token_enabled=query_auth_token_enabled,
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
- primary_version = await primary_version_task if primary_version_task else None
697
- canary_version = await canary_version_task if canary_version_task else None
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
- project_name = None
700
- if primary_version and isinstance(primary_version.config, ServiceConfig):
701
- project_name = primary_version.config.project
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]]],