@@ -208,6 +208,10 @@ def apply(self, force=False):
208208 self ._throw_for_no_raycluster ()
209209 namespace = self .config .namespace
210210 name = self .config .name
211+
212+ # Regenerate resource_yaml to reflect any configuration changes
213+ self .resource_yaml = self .create_resource ()
214+
211215 try :
212216 self .config_check ()
213217 api_instance = client .CustomObjectsApi (get_api_client ())
@@ -387,16 +391,24 @@ def is_dashboard_ready(self) -> bool:
387391 bool:
388392 True if the dashboard is ready, False otherwise.
389393 """
394+ dashboard_uri = self .cluster_dashboard_uri ()
395+ if dashboard_uri is None :
396+ return False
397+
390398 try :
391399 response = requests .get (
392- self . cluster_dashboard_uri () ,
400+ dashboard_uri ,
393401 headers = self ._client_headers ,
394402 timeout = 5 ,
395403 verify = self ._client_verify_tls ,
396404 )
397405 except requests .exceptions .SSLError : # pragma no cover
398406 # SSL exception occurs when oauth ingress has been created but cluster is not up
399407 return False
408+ except Exception : # pragma no cover
409+ # Any other exception (connection errors, timeouts, etc.)
410+ return False
411+
400412 if response .status_code == 200 :
401413 return True
402414 else :
@@ -523,7 +535,24 @@ def cluster_dashboard_uri(self) -> str:
523535 elif "route.openshift.io/termination" in annotations :
524536 protocol = "https"
525537 return f"{ protocol } ://{ ingress .spec .rules [0 ].host } "
526- return "Dashboard not available yet, have you run cluster.up()?"
538+
539+ # For local/test environments without ingress controller (e.g., KIND)
540+ # Try to find the Ray head service
541+ try :
542+ api_instance = client .CoreV1Api (get_api_client ())
543+ services = api_instance .list_namespaced_service (
544+ self .config .namespace ,
545+ label_selector = f"ray.io/cluster={ self .config .name } ,ray.io/node-type=head" ,
546+ )
547+ for service in services .items :
548+ if service .metadata .name == f"{ self .config .name } -head-svc" :
549+ # For ClusterIP services in local environments, return a placeholder
550+ # The actual connection would need port-forwarding or NodePort
551+ return f"http://{ service .metadata .name } .{ self .config .namespace } .svc.cluster.local:8265"
552+ except Exception : # pragma: no cover
553+ pass
554+
555+ return None
527556
528557 def list_jobs (self ) -> List :
529558 """
@@ -783,6 +812,12 @@ def remove_autogenerated_fields(resource):
783812 del resource [key ]
784813 else :
785814 remove_autogenerated_fields (resource [key ])
815+
816+ # After cleaning, remove empty metadata sections
817+ if "metadata" in resource and isinstance (resource ["metadata" ], dict ):
818+ if len (resource ["metadata" ]) == 0 :
819+ del resource ["metadata" ]
820+
786821 elif isinstance (resource , list ):
787822 for item in resource :
788823 remove_autogenerated_fields (item )
0 commit comments