1111)
1212from dstack ._internal .cli .utils .gateway import get_gateways_table
1313from dstack ._internal .cli .utils .rich import MultiItemStatus
14- from dstack ._internal .core .errors import ResourceNotExistsError
14+ from dstack ._internal .core .errors import (
15+ MethodNotAllowedError ,
16+ ResourceNotExistsError ,
17+ )
18+ from dstack ._internal .core .models .common import ApplyAction
1519from dstack ._internal .core .models .configurations import ApplyConfigurationType
1620from dstack ._internal .core .models .gateways import (
21+ ApplyGatewayPlanInput ,
1722 Gateway ,
1823 GatewayConfiguration ,
1924 GatewayPlan ,
2328from dstack ._internal .core .services .diff import diff_models
2429from dstack ._internal .utils .common import local_time
2530from dstack ._internal .utils .logging import get_logger
31+ from dstack ._internal .utils .nested_list import NestedList , NestedListItem
2632from dstack .api ._public import Client
2733
2834logger = get_logger (__name__ )
@@ -51,26 +57,31 @@ def apply_configuration(
5157 " https://dstack.ai/docs/concepts/services/#pd-disaggregation"
5258 )
5359 with console .status ("Getting apply plan..." ):
54- plan = _get_plan (api = self .api , spec = spec )
60+ try :
61+ plan = self .api .client .gateways .get_plan (project_name = self .api .project , spec = spec )
62+ use_legacy_api = False
63+ except MethodNotAllowedError :
64+ # pre-0.20.27 server
65+ plan = _get_plan_legacy (self .api , spec )
66+ use_legacy_api = True
5567 _print_plan_header (plan )
5668
5769 action_message = ""
5870 confirm_message = ""
71+ delete_gateway_name = None
5972 if plan .current_resource is None :
60- if plan .spec .configuration .name is not None :
61- action_message += (
62- f"Gateway [code]{ plan .spec .configuration .name } [/] does not exist yet."
63- )
73+ if plan .effective_spec .configuration .name is not None :
74+ action_message += f"Gateway [code]{ plan .effective_spec .configuration .name } [/] does not exist yet."
6475 confirm_message += "Create the gateway?"
6576 else :
66- action_message += f"Found gateway [code]{ plan .spec .configuration .name } [/]."
77+ action_message += f"Found gateway [code]{ plan .effective_spec .configuration .name } [/]."
6778 diff = diff_models (
68- plan .spec .configuration ,
6979 plan .current_resource .configuration ,
80+ plan .effective_spec .configuration ,
7081 )
7182 changed_fields = list (diff .keys ())
7283 if (
73- plan .current_resource .configuration == plan .spec .configuration
84+ plan .current_resource .configuration == plan .effective_spec .configuration
7485 or changed_fields == ["default" ]
7586 ):
7687 if command_args .yes and not command_args .force :
@@ -82,38 +93,61 @@ def apply_configuration(
8293 return
8394 action_message += " No configuration changes detected."
8495 confirm_message += "Re-create the gateway?"
96+ delete_gateway_name = plan .current_resource .name
8597 else :
86- action_message += " Configuration changes detected."
87- confirm_message += "Re-create the gateway?"
98+ formatted_diff = NestedList (
99+ children = [NestedListItem (field ) for field in diff ]
100+ ).render ()
101+ if plan .action == ApplyAction .UPDATE :
102+ action_message += f" Detected changes that [code]can[/] be updated in-place:\n { formatted_diff } "
103+ confirm_message += "Update the gateway?"
104+ else :
105+ action_message += f" Detected changes that [error]cannot[/] be updated in-place:\n { formatted_diff } "
106+ confirm_message += "Re-create the gateway?"
107+ delete_gateway_name = plan .current_resource .name
88108
89109 console .print (action_message )
90110 if not command_args .yes and not confirm_ask (confirm_message ):
91111 console .print ("\n Exiting..." )
92112 return
93113
94- if plan . current_resource is not None :
114+ if delete_gateway_name is not None :
95115 with console .status ("Deleting existing gateway..." ):
96116 self .api .client .gateways .delete (
97117 project_name = self .api .project ,
98- gateways_names = [plan . current_resource . name ],
118+ gateways_names = [delete_gateway_name ],
99119 )
100120 # Gateway deletion is async. Wait for gateway to be deleted.
101121 while True :
102122 try :
103123 self .api .client .gateways .get (
104124 project_name = self .api .project ,
105- gateway_name = plan . current_resource . name ,
125+ gateway_name = delete_gateway_name ,
106126 )
107127 except ResourceNotExistsError :
108128 break
109129 else :
110130 time .sleep (1 )
111131
112- with console .status ("Creating gateway..." ):
113- gateway = self .api .client .gateways .create (
114- project_name = self .api .project ,
115- configuration = conf ,
116- )
132+ with console .status ("Applying plan..." ):
133+ if use_legacy_api :
134+ gateway = self .api .client .gateways .create (
135+ project_name = self .api .project ,
136+ configuration = conf ,
137+ )
138+ else :
139+ gateway = self .api .client .gateways .apply_plan (
140+ project_name = self .api .project ,
141+ plan = ApplyGatewayPlanInput (
142+ spec = spec ,
143+ current_resource = plan .current_resource ,
144+ ),
145+ )
146+
147+ if plan .action == ApplyAction .UPDATE and delete_gateway_name is None :
148+ console .print (get_gateways_table ([gateway ], current_project = self .api .project ))
149+ return
150+
117151 if command_args .detach :
118152 console .print ("Gateway configuration submitted. Exiting..." )
119153 return
@@ -195,8 +229,7 @@ def apply_args(self, conf: GatewayConfiguration, args: argparse.Namespace):
195229 conf .name = args .name
196230
197231
198- def _get_plan (api : Client , spec : GatewaySpec ) -> GatewayPlan :
199- # TODO: Implement server-side /get_plan with an offer included
232+ def _get_plan_legacy (api : Client , spec : GatewaySpec ) -> GatewayPlan :
200233 user = api .client .users .get_my_user ()
201234 current_resource = None
202235 if spec .configuration .name is not None :
@@ -211,7 +244,9 @@ def _get_plan(api: Client, spec: GatewaySpec) -> GatewayPlan:
211244 project_name = api .project ,
212245 user = user .username ,
213246 spec = spec ,
247+ effective_spec = spec ,
214248 current_resource = current_resource ,
249+ action = ApplyAction .CREATE ,
215250 )
216251
217252
@@ -225,20 +260,22 @@ def th(s: str) -> str:
225260
226261 configuration_table .add_row (th ("Project" ), plan .project_name )
227262 configuration_table .add_row (th ("User" ), plan .user )
228- configuration_table .add_row (th ("Configuration" ), plan .spec .configuration_path )
229- configuration_table .add_row (th ("Type" ), plan .spec .configuration .type )
263+ configuration_table .add_row (th ("Configuration" ), plan .effective_spec .configuration_path )
264+ configuration_table .add_row (th ("Type" ), plan .effective_spec .configuration .type )
230265
231266 domain = "-"
232- if plan .spec .configuration .domain is not None :
233- domain = plan .spec .configuration .domain
267+ if plan .effective_spec .configuration .domain is not None :
268+ domain = plan .effective_spec .configuration .domain
234269
235- configuration_table .add_row (th ("Backend" ), plan .spec .configuration .backend .value )
236- configuration_table .add_row (th ("Region" ), plan .spec .configuration .region )
270+ configuration_table .add_row (th ("Backend" ), plan .effective_spec .configuration .backend .value )
271+ configuration_table .add_row (th ("Region" ), plan .effective_spec .configuration .region )
237272 configuration_table .add_row (th ("Domain" ), domain )
238273
239- if plan .spec .configuration .replicas is not None :
240- assert isinstance (plan .spec .configuration .replicas , int )
241- configuration_table .add_row (th ("Replicas" ), str (plan .spec .configuration .replicas ))
274+ if plan .effective_spec .configuration .replicas is not None :
275+ assert isinstance (plan .effective_spec .configuration .replicas , int )
276+ configuration_table .add_row (
277+ th ("Replicas" ), str (plan .effective_spec .configuration .replicas )
278+ )
242279
243280 console .print (configuration_table )
244281 console .print ()
0 commit comments