anyscale 0.26.21__py3-none-any.whl → 0.26.23__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 +103 -43
- anyscale/_private/anyscale_client/common.py +38 -8
- anyscale/_private/anyscale_client/fake_anyscale_client.py +98 -27
- anyscale/_private/docgen/models.md +2 -2
- anyscale/_private/models/model_base.py +95 -0
- anyscale/_private/workload/workload_sdk.py +3 -1
- anyscale/aggregated_instance_usage/models.py +4 -4
- anyscale/client/README.md +1 -0
- anyscale/client/openapi_client/api/default_api.py +122 -1
- anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
- anyscale/commands/command_examples.py +4 -0
- anyscale/commands/list_util.py +107 -0
- anyscale/commands/service_commands.py +267 -31
- anyscale/commands/util.py +5 -4
- anyscale/controllers/service_controller.py +7 -86
- anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
- anyscale/service/__init__.py +53 -3
- anyscale/service/_private/service_sdk.py +177 -41
- anyscale/service/commands.py +78 -1
- anyscale/service/models.py +65 -0
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/util.py +35 -1
- anyscale/version.py +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/METADATA +1 -1
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/RECORD +32 -31
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/LICENSE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/NOTICE +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/WHEEL +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/top_level.txt +0 -0
@@ -2,15 +2,18 @@ from io import StringIO
|
|
2
2
|
from json import dumps as json_dumps
|
3
3
|
import pathlib
|
4
4
|
import sys
|
5
|
-
from typing import Any, Dict, Optional, Tuple
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
6
6
|
|
7
7
|
import click
|
8
|
+
from rich.console import Console
|
9
|
+
from rich.table import Table
|
8
10
|
from typing_extensions import Literal
|
9
11
|
import yaml
|
10
12
|
|
11
13
|
from anyscale._private.models.image_uri import ImageURI
|
12
14
|
from anyscale.cli_logger import BlockLogger
|
13
15
|
from anyscale.commands import command_examples
|
16
|
+
from anyscale.commands.list_util import display_list
|
14
17
|
from anyscale.commands.util import (
|
15
18
|
AnyscaleCommand,
|
16
19
|
convert_kv_strings_to_dict,
|
@@ -19,8 +22,21 @@ from anyscale.commands.util import (
|
|
19
22
|
)
|
20
23
|
from anyscale.controllers.service_controller import ServiceController
|
21
24
|
import anyscale.service
|
22
|
-
from anyscale.service import
|
23
|
-
|
25
|
+
from anyscale.service.models import (
|
26
|
+
ServiceConfig,
|
27
|
+
ServiceLogMode,
|
28
|
+
ServiceSortField,
|
29
|
+
ServiceState,
|
30
|
+
ServiceStatus,
|
31
|
+
ServiceVersionStatus,
|
32
|
+
SortOrder,
|
33
|
+
)
|
34
|
+
from anyscale.util import (
|
35
|
+
AnyscaleJSONEncoder,
|
36
|
+
get_endpoint,
|
37
|
+
validate_non_negative_arg,
|
38
|
+
validate_service_state_filter,
|
39
|
+
)
|
24
40
|
|
25
41
|
|
26
42
|
log = BlockLogger() # CLI Logger
|
@@ -389,12 +405,14 @@ def status(
|
|
389
405
|
status_dict.get("primary_version", {}).pop("config", None)
|
390
406
|
status_dict.get("canary_version", {}).pop("config", None)
|
391
407
|
|
408
|
+
console = Console()
|
392
409
|
if json:
|
393
|
-
|
410
|
+
json_str = json_dumps(status_dict, indent=2, cls=AnyscaleJSONEncoder)
|
411
|
+
console.print_json(json=json_str)
|
394
412
|
else:
|
395
413
|
stream = StringIO()
|
396
414
|
yaml.dump(status_dict, stream, sort_keys=False)
|
397
|
-
print(stream.getvalue(), end="")
|
415
|
+
console.print(stream.getvalue(), end="")
|
398
416
|
|
399
417
|
|
400
418
|
@service_cli.command(
|
@@ -624,51 +642,270 @@ def rollout( # noqa: PLR0913
|
|
624
642
|
)
|
625
643
|
|
626
644
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
645
|
+
MAX_PAGE_SIZE = 50
|
646
|
+
NON_INTERACTIVE_DEFAULT_MAX_ITEMS = 10
|
647
|
+
|
648
|
+
|
649
|
+
def validate_page_size(ctx, param, value):
|
650
|
+
value = validate_non_negative_arg(ctx, param, value)
|
651
|
+
if value is not None and value > MAX_PAGE_SIZE:
|
652
|
+
raise click.BadParameter(f"must be less than or equal to {MAX_PAGE_SIZE}.")
|
653
|
+
return value
|
654
|
+
|
655
|
+
|
656
|
+
def validate_max_items(ctx, param, value):
|
657
|
+
if value is None:
|
658
|
+
return None
|
659
|
+
return validate_non_negative_arg(ctx, param, value)
|
660
|
+
|
661
|
+
|
662
|
+
def _parse_sort_option(sort: Optional[str],) -> Tuple[Optional[str], SortOrder]:
|
663
|
+
"""
|
664
|
+
Given a raw sort string (e.g. "-created_at"), return
|
665
|
+
(canonical_field_name, SortOrder).
|
666
|
+
"""
|
667
|
+
if not sort:
|
668
|
+
return None, SortOrder.ASC
|
669
|
+
|
670
|
+
# build case-insensitive map of allowed fields
|
671
|
+
allowed = {f.value.lower(): f.value for f in ServiceSortField.__members__.values()}
|
672
|
+
|
673
|
+
# detect leading '-' for descending
|
674
|
+
if sort.startswith("-"):
|
675
|
+
raw = sort[1:]
|
676
|
+
order = SortOrder.DESC
|
677
|
+
else:
|
678
|
+
raw = sort
|
679
|
+
order = SortOrder.ASC
|
680
|
+
|
681
|
+
key = raw.lower()
|
682
|
+
if key not in allowed:
|
683
|
+
allowed_names = ", ".join(sorted(allowed.values()))
|
684
|
+
raise click.BadParameter(
|
685
|
+
f"Invalid sort field '{raw}'. Allowed fields: {allowed_names}"
|
686
|
+
)
|
687
|
+
|
688
|
+
return allowed[key], order
|
689
|
+
|
690
|
+
|
691
|
+
def _create_service_list_table(show_header: bool) -> Table:
|
692
|
+
table = Table(show_header=show_header, expand=True)
|
693
|
+
# NAME and ID: larger ratios, can wrap but never truncate
|
694
|
+
table.add_column(
|
695
|
+
"NAME", no_wrap=False, overflow="fold", ratio=3, min_width=15,
|
696
|
+
)
|
697
|
+
table.add_column(
|
698
|
+
"ID", no_wrap=False, overflow="fold", ratio=2, min_width=12,
|
699
|
+
)
|
700
|
+
# all other columns will wrap as needed
|
701
|
+
for heading in (
|
702
|
+
"CURRENT STATE",
|
703
|
+
"CREATOR",
|
704
|
+
"PROJECT",
|
705
|
+
"LAST DEPLOYED AT",
|
706
|
+
):
|
707
|
+
table.add_column(
|
708
|
+
heading, no_wrap=False, overflow="fold", ratio=1, min_width=8,
|
709
|
+
)
|
710
|
+
|
711
|
+
return table
|
712
|
+
|
713
|
+
|
714
|
+
def _format_service_output_data(svc: ServiceStatus) -> Dict[str, str]:
|
715
|
+
last_deployed_at = ""
|
716
|
+
if isinstance(svc.primary_version, ServiceVersionStatus):
|
717
|
+
last_deployed_at = svc.primary_version.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
718
|
+
|
719
|
+
return {
|
720
|
+
"name": svc.name,
|
721
|
+
"id": svc.id,
|
722
|
+
"current_state": str(svc.state),
|
723
|
+
"creator": str(svc.creator or ""),
|
724
|
+
"project": str(svc.project or ""),
|
725
|
+
"last_deployed_at": last_deployed_at,
|
726
|
+
}
|
727
|
+
|
728
|
+
|
729
|
+
@service_cli.command(
|
730
|
+
name="list", help="List services.", cls=AnyscaleCommand,
|
631
731
|
)
|
732
|
+
@click.option("--service-id", "--id", help="ID of the service to display.")
|
733
|
+
@click.option("--name", "-n", help="Name of the service to display.")
|
632
734
|
@click.option(
|
633
|
-
"--
|
735
|
+
"--cloud",
|
736
|
+
type=str,
|
737
|
+
help="The Anyscale Cloud of this workload; defaults to your org/workspace cloud.",
|
634
738
|
)
|
635
739
|
@click.option(
|
636
|
-
"--project
|
740
|
+
"--project",
|
741
|
+
type=str,
|
742
|
+
help="Named project to use; defaults to your org/workspace project.",
|
637
743
|
)
|
638
744
|
@click.option(
|
639
745
|
"--created-by-me",
|
746
|
+
is_flag=True,
|
747
|
+
default=False,
|
640
748
|
help="List services created by me only.",
|
749
|
+
)
|
750
|
+
@click.option(
|
751
|
+
"--state",
|
752
|
+
"-s",
|
753
|
+
"state_filter",
|
754
|
+
multiple=True,
|
755
|
+
callback=validate_service_state_filter,
|
756
|
+
help=(
|
757
|
+
"Filter by service state (repeatable). "
|
758
|
+
f"Allowed: {', '.join(s.value for s in ServiceState)}"
|
759
|
+
),
|
760
|
+
)
|
761
|
+
@click.option(
|
762
|
+
"--include-archived",
|
641
763
|
is_flag=True,
|
642
764
|
default=False,
|
765
|
+
help="Include archived services.",
|
643
766
|
)
|
644
767
|
@click.option(
|
645
768
|
"--max-items",
|
646
|
-
required=False,
|
647
|
-
default=10,
|
648
769
|
type=int,
|
649
|
-
|
650
|
-
|
770
|
+
callback=validate_max_items,
|
771
|
+
help="Max total items (only with --no-interactive).",
|
651
772
|
)
|
652
|
-
|
653
|
-
|
773
|
+
@click.option(
|
774
|
+
"--page-size",
|
775
|
+
type=int,
|
776
|
+
default=10,
|
777
|
+
show_default=True,
|
778
|
+
callback=validate_page_size,
|
779
|
+
help=f"Items per page (max {MAX_PAGE_SIZE}).",
|
780
|
+
)
|
781
|
+
@click.option(
|
782
|
+
"--interactive/--no-interactive",
|
783
|
+
default=True,
|
784
|
+
show_default=True,
|
785
|
+
help="Use interactive paging.",
|
786
|
+
)
|
787
|
+
@click.option(
|
788
|
+
"--sort",
|
789
|
+
help=(
|
790
|
+
"Sort by FIELD (prefix with '-' for desc). "
|
791
|
+
f"Allowed: {', '.join(f.value for f in ServiceSortField.__members__.values())}"
|
792
|
+
),
|
793
|
+
)
|
794
|
+
@click.option(
|
795
|
+
"-v",
|
796
|
+
"--verbose",
|
797
|
+
is_flag=True,
|
798
|
+
default=False,
|
799
|
+
help="Include full config in JSON output.",
|
800
|
+
)
|
801
|
+
@click.option(
|
802
|
+
"-j",
|
803
|
+
"--json",
|
804
|
+
"json_output",
|
805
|
+
is_flag=True,
|
806
|
+
default=False,
|
807
|
+
help="Emit structured JSON to stdout.",
|
808
|
+
)
|
809
|
+
def list( # noqa: PLR0913, A001
|
654
810
|
service_id: Optional[str],
|
655
|
-
|
811
|
+
name: Optional[str],
|
656
812
|
created_by_me: bool,
|
657
|
-
|
813
|
+
cloud: Optional[str],
|
814
|
+
project: Optional[str],
|
815
|
+
state_filter: List[str],
|
816
|
+
include_archived: bool,
|
817
|
+
max_items: Optional[int],
|
818
|
+
page_size: int,
|
819
|
+
sort: Optional[str],
|
820
|
+
json_output: bool,
|
821
|
+
interactive: bool,
|
822
|
+
verbose: bool,
|
658
823
|
):
|
659
|
-
"""List services based on the provided filters.
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
824
|
+
"""List services based on the provided filters."""
|
825
|
+
if max_items is not None and interactive:
|
826
|
+
raise click.UsageError("--max-items only allowed with --no-interactive")
|
827
|
+
|
828
|
+
# parse sort
|
829
|
+
sort_field, sort_order = _parse_sort_option(sort)
|
830
|
+
|
831
|
+
# normalize max_items
|
832
|
+
effective_max = max_items
|
833
|
+
if not interactive and effective_max is None:
|
834
|
+
stderr = Console(stderr=True)
|
835
|
+
stderr.print(
|
836
|
+
f"Defaulting to {NON_INTERACTIVE_DEFAULT_MAX_ITEMS} items in batch mode; "
|
837
|
+
"use --max-items to override."
|
838
|
+
)
|
839
|
+
effective_max = NON_INTERACTIVE_DEFAULT_MAX_ITEMS
|
840
|
+
|
841
|
+
console = Console()
|
842
|
+
stderr = Console(stderr=True)
|
843
|
+
|
844
|
+
# diagnostics
|
845
|
+
stderr.print("[bold]Listing services with:[/]")
|
846
|
+
stderr.print(f"• name = {name or '<any>'}")
|
847
|
+
stderr.print(f"• states = {', '.join(state_filter) or '<all>'}")
|
848
|
+
stderr.print(f"• created_by_me = {created_by_me}")
|
849
|
+
stderr.print(f"• include_archived= {include_archived}")
|
850
|
+
stderr.print(f"• sort = {sort or '<none>'}")
|
851
|
+
stderr.print(f"• mode = {'interactive' if interactive else 'batch'}")
|
852
|
+
stderr.print(f"• per-page limit = {page_size}")
|
853
|
+
stderr.print(f"• max-items total = {effective_max or 'all'}")
|
854
|
+
stderr.print(f"\nView your Services in the UI at {get_endpoint('/services')}\n")
|
855
|
+
|
856
|
+
creator_id = (
|
857
|
+
ServiceController().get_authenticated_user_id() if created_by_me else None
|
670
858
|
)
|
671
859
|
|
860
|
+
# choose formatter
|
861
|
+
if json_output:
|
862
|
+
|
863
|
+
def json_formatter(svc: ServiceStatus) -> Dict[str, Any]:
|
864
|
+
data = svc.to_dict()
|
865
|
+
if not verbose:
|
866
|
+
data.get("primary_version", {}).pop("config", None)
|
867
|
+
data.get("canary_version", {}).pop("config", None)
|
868
|
+
return data
|
869
|
+
|
870
|
+
formatter = json_formatter
|
871
|
+
else:
|
872
|
+
formatter = _format_service_output_data
|
873
|
+
|
874
|
+
total = 0
|
875
|
+
try:
|
876
|
+
iterator = anyscale.service.list(
|
877
|
+
service_id=service_id,
|
878
|
+
name=name,
|
879
|
+
state_filter=state_filter,
|
880
|
+
creator_id=creator_id,
|
881
|
+
cloud=cloud,
|
882
|
+
project=project,
|
883
|
+
include_archived=include_archived,
|
884
|
+
max_items=None if interactive else effective_max,
|
885
|
+
page_size=page_size,
|
886
|
+
sort_field=sort_field,
|
887
|
+
sort_order=sort_order,
|
888
|
+
)
|
889
|
+
total = display_list(
|
890
|
+
iterator=iter(iterator),
|
891
|
+
item_formatter=formatter,
|
892
|
+
table_creator=_create_service_list_table,
|
893
|
+
json_output=json_output,
|
894
|
+
page_size=page_size,
|
895
|
+
interactive=interactive,
|
896
|
+
max_items=effective_max,
|
897
|
+
console=console,
|
898
|
+
)
|
899
|
+
|
900
|
+
if not json_output:
|
901
|
+
if total > 0:
|
902
|
+
stderr.print(f"\nFetched {total} services.")
|
903
|
+
else:
|
904
|
+
stderr.print("\nNo services found.")
|
905
|
+
except Exception as e: # noqa: BLE001
|
906
|
+
log.error(f"Failed to list services: {e}")
|
907
|
+
sys.exit(1)
|
908
|
+
|
672
909
|
|
673
910
|
# TODO(mowen): Add cloud support for this when we refactor to new SDK method
|
674
911
|
@service_cli.command(name="rollback", help="Roll back a service.")
|
@@ -708,7 +945,6 @@ def rollback(
|
|
708
945
|
service_controller.rollback(service_id, max_surge_percent)
|
709
946
|
|
710
947
|
|
711
|
-
# TODO(mowen): Add cloud support for this when we refactor to new SDK method
|
712
948
|
@service_cli.command(name="terminate", help="Terminate a service.")
|
713
949
|
@click.option(
|
714
950
|
"--service-id", "--id", required=False, help="ID of service.",
|
anyscale/commands/util.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from copy import deepcopy
|
2
|
-
from typing import Dict, Tuple
|
2
|
+
from typing import Dict, Tuple, TypeVar
|
3
3
|
|
4
4
|
import click
|
5
5
|
|
@@ -136,9 +136,10 @@ def convert_kv_strings_to_dict(strings: Tuple[str]) -> Dict[str, str]:
|
|
136
136
|
return ret_dict
|
137
137
|
|
138
138
|
|
139
|
-
|
140
|
-
|
141
|
-
|
139
|
+
T = TypeVar("T", bound=WorkloadConfig)
|
140
|
+
|
141
|
+
|
142
|
+
def override_env_vars(config: T, overrides: Dict[str, str]) -> T:
|
142
143
|
"""Returns a new copy of the WorkloadConfig with env vars overridden.
|
143
144
|
|
144
145
|
This is a per-key override, so keys already in the config that are not specified in
|
@@ -2,7 +2,6 @@ import os
|
|
2
2
|
from typing import Any, Dict, List, Optional
|
3
3
|
|
4
4
|
import click
|
5
|
-
import tabulate
|
6
5
|
import yaml
|
7
6
|
|
8
7
|
from anyscale import AnyscaleSDK
|
@@ -14,9 +13,6 @@ from anyscale.client.openapi_client.api.default_api import DefaultApi as Interna
|
|
14
13
|
from anyscale.client.openapi_client.models.decorated_list_service_api_model import (
|
15
14
|
DecoratedListServiceAPIModel,
|
16
15
|
)
|
17
|
-
from anyscale.client.openapi_client.models.decorated_production_service_v2_api_model import (
|
18
|
-
DecoratedProductionServiceV2APIModel,
|
19
|
-
)
|
20
16
|
from anyscale.controllers.base_controller import BaseController
|
21
17
|
from anyscale.models.service_model import ServiceConfig
|
22
18
|
from anyscale.project_utils import infer_project_id
|
@@ -81,6 +77,12 @@ class ServiceController(BaseController):
|
|
81
77
|
max_surge_percent=config.max_surge_percent,
|
82
78
|
)
|
83
79
|
|
80
|
+
def get_authenticated_user_id(self) -> str:
|
81
|
+
user_info_response = (
|
82
|
+
self.internal_api_client.get_user_info_api_v2_userinfo_get()
|
83
|
+
)
|
84
|
+
return user_info_response.result.id
|
85
|
+
|
84
86
|
def _get_services_by_name(
|
85
87
|
self,
|
86
88
|
*,
|
@@ -93,12 +95,7 @@ class ServiceController(BaseController):
|
|
93
95
|
|
94
96
|
Note that "name" is an *exact match* (different from the REST API semantics).
|
95
97
|
"""
|
96
|
-
creator_id = None
|
97
|
-
if created_by_me:
|
98
|
-
user_info_response = (
|
99
|
-
self.internal_api_client.get_user_info_api_v2_userinfo_get()
|
100
|
-
)
|
101
|
-
creator_id = user_info_response.result.id
|
98
|
+
creator_id = self.get_authenticated_user_id() if created_by_me else None
|
102
99
|
|
103
100
|
paging_token = None
|
104
101
|
services_list: List[DecoratedListServiceAPIModel] = []
|
@@ -364,75 +361,6 @@ class ServiceController(BaseController):
|
|
364
361
|
log=self.log,
|
365
362
|
)
|
366
363
|
|
367
|
-
def list(
|
368
|
-
self,
|
369
|
-
*,
|
370
|
-
name: Optional[str] = None,
|
371
|
-
service_id: Optional[str] = None,
|
372
|
-
project_id: Optional[str] = None,
|
373
|
-
created_by_me: bool = False,
|
374
|
-
max_items: int = 10,
|
375
|
-
) -> None:
|
376
|
-
self._list_via_service_list_endpoint(
|
377
|
-
name=name,
|
378
|
-
service_id=service_id,
|
379
|
-
project_id=project_id,
|
380
|
-
created_by_me=created_by_me,
|
381
|
-
max_items=max_items,
|
382
|
-
)
|
383
|
-
|
384
|
-
def _list_via_service_list_endpoint(
|
385
|
-
self,
|
386
|
-
*,
|
387
|
-
name: Optional[str] = None,
|
388
|
-
service_id: Optional[str] = None,
|
389
|
-
project_id: Optional[str] = None,
|
390
|
-
created_by_me: bool = False,
|
391
|
-
max_items: int,
|
392
|
-
):
|
393
|
-
services = []
|
394
|
-
if service_id:
|
395
|
-
service_v2_result: DecoratedProductionServiceV2APIModel = (
|
396
|
-
self.internal_api_client.get_service_api_v2_services_v2_service_id_get(
|
397
|
-
service_id
|
398
|
-
).result
|
399
|
-
)
|
400
|
-
services.append(
|
401
|
-
[
|
402
|
-
service_v2_result.name,
|
403
|
-
service_v2_result.id,
|
404
|
-
service_v2_result.current_state,
|
405
|
-
# TODO: change to email once https://github.com/anyscale/product/pull/18189 is merged
|
406
|
-
service_v2_result.creator_id,
|
407
|
-
]
|
408
|
-
)
|
409
|
-
|
410
|
-
else:
|
411
|
-
services_data = self._get_services_by_name(
|
412
|
-
name=name,
|
413
|
-
project_id=project_id,
|
414
|
-
created_by_me=created_by_me,
|
415
|
-
max_items=max_items,
|
416
|
-
)
|
417
|
-
|
418
|
-
services = [
|
419
|
-
[
|
420
|
-
service.name,
|
421
|
-
service.id,
|
422
|
-
service.current_state,
|
423
|
-
service.creator.email,
|
424
|
-
]
|
425
|
-
for service in services_data
|
426
|
-
]
|
427
|
-
|
428
|
-
table = tabulate.tabulate(
|
429
|
-
services,
|
430
|
-
headers=["NAME", "ID", "CURRENT STATE", "CREATOR"],
|
431
|
-
tablefmt="plain",
|
432
|
-
)
|
433
|
-
self.log.info(f'View your Services in the UI at {get_endpoint("/services")}')
|
434
|
-
self.log.info(f"Services:\n{table}")
|
435
|
-
|
436
364
|
def rollback(self, service_id: str, max_surge_percent: Optional[int] = None):
|
437
365
|
service: ServiceModel = self.sdk.rollback_service(
|
438
366
|
service_id,
|
@@ -445,10 +373,3 @@ class ServiceController(BaseController):
|
|
445
373
|
self.log.info(
|
446
374
|
f'View the service in the UI at {get_endpoint(f"/services/{service.id}")}'
|
447
375
|
)
|
448
|
-
|
449
|
-
def terminate(self, service_id: str):
|
450
|
-
service: ServiceModel = self.sdk.terminate_service(service_id).result
|
451
|
-
self.log.info(f"Service {service.id} terminate initiated.")
|
452
|
-
self.log.info(
|
453
|
-
f'View the service in the UI at {get_endpoint(f"/services/{service.id}")}'
|
454
|
-
)
|