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
|
@@ -11,7 +11,11 @@ from typing_extensions import Literal
|
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
13
|
from anyscale._private.models.image_uri import ImageURI
|
|
14
|
+
from anyscale.authenticate import get_auth_api_client
|
|
14
15
|
from anyscale.cli_logger import BlockLogger
|
|
16
|
+
from anyscale.client.openapi_client.models.resource_tag_resource_type import (
|
|
17
|
+
ResourceTagResourceType,
|
|
18
|
+
)
|
|
15
19
|
from anyscale.commands import command_examples
|
|
16
20
|
from anyscale.commands.list_util import (
|
|
17
21
|
display_list,
|
|
@@ -21,9 +25,12 @@ from anyscale.commands.list_util import (
|
|
|
21
25
|
)
|
|
22
26
|
from anyscale.commands.util import (
|
|
23
27
|
AnyscaleCommand,
|
|
28
|
+
build_kv_table,
|
|
24
29
|
convert_kv_strings_to_dict,
|
|
25
30
|
DeprecatedAnyscaleCommand,
|
|
26
31
|
override_env_vars,
|
|
32
|
+
parse_repeatable_tags_to_dict,
|
|
33
|
+
parse_tags_kv_to_str_map,
|
|
27
34
|
)
|
|
28
35
|
from anyscale.controllers.service_controller import ServiceController
|
|
29
36
|
import anyscale.service
|
|
@@ -79,8 +86,9 @@ def _read_name_from_config_file(path: str):
|
|
|
79
86
|
"-f",
|
|
80
87
|
"--config-file",
|
|
81
88
|
required=False,
|
|
82
|
-
default=
|
|
89
|
+
default=[],
|
|
83
90
|
type=str,
|
|
91
|
+
multiple=True,
|
|
84
92
|
help="Path to a YAML config file to deploy. When deploying from a file, import path and arguments cannot be provided. Command-line flags will overwrite values read from the file.",
|
|
85
93
|
)
|
|
86
94
|
@click.option(
|
|
@@ -202,8 +210,21 @@ def _read_name_from_config_file(path: str):
|
|
|
202
210
|
type=str,
|
|
203
211
|
help="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).",
|
|
204
212
|
)
|
|
213
|
+
@click.option(
|
|
214
|
+
"--versions",
|
|
215
|
+
required=False,
|
|
216
|
+
default=None,
|
|
217
|
+
type=str,
|
|
218
|
+
help="Defines the traffic and capacity percents per version. Capacity defaults to traffic.",
|
|
219
|
+
)
|
|
220
|
+
@click.option(
|
|
221
|
+
"--tag",
|
|
222
|
+
"tags",
|
|
223
|
+
multiple=True,
|
|
224
|
+
help="Tag in key=value (or key:value) format. Repeat to add multiple.",
|
|
225
|
+
)
|
|
205
226
|
def deploy( # noqa: PLR0912, PLR0913 C901
|
|
206
|
-
config_file:
|
|
227
|
+
config_file: List[str],
|
|
207
228
|
import_path: Optional[str],
|
|
208
229
|
arguments: Tuple[str],
|
|
209
230
|
name: Optional[str],
|
|
@@ -222,6 +243,8 @@ def deploy( # noqa: PLR0912, PLR0913 C901
|
|
|
222
243
|
py_module: Tuple[str],
|
|
223
244
|
cloud: Optional[str],
|
|
224
245
|
project: Optional[str],
|
|
246
|
+
versions: Optional[str],
|
|
247
|
+
tags: Optional[Tuple[str]],
|
|
225
248
|
):
|
|
226
249
|
"""Deploy or update a service.
|
|
227
250
|
|
|
@@ -238,102 +261,165 @@ def deploy( # noqa: PLR0912, PLR0913 C901
|
|
|
238
261
|
Command-line flags override values in the config file.
|
|
239
262
|
"""
|
|
240
263
|
|
|
241
|
-
if
|
|
242
|
-
if
|
|
264
|
+
if versions is None:
|
|
265
|
+
if len(config_file) == 1:
|
|
266
|
+
if import_path is not None or len(arguments) > 0:
|
|
267
|
+
raise click.ClickException(
|
|
268
|
+
"When a config file is provided, import path and application arguments can't be."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if not pathlib.Path(config_file[0]).is_file():
|
|
272
|
+
raise click.ClickException(f"Config file '{config_file[0]}' not found.")
|
|
273
|
+
|
|
274
|
+
config = ServiceConfig.from_yaml(config_file[0])
|
|
275
|
+
elif len(config_file) > 1:
|
|
243
276
|
raise click.ClickException(
|
|
244
|
-
"
|
|
277
|
+
"Multiple config files can be provided only when deploying multiple versions with --versions."
|
|
245
278
|
)
|
|
279
|
+
else:
|
|
280
|
+
# when config_file is not provided.
|
|
281
|
+
if import_path is None:
|
|
282
|
+
raise click.ClickException(
|
|
283
|
+
"Either config file or import path must be provided."
|
|
284
|
+
)
|
|
246
285
|
|
|
247
|
-
|
|
248
|
-
|
|
286
|
+
if (
|
|
287
|
+
import_path.endswith((".yaml", ".yml"))
|
|
288
|
+
or pathlib.Path(import_path).is_file()
|
|
289
|
+
):
|
|
290
|
+
log.warning(
|
|
291
|
+
f"The provided import path '{import_path}' looks like a config file. Did you mean to use '-f config.yaml'?"
|
|
292
|
+
)
|
|
249
293
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
294
|
+
app: Dict[str, Any] = {"import_path": import_path}
|
|
295
|
+
arguments_dict = convert_kv_strings_to_dict(arguments)
|
|
296
|
+
if arguments_dict:
|
|
297
|
+
app["args"] = arguments_dict
|
|
298
|
+
|
|
299
|
+
config = ServiceConfig(applications=[app])
|
|
300
|
+
|
|
301
|
+
if containerfile and image_uri:
|
|
253
302
|
raise click.ClickException(
|
|
254
|
-
"
|
|
303
|
+
"Only one of '--containerfile' and '--image-uri' can be provided."
|
|
255
304
|
)
|
|
256
305
|
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
306
|
+
if ray_version and (not image_uri and not containerfile):
|
|
307
|
+
raise click.ClickException(
|
|
308
|
+
"Ray version can only be used with an image or containerfile.",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if registry_login_secret and (
|
|
312
|
+
not image_uri or ImageURI.from_str(image_uri).is_cluster_env_image()
|
|
260
313
|
):
|
|
261
|
-
|
|
262
|
-
|
|
314
|
+
raise click.ClickException(
|
|
315
|
+
"Registry login secret can only be used with an image that is not hosted on Anyscale."
|
|
263
316
|
)
|
|
264
317
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if arguments_dict:
|
|
268
|
-
app["args"] = arguments_dict
|
|
318
|
+
if name is not None:
|
|
319
|
+
config = config.options(name=name)
|
|
269
320
|
|
|
270
|
-
|
|
321
|
+
if image_uri is not None:
|
|
322
|
+
config = config.options(image_uri=image_uri)
|
|
271
323
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"Only one of '--containerfile' and '--image-uri' can be provided."
|
|
275
|
-
)
|
|
324
|
+
if registry_login_secret is not None:
|
|
325
|
+
config = config.options(registry_login_secret=registry_login_secret)
|
|
276
326
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"Ray version can only be used with an image or containerfile.",
|
|
280
|
-
)
|
|
327
|
+
if ray_version is not None:
|
|
328
|
+
config = config.options(ray_version=ray_version)
|
|
281
329
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
):
|
|
285
|
-
raise click.ClickException(
|
|
286
|
-
"Registry login secret can only be used with an image that is not hosted on Anyscale."
|
|
287
|
-
)
|
|
330
|
+
if containerfile is not None:
|
|
331
|
+
config = config.options(containerfile=containerfile)
|
|
288
332
|
|
|
289
|
-
|
|
290
|
-
|
|
333
|
+
if compute_config is not None:
|
|
334
|
+
config = config.options(compute_config=compute_config)
|
|
291
335
|
|
|
292
|
-
|
|
293
|
-
|
|
336
|
+
if working_dir is not None:
|
|
337
|
+
config = config.options(working_dir=working_dir)
|
|
294
338
|
|
|
295
|
-
|
|
296
|
-
|
|
339
|
+
if exclude:
|
|
340
|
+
config = config.options(excludes=[e for e in exclude])
|
|
297
341
|
|
|
298
|
-
|
|
299
|
-
|
|
342
|
+
if requirements is not None:
|
|
343
|
+
config = config.options(requirements=requirements)
|
|
300
344
|
|
|
301
|
-
|
|
302
|
-
|
|
345
|
+
if env:
|
|
346
|
+
config = override_env_vars(config, convert_kv_strings_to_dict(env))
|
|
303
347
|
|
|
304
|
-
|
|
305
|
-
|
|
348
|
+
if py_module:
|
|
349
|
+
for module in py_module:
|
|
350
|
+
if not pathlib.Path(module).is_dir():
|
|
351
|
+
raise click.ClickException(
|
|
352
|
+
f"Python module path '{module}' does not exist or is not a directory."
|
|
353
|
+
)
|
|
354
|
+
config = config.options(py_modules=[*py_module])
|
|
306
355
|
|
|
307
|
-
|
|
308
|
-
|
|
356
|
+
if cloud is not None:
|
|
357
|
+
config = config.options(cloud=cloud)
|
|
358
|
+
if project is not None:
|
|
359
|
+
config = config.options(project=project)
|
|
309
360
|
|
|
310
|
-
|
|
311
|
-
|
|
361
|
+
configs = config
|
|
362
|
+
else:
|
|
363
|
+
# When multiple versions are being deployed.
|
|
364
|
+
# configs = [ServiceConfig.from_yaml(config) for config in config_file]
|
|
365
|
+
configs = []
|
|
366
|
+
for config in config_file:
|
|
367
|
+
config = ServiceConfig.from_yaml(config)
|
|
368
|
+
if name is not None:
|
|
369
|
+
config = config.options(name=name)
|
|
312
370
|
|
|
313
|
-
|
|
314
|
-
|
|
371
|
+
if cloud is not None:
|
|
372
|
+
config = config.options(cloud=cloud)
|
|
315
373
|
|
|
316
|
-
|
|
317
|
-
|
|
374
|
+
if project is not None:
|
|
375
|
+
config = config.options(project=project)
|
|
318
376
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
377
|
+
if image_uri is not None:
|
|
378
|
+
log.warning("--image-uri is ignored.")
|
|
379
|
+
|
|
380
|
+
if registry_login_secret is not None:
|
|
381
|
+
log.warning("--registry-login-secret is ignored.")
|
|
382
|
+
|
|
383
|
+
if ray_version is not None:
|
|
384
|
+
log.warning("--ray-version is ignored.")
|
|
385
|
+
|
|
386
|
+
if containerfile is not None:
|
|
387
|
+
log.warning("--containerfile is ignored.")
|
|
388
|
+
|
|
389
|
+
if compute_config is not None:
|
|
390
|
+
log.warning("--compute-config is ignored.")
|
|
391
|
+
|
|
392
|
+
if working_dir is not None:
|
|
393
|
+
log.warning("--working-dir is ignored.")
|
|
394
|
+
|
|
395
|
+
if exclude:
|
|
396
|
+
log.warning("--exclude is ignored.")
|
|
397
|
+
|
|
398
|
+
if requirements is not None:
|
|
399
|
+
log.warning("--requirements is ignored.")
|
|
400
|
+
|
|
401
|
+
if env:
|
|
402
|
+
log.warning("--env is ignored.")
|
|
326
403
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
404
|
+
if py_module:
|
|
405
|
+
log.warning("--py-module is ignored.")
|
|
406
|
+
|
|
407
|
+
configs.append(config)
|
|
408
|
+
|
|
409
|
+
if tags:
|
|
410
|
+
tag_map = parse_tags_kv_to_str_map(tags)
|
|
411
|
+
if tag_map:
|
|
412
|
+
config = config.options(tags=tag_map)
|
|
331
413
|
|
|
332
414
|
anyscale.service.deploy(
|
|
333
|
-
|
|
415
|
+
configs,
|
|
334
416
|
in_place=in_place,
|
|
335
417
|
canary_percent=canary_percent,
|
|
336
418
|
max_surge_percent=max_surge_percent,
|
|
419
|
+
versions=versions,
|
|
420
|
+
name=name,
|
|
421
|
+
cloud=cloud,
|
|
422
|
+
project=project,
|
|
337
423
|
)
|
|
338
424
|
|
|
339
425
|
|
|
@@ -409,6 +495,10 @@ def status(
|
|
|
409
495
|
# becomes a common pattern.
|
|
410
496
|
status_dict.get("primary_version", {}).pop("config", None)
|
|
411
497
|
status_dict.get("canary_version", {}).pop("config", None)
|
|
498
|
+
# Remove config from all versions in multi-version services
|
|
499
|
+
if status_dict.get("versions"):
|
|
500
|
+
for version in status_dict.get("versions", []):
|
|
501
|
+
version.pop("config", None)
|
|
412
502
|
|
|
413
503
|
console = Console()
|
|
414
504
|
if json:
|
|
@@ -743,6 +833,17 @@ def _format_service_output_data(svc: ServiceStatus) -> Dict[str, str]:
|
|
|
743
833
|
type=str,
|
|
744
834
|
help="Named project to use; defaults to your org/workspace project.",
|
|
745
835
|
)
|
|
836
|
+
@click.option(
|
|
837
|
+
"--tag",
|
|
838
|
+
"tags",
|
|
839
|
+
multiple=True,
|
|
840
|
+
help=(
|
|
841
|
+
"This option can be repeated to filter by multiple tags. "
|
|
842
|
+
"Tags with the same key are ORed, whereas tags with different keys are ANDed. "
|
|
843
|
+
"Example: --tag team:mlops --tag team:infra --tag env:prod. "
|
|
844
|
+
"Filters with team: (mlops OR infra) AND env:prod."
|
|
845
|
+
),
|
|
846
|
+
)
|
|
746
847
|
@click.option(
|
|
747
848
|
"--created-by-me",
|
|
748
849
|
is_flag=True,
|
|
@@ -811,6 +912,7 @@ def _format_service_output_data(svc: ServiceStatus) -> Dict[str, str]:
|
|
|
811
912
|
def list( # noqa: PLR0913, A001
|
|
812
913
|
service_id: Optional[str],
|
|
813
914
|
name: Optional[str],
|
|
915
|
+
tags: List[str],
|
|
814
916
|
created_by_me: bool,
|
|
815
917
|
cloud: Optional[str],
|
|
816
918
|
project: Optional[str],
|
|
@@ -847,6 +949,7 @@ def list( # noqa: PLR0913, A001
|
|
|
847
949
|
stderr.print("[bold]Listing services with:[/]")
|
|
848
950
|
stderr.print(f"• name = {name or '<any>'}")
|
|
849
951
|
stderr.print(f"• states = {', '.join(state_filter) or '<all>'}")
|
|
952
|
+
stderr.print(f"• tags = {', '.join(tags) or '<none>'}")
|
|
850
953
|
stderr.print(f"• created_by_me = {created_by_me}")
|
|
851
954
|
stderr.print(f"• include_archived= {include_archived}")
|
|
852
955
|
stderr.print(f"• sort = {sort or '<none>'}")
|
|
@@ -879,6 +982,7 @@ def list( # noqa: PLR0913, A001
|
|
|
879
982
|
service_id=service_id,
|
|
880
983
|
name=name,
|
|
881
984
|
state_filter=state_filter,
|
|
985
|
+
tags_filter=parse_repeatable_tags_to_dict(tags) if tags else None,
|
|
882
986
|
creator_id=creator_id,
|
|
883
987
|
cloud=cloud,
|
|
884
988
|
project=project,
|
|
@@ -909,6 +1013,102 @@ def list( # noqa: PLR0913, A001
|
|
|
909
1013
|
sys.exit(1)
|
|
910
1014
|
|
|
911
1015
|
|
|
1016
|
+
@service_cli.group("tags", help="Manage tags for services.")
|
|
1017
|
+
def service_tags_cli() -> None:
|
|
1018
|
+
pass
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
@service_tags_cli.command(name="add", help="Add or update tags on a service.")
|
|
1022
|
+
@click.option("--service-id", "--id", help="ID of the service.")
|
|
1023
|
+
@click.option("--name", "-n", help="Name of the service.")
|
|
1024
|
+
@click.option("--cloud", type=str, help="Cloud name (for name resolution).")
|
|
1025
|
+
@click.option("--project", type=str, help="Project name (for name resolution).")
|
|
1026
|
+
@click.option(
|
|
1027
|
+
"--tag",
|
|
1028
|
+
"tags",
|
|
1029
|
+
multiple=True,
|
|
1030
|
+
help="Tag in key=value (or key:value) format. Repeat to add multiple.",
|
|
1031
|
+
)
|
|
1032
|
+
def tags_add(
|
|
1033
|
+
service_id: Optional[str],
|
|
1034
|
+
name: Optional[str],
|
|
1035
|
+
cloud: Optional[str],
|
|
1036
|
+
project: Optional[str],
|
|
1037
|
+
tags: Tuple[str],
|
|
1038
|
+
) -> None:
|
|
1039
|
+
if not service_id and not name:
|
|
1040
|
+
raise click.ClickException("Provide either --service-id/--id or --name.")
|
|
1041
|
+
tag_map = parse_tags_kv_to_str_map(tags)
|
|
1042
|
+
if not tag_map:
|
|
1043
|
+
raise click.ClickException("Provide at least one --tag key=value.")
|
|
1044
|
+
anyscale.service.add_tags(
|
|
1045
|
+
id=service_id, name=name, cloud=cloud, project=project, tags=tag_map
|
|
1046
|
+
)
|
|
1047
|
+
stderr = Console(stderr=True)
|
|
1048
|
+
ident = service_id or name or "<unknown>"
|
|
1049
|
+
stderr.print(f"Tags updated for service '{ident}'.")
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
@service_tags_cli.command(name="remove", help="Remove tags by key from a service.")
|
|
1053
|
+
@click.option("--service-id", "--id", help="ID of the service.")
|
|
1054
|
+
@click.option("--name", "-n", help="Name of the service.")
|
|
1055
|
+
@click.option("--cloud", type=str, help="Cloud name (for name resolution).")
|
|
1056
|
+
@click.option("--project", type=str, help="Project name (for name resolution).")
|
|
1057
|
+
@click.option("--key", "keys", multiple=True, help="Tag key to remove. Repeatable.")
|
|
1058
|
+
def tags_remove(
|
|
1059
|
+
service_id: Optional[str],
|
|
1060
|
+
name: Optional[str],
|
|
1061
|
+
cloud: Optional[str],
|
|
1062
|
+
project: Optional[str],
|
|
1063
|
+
keys: Tuple[str],
|
|
1064
|
+
) -> None:
|
|
1065
|
+
if not service_id and not name:
|
|
1066
|
+
raise click.ClickException("Provide either --service-id/--id or --name.")
|
|
1067
|
+
key_list = [k for k in keys if k and k.strip()]
|
|
1068
|
+
if not key_list:
|
|
1069
|
+
raise click.ClickException("Provide at least one --key to remove.")
|
|
1070
|
+
anyscale.service.remove_tags(
|
|
1071
|
+
id=service_id, name=name, cloud=cloud, project=project, keys=key_list
|
|
1072
|
+
)
|
|
1073
|
+
stderr = Console(stderr=True)
|
|
1074
|
+
ident = service_id or name or "<unknown>"
|
|
1075
|
+
stderr.print(f"Removed tag keys {key_list} from service '{ident}'.")
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
@service_tags_cli.command(name="list", help="List tags for a service.")
|
|
1079
|
+
@click.option("--service-id", "--id", help="ID of the service.")
|
|
1080
|
+
@click.option("--name", "-n", help="Name of the service.")
|
|
1081
|
+
@click.option("--cloud", type=str, help="Cloud name (for name resolution).")
|
|
1082
|
+
@click.option("--project", type=str, help="Project name (for name resolution).")
|
|
1083
|
+
@click.option("--json", "json_output", is_flag=True, default=False)
|
|
1084
|
+
def tags_list(
|
|
1085
|
+
service_id: Optional[str],
|
|
1086
|
+
name: Optional[str],
|
|
1087
|
+
cloud: Optional[str],
|
|
1088
|
+
project: Optional[str],
|
|
1089
|
+
json_output: bool,
|
|
1090
|
+
) -> None:
|
|
1091
|
+
if not service_id and not name:
|
|
1092
|
+
raise click.ClickException("Provide either --service-id/--id or --name.")
|
|
1093
|
+
if not service_id:
|
|
1094
|
+
svc: ServiceStatus = anyscale.service.status(name=name, cloud=cloud, project=project) # type: ignore
|
|
1095
|
+
service_id = svc.id
|
|
1096
|
+
auth = get_auth_api_client()
|
|
1097
|
+
resp = auth.api_client.get_tags_for_resource_api_v2_tags_resource_get(
|
|
1098
|
+
ResourceTagResourceType.SERVICE, service_id
|
|
1099
|
+
)
|
|
1100
|
+
tags = getattr(resp.result, "tags", [])
|
|
1101
|
+
if json_output:
|
|
1102
|
+
Console().print_json(json=json_dumps([t.to_dict() for t in tags], indent=2))
|
|
1103
|
+
else:
|
|
1104
|
+
stderr = Console(stderr=True)
|
|
1105
|
+
if not tags:
|
|
1106
|
+
stderr.print("No tags found.")
|
|
1107
|
+
return
|
|
1108
|
+
pairs = [(t.key, t.value) for t in tags]
|
|
1109
|
+
stderr.print(build_kv_table(pairs, title="Tags"))
|
|
1110
|
+
|
|
1111
|
+
|
|
912
1112
|
# TODO(mowen): Add cloud support for this when we refactor to new SDK method
|
|
913
1113
|
@service_cli.command(name="rollback", help="Roll back a service.")
|
|
914
1114
|
@click.option(
|