Implementation reference. These symbols describe how the project works; their presence here does not make them supported application imports. Use the documented entry points and support policy.
@router.websocket("/ws/logs/{namespace}/{app_id}")asyncdefapp_logs_websocket(websocket:WebSocket,namespace:str,app_id:str,tail:int=Query(default=-1,ge=-1,le=10000)):"""Stream literal log text and signal failures through the WebSocket close state."""ifnot_logs_origin_allowed(websocket):awaitwebsocket.close(code=1008,reason="Browser origin is not permitted.")returnifnotnamespace_scope().allows(namespace):note(code="namespace_forbidden")awaitwebsocket.close(code=1008,reason="The namespace is outside this API's namespace scope.")returnawaitwebsocket.accept()manager=AsyncSparkAppManager(k8s_client_manager=KubernetesClientSingleton.client_manager)asyncdefsend_logs():asyncwithaclosing(manager.logs_streamer(namespace=namespace,app_id=app_id,tail_lines=tail))aslogs:asyncforloginlogs:awaitwebsocket.send_text(log)asyncdefwait_for_disconnect():whileTrue:message=awaitwebsocket.receive()ifmessage["type"]=="websocket.disconnect":returnproducer=asyncio.create_task(send_logs())disconnected=asyncio.create_task(wait_for_disconnect())code,reason=1000,""try:finished,_=awaitasyncio.wait({producer,disconnected},return_when=asyncio.FIRST_COMPLETED)ifdisconnectedinfinished:awaitdisconnectedreturnawaitproducerexceptWebSocketDisconnect:returnexceptApiExceptionaserror:count_kubernetes_error(error)code=1008iferror.statusin(403,404)else1011reason="Driver logs are unavailable."exceptValueError:code,reason=1008,"The application is no longer available."exceptException:code,reason=1011,"Unable to stream driver logs."finally:producer.cancel()disconnected.cancel()awaitasyncio.gather(producer,disconnected,return_exceptions=True)try:awaitwebsocket.close(code=code,reason=reason)exceptWebSocketDisconnect:pass
Display the application console, with namespace preserved in the URL.
Source code in spark_on_k8s/api/webserver/__init__.py
346347348349350351
@router.get("/apps",response_class=HTMLResponse)asyncdefapps(request:Request):"""Display the application console, with namespace preserved in the URL."""scope=namespace_scope()namespace=scope.require(request.query_params.get("namespace")orscope.default)returnconsole_page(request,view="apps",namespace=namespace)
defconsole_page(request:Request,*,view:str,namespace:str,app_id:str|None=None,tail:int=1000):"""Serve the packaged UI; fetch application data without reloading the page."""scope=namespace_scope()config={"basePath":request.scope.get("root_path","").rstrip("/"),"namespace":namespace,# A listed scope becomes a picker; a single namespace is not editable at all."namespaces":list(scope.namespaces)ifscope.namespacesisnotNoneelseNone,"namespaceEditable":scope.namespacesisNoneorlen(scope.namespaces)>1,"refreshInterval":APIConfiguration.SPARK_ON_K8S_API_WEBSERVER_APPS_REFRESH_INTERVAL,"allowKill":APIConfiguration.SPARK_ON_K8S_API_WEBSERVER_APPS_KILL_BUTTON,"allowDelete":APIConfiguration.SPARK_ON_K8S_API_WEBSERVER_APPS_DELETE_BUTTON,"historyEnabled":bool(APIConfiguration.SPARK_ON_K8S_API_SPARK_HISTORY_HOST),"runsEnabled":get_history_settings().enabled,"version":__version__,"view":view,"appId":app_id,"tail":tail,}returntemplates.TemplateResponse(request=request,name="console.html",context={"config":config},headers={"Cache-Control":"no-cache"},)
defhandle_k8s_errors(func):@wraps(func)asyncdefwrapper(*args,request:Request,**kwargs):try:returnawaitfunc(*args,request=request,**kwargs)exceptApiExceptionase:count_kubernetes_error(e)title=f"{e.status}{e.reason}"message=e.bodyreturntemplates.TemplateResponse(request=request,name="error.html",context={"title":title,"message":message,},status_code=e.statusor500,)excepthttpx.HTTPErrorase:# Finished drivers and restarting history servers are upstream failures, not API errors.# Exception text can contain internal resolver/upstream details; report only its type.logger.warning("Spark UI proxy upstream request failed: %s",type(e).__name__)ifisinstance(e,httpx.TimeoutException):raiseHTTPException(504,detail="The Spark UI did not respond in time.")fromeraiseHTTPException(502,detail="The Spark UI is unavailable.")fromereturnwrapper
Source code in spark_on_k8s/api/webserver/__init__.py
420421422423424425426427428429
@router.get("/runs",response_class=HTMLResponse)asyncdefruns_page(request:Request,namespace:str|None=None):settings=get_history_settings()ifnotsettings.enabled:raiseHTTPException(status_code=404,detail="Run history is disabled.")# The console links here from any namespace in the API scope, which may be wider than the# recorded ones; open a recorded namespace instead of an error page. /runs/v1 still refuses it.ifnamespacenotinsettings.namespaces:namespace=settings.namespaces[0]returnconsole_page(request,view="runs",namespace=namespace)
Source code in spark_on_k8s/api/webserver/__init__.py
298299300301302303304305306307308309310311312
@router.get("/ui-history/{path:path}")@handle_k8s_errorsasyncdefspark_history_reverse_proxy(request:Request,path:str):spark_history_url=APIConfiguration.SPARK_ON_K8S_API_SPARK_HISTORY_HOSTifnotspark_history_url:raiseHTTPException(status_code=412,detail="SPARK_ON_K8S_API_SPARK_HISTORY_HOST is not set.")# Match the console listing: a scheme is explicit, not inferred from a host name starting "http".spark_history_url=spark_history_urlif"://"inspark_history_urlelsef"http://{spark_history_url}"url=_upstream_url(spark_history_url,f"/{path}",request)async_http_client=AsyncHttpClientSingleton.client()reverse_proxy_req=async_http_client.build_request(request.method,url=url,headers=_proxy_headers(request),content=request.stream())reverse_proxy_resp=awaitasync_http_client.send(reverse_proxy_req,stream=True)returnawait_proxy_response(reverse_proxy_resp,url,request,"/webserver/ui-history")
@router.get("/ui/{path:path}")@handle_k8s_errorsasyncdefui_reverse_proxy(request:Request,path:str):parts=path.split("/",2)iflen(parts)<2ornotall(parts[:2]):raiseHTTPException(status_code=404,detail="A namespace and Spark service are required.")namespace,service_name=parts[:2]ifnotall(_DNS_LABEL.fullmatch(value)forvaluein(namespace,service_name)):raiseHTTPException(status_code=400,detail="Invalid Kubernetes namespace or service name.")# The proxy connects to Services directly, so Kubernetes RBAC cannot bound it; the scope does.namespace_scope().require(namespace)path="/"+parts[2]iflen(parts)==3else"/"url=_upstream_url(f"http://{service_name}.{namespace}.svc.cluster.local:4040",path,request)async_http_client=AsyncHttpClientSingleton.client()reverse_proxy_req=async_http_client.build_request(request.method,url=url,headers=_proxy_headers(request),content=request.stream())reverse_proxy_resp=awaitasync_http_client.send(reverse_proxy_req,stream=True)returnawait_proxy_response(reverse_proxy_resp,url,request,f"/webserver/ui/{namespace}/{service_name}")