3434 InstanceGroupPlacement ,
3535)
3636from dstack ._internal .core .models .instances import InstanceStatus , SSHKey
37- from dstack ._internal .core .services .diff import diff_models
37+ from dstack ._internal .core .services .diff import copy_model , diff_models
3838from dstack ._internal .utils .common import local_time
3939from dstack ._internal .utils .logging import get_logger
40+ from dstack ._internal .utils .nested_list import NestedList , NestedListItem
4041from dstack ._internal .utils .ssh import convert_ssh_key_to_pem , generate_public_key , pkey_from_str
4142from dstack .api .utils import load_profile
4243
@@ -85,14 +86,10 @@ def _apply_plan(self, plan: FleetPlan, command_args: argparse.Namespace):
8586 )
8687 confirm_message += "Create the fleet?"
8788 else :
89+ effective_spec = plan .get_effective_spec ()
90+ diff = _render_fleet_spec_diff (plan .current_resource .spec , effective_spec )
8891 action_message += f"Found fleet [code]{ plan .spec .configuration .name } [/]."
89- if plan .action == ApplyAction .CREATE :
90- delete_fleet_name = plan .current_resource .name
91- action_message += (
92- " Configuration changes detected. Cannot update the fleet in-place"
93- )
94- confirm_message += "Re-create the fleet?"
95- elif plan .current_resource .spec == plan .effective_spec :
92+ if plan .current_resource .spec == effective_spec :
9693 if command_args .yes and not command_args .force :
9794 # --force is required only with --yes,
9895 # otherwise we may ask for force apply interactively.
@@ -103,8 +100,26 @@ def _apply_plan(self, plan: FleetPlan, command_args: argparse.Namespace):
103100 delete_fleet_name = plan .current_resource .name
104101 action_message += " No configuration changes detected."
105102 confirm_message += "Re-create the fleet?"
103+ elif plan .action == ApplyAction .CREATE :
104+ delete_fleet_name = plan .current_resource .name
105+ if diff is not None :
106+ # TODO: Highlight only the fields that block in-place update instead of
107+ # showing the full detected diff here.
108+ action_message += (
109+ f" Detected changes that [error]cannot[/] be updated in-place:\n { diff } "
110+ )
111+ else :
112+ action_message += (
113+ " Configuration changes detected. Cannot update the fleet in-place."
114+ )
115+ confirm_message += "Re-create the fleet?"
106116 else :
107- action_message += " Configuration changes detected."
117+ if diff is not None :
118+ action_message += (
119+ f" Detected changes that [code]can[/] be updated in-place:\n { diff } "
120+ )
121+ else :
122+ action_message += " Configuration changes detected."
108123 confirm_message += "Update the fleet in-place?"
109124
110125 console .print (action_message )
@@ -357,6 +372,44 @@ def _resolve_ssh_key(ssh_key_path: Optional[str]) -> Optional[SSHKey]:
357372 exit ()
358373
359374
375+ def _render_fleet_spec_diff (old_spec : FleetSpec , new_spec : FleetSpec ) -> Optional [str ]:
376+ old_spec = copy_model (old_spec )
377+ new_spec = copy_model (new_spec )
378+ changed_spec_fields = list (diff_models (old_spec , new_spec ))
379+ if not changed_spec_fields :
380+ return None
381+
382+ nested_list = NestedList ()
383+ for spec_field in changed_spec_fields :
384+ if spec_field == "merged_profile" :
385+ continue
386+ if spec_field == "configuration" :
387+ item = NestedListItem (
388+ "Configuration properties:" ,
389+ children = [
390+ NestedListItem (field )
391+ for field in diff_models (old_spec .configuration , new_spec .configuration )
392+ ],
393+ )
394+ elif spec_field == "profile" :
395+ item = NestedListItem (
396+ "Profile properties:" ,
397+ children = [
398+ NestedListItem (field )
399+ for field in diff_models (old_spec .profile , new_spec .profile )
400+ ],
401+ )
402+ elif spec_field == "configuration_path" :
403+ item = NestedListItem ("Configuration path" )
404+ else :
405+ item = NestedListItem (spec_field .replace ("_" , " " ).capitalize ())
406+ nested_list .children .append (item )
407+
408+ if not nested_list .children :
409+ return None
410+ return nested_list .render ()
411+
412+
360413def _print_plan_header (plan : FleetPlan ):
361414 def th (s : str ) -> str :
362415 return f"[bold]{ s } [/bold]"
0 commit comments