2525 ServerClientError ,
2626 URLNotFoundError ,
2727)
28+ from dstack ._internal .core .models .common import ApplyAction
2829from dstack ._internal .core .models .configurations import ApplyConfigurationType
2930from dstack ._internal .core .models .fleets import (
3031 Fleet ,
@@ -72,7 +73,104 @@ def apply_configuration(
7273 spec = spec ,
7374 )
7475 _print_plan_header (plan )
76+ if plan .action is not None :
77+ self ._apply_plan (plan , command_args )
78+ else :
79+ # Old servers don't support spec update
80+ self ._apply_plan_on_old_server (plan , command_args )
81+
82+ def _apply_plan (self , plan : FleetPlan , command_args : argparse .Namespace ):
83+ delete_fleet_name : Optional [str ] = None
84+ action_message = ""
85+ confirm_message = ""
86+ if plan .current_resource is None :
87+ if plan .spec .configuration .name is not None :
88+ action_message += (
89+ f"Fleet [code]{ plan .spec .configuration .name } [/] does not exist yet."
90+ )
91+ confirm_message += "Create the fleet?"
92+ else :
93+ action_message += f"Found fleet [code]{ plan .spec .configuration .name } [/]."
94+ if plan .action == ApplyAction .CREATE :
95+ delete_fleet_name = plan .current_resource .name
96+ action_message += (
97+ " Configuration changes detected. Cannot update the fleet in-place"
98+ )
99+ confirm_message += "Re-create the fleet?"
100+ elif plan .current_resource .spec == plan .effective_spec :
101+ if command_args .yes and not command_args .force :
102+ # --force is required only with --yes,
103+ # otherwise we may ask for force apply interactively.
104+ console .print (
105+ "No configuration changes detected. Use --force to apply anyway."
106+ )
107+ return
108+ delete_fleet_name = plan .current_resource .name
109+ action_message += " No configuration changes detected."
110+ confirm_message += "Re-create the fleet?"
111+ else :
112+ action_message += " Configuration changes detected."
113+ confirm_message += "Update the fleet in-place?"
114+
115+ console .print (action_message )
116+ if not command_args .yes and not confirm_ask (confirm_message ):
117+ console .print ("\n Exiting..." )
118+ return
119+
120+ if delete_fleet_name is not None :
121+ with console .status ("Deleting existing fleet..." ):
122+ self .api .client .fleets .delete (
123+ project_name = self .api .project , names = [delete_fleet_name ]
124+ )
125+ # Fleet deletion is async. Wait for fleet to be deleted.
126+ while True :
127+ try :
128+ self .api .client .fleets .get (
129+ project_name = self .api .project , name = delete_fleet_name
130+ )
131+ except ResourceNotExistsError :
132+ break
133+ else :
134+ time .sleep (1 )
135+
136+ try :
137+ with console .status ("Applying plan..." ):
138+ fleet = self .api .client .fleets .apply_plan (project_name = self .api .project , plan = plan )
139+ except ServerClientError as e :
140+ raise CLIError (e .msg )
141+ if command_args .detach :
142+ console .print ("Fleet configuration submitted. Exiting..." )
143+ return
144+ try :
145+ with MultiItemStatus (
146+ f"Provisioning [code]{ fleet .name } [/]..." , console = console
147+ ) as live :
148+ while not _finished_provisioning (fleet ):
149+ table = get_fleets_table ([fleet ])
150+ live .update (table )
151+ time .sleep (LIVE_TABLE_PROVISION_INTERVAL_SECS )
152+ fleet = self .api .client .fleets .get (self .api .project , fleet .name )
153+ except KeyboardInterrupt :
154+ if confirm_ask ("Delete the fleet before exiting?" ):
155+ with console .status ("Deleting fleet..." ):
156+ self .api .client .fleets .delete (
157+ project_name = self .api .project , names = [fleet .name ]
158+ )
159+ else :
160+ console .print ("Exiting... Fleet provisioning will continue in the background." )
161+ return
162+ console .print (
163+ get_fleets_table (
164+ [fleet ],
165+ verbose = _failed_provisioning (fleet ),
166+ format_date = local_time ,
167+ )
168+ )
169+ if _failed_provisioning (fleet ):
170+ console .print ("\n [error]Some instances failed. Check the table above for errors.[/]" )
171+ exit (1 )
75172
173+ def _apply_plan_on_old_server (self , plan : FleetPlan , command_args : argparse .Namespace ):
76174 action_message = ""
77175 confirm_message = ""
78176 if plan .current_resource is None :
@@ -86,7 +184,7 @@ def apply_configuration(
86184 diff = diff_models (
87185 old = plan .current_resource .spec .configuration ,
88186 new = plan .spec .configuration ,
89- ignore = {
187+ reset = {
90188 "ssh_config" : {
91189 "ssh_key" : True ,
92190 "proxy_jump" : {"ssh_key" },
0 commit comments