From 29b2dfb1d709db23f9328a95e103c309e6be18be Mon Sep 17 00:00:00 2001 From: Benjamin Teke Date: Wed, 12 Nov 2025 10:06:23 +0100 Subject: [PATCH] YARN-11888. Serve the Capacity Scheduler UI. --- .gitignore | 1 + LICENSE.txt | 1 + .../resources/assemblies/hadoop-yarn-dist.xml | 8 ++++ .../hadoop/yarn/conf/YarnConfiguration.java | 15 +++++++ .../src/main/webapp/react-router.config.ts | 1 + .../src/main/webapp/src/app/entry.client.tsx | 7 ++++ .../src/main/webapp/src/app/root.tsx | 18 ++++++++ .../src/main/webapp/vite.config.ts | 1 + .../apache/hadoop/yarn/webapp/WebApps.java | 20 +++++---- .../src/main/resources/yarn-default.xml | 24 +++++++++++ .../resourcemanager/ResourceManager.java | 41 ++++++++++++++++++- hadoop-yarn-project/pom.xml | 7 ++++ 12 files changed, 135 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 84d9572cbb5e7..617d5d2bc9891 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/npm-debug.log hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/src/main/webapp/testem.log hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/dist hadoop-yarn-project/hadoop-yarn/hadoop-yarn-ui/tmp +hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/node_modules/ yarnregistry.pdf patchprocess/ .history/ diff --git a/LICENSE.txt b/LICENSE.txt index d9689b47d3fda..836887e93cd27 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -257,6 +257,7 @@ hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/st hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/jquery hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/jt/jquery.jstree.js hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/resources/TERMINAL +hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/node_modules ======= For hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/native/container-executor/impl/utils/cJSON.[ch]: diff --git a/hadoop-assemblies/src/main/resources/assemblies/hadoop-yarn-dist.xml b/hadoop-assemblies/src/main/resources/assemblies/hadoop-yarn-dist.xml index cb90d59fcd774..f1396bfc93fae 100644 --- a/hadoop-assemblies/src/main/resources/assemblies/hadoop-yarn-dist.xml +++ b/hadoop-assemblies/src/main/resources/assemblies/hadoop-yarn-dist.xml @@ -211,6 +211,13 @@ **/* + + hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/target/hadoop-yarn-capacity-scheduler-ui-${project.version} + /share/hadoop/${hadoop.component}/webapps/scheduler-ui + + **/* + + hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice-hbase/hadoop-yarn-server-timelineservice-hbase-client/target/lib @@ -280,6 +287,7 @@ org.apache.hadoop:hadoop-yarn-server-timelineservice* org.apache.hadoop:hadoop-yarn-ui + org.apache.hadoop:hadoop-yarn-capacity-scheduler-ui org.apache.hadoop:hadoop-yarn-csi diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java index 777617d7e640a..bdeabcaa93478 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java @@ -378,6 +378,21 @@ private static void addDeprecatedKeys() { public static final String YARN_WEBAPP_UI2_WARFILE_PATH = "yarn." + "webapp.ui2.war-file-path"; + + /** + * Enable YARN Capacity Scheduler UI. + */ + public static final String YARN_WEBAPP_SCHEDULER_UI_ENABLE = "yarn." + + "webapp.scheduler-ui.enable"; + public static final boolean DEFAULT_YARN_WEBAPP_SCHEDULER_UI_ENABLE = false; + + public static final String YARN_WEBAPP_SCHEDULER_UI_WARFILE_PATH = "yarn." + + "webapp.scheduler-ui.war-file-path"; + + public static final String YARN_WEBAPP_SCHEDULER_UI_READ_ONLY_ENABLE = "yarn." + + "webapp.scheduler-ui.read-only.enable"; + public static final boolean DEFAULT_YARN_WEBAPP_SCHEDULER_UI_READ_ONLY = false; + public static final String YARN_API_SERVICES_ENABLE = "yarn." + "webapp.api-service.enable"; public static final String YARN_WEBAPP_UI1_ENABLE_TOOLS = "yarn." diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts index ac097103584d9..d6eefc38f8a06 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts @@ -5,4 +5,5 @@ export default { // Server-side render by default, to enable SPA mode set this to `false` ssr: false, appDirectory: "src/app", + basename: "/scheduler-ui", } satisfies Config; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx index 94b18d016a949..0640365b6c0d9 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx @@ -40,6 +40,13 @@ async function enableMocking() { } enableMocking().then(() => { + // Handle servlet welcome-file redirect to index.html + // React Router doesn't need index.html in the URL, so redirect without it + if (window.location.pathname.endsWith('/index.html')) { + const newPath = window.location.pathname.replace(/\/index\.html$/, '/'); + window.history.replaceState(null, '', newPath + window.location.search + window.location.hash); + } + startTransition(() => { hydrateRoot( document, diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/root.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/root.tsx index bebaa128a3c6d..0a84e2ecac7ee 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/root.tsx +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/root.tsx @@ -55,3 +55,21 @@ export default function App() { ); } + +export function HydrateFallback() { + return ( +
+
+
+ Loading... +
+

+ Loading YARN Capacity Scheduler UI... +

+
+
+ ); +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/vite.config.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/vite.config.ts index 1946f9ffd19a9..71958e987fe0a 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/vite.config.ts +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig(({ mode }) => { const clusterProxyTarget = env.VITE_CLUSTER_PROXY_TARGET; return { + base: '/scheduler-ui/', plugins: [ tailwindcss(), reactRouter(), diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java index 63e3679a5d1d0..27600f24211de 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java @@ -481,16 +481,22 @@ public WebApp start() { } public WebApp start(WebApp webapp) { - return start(webapp, null); + return start(webapp, (WebAppContext[]) null); } - public WebApp start(WebApp webapp, WebAppContext ui2Context) { + public WebApp start(WebApp webapp, WebAppContext... additionalContexts) { WebApp webApp = build(webapp); HttpServer2 httpServer = webApp.httpServer(); - if (ui2Context != null) { - addFiltersForNewContext(ui2Context); - httpServer.addHandlerAtFront(ui2Context); + + if (additionalContexts != null) { + for (WebAppContext context : additionalContexts) { + if (context != null) { + addFiltersForNewContext(context); + httpServer.addHandlerAtFront(context); + } + } } + try { httpServer.start(); LOG.info("Web app {} started at {}.", name, httpServer.getConnectorAddress(0).getPort()); @@ -500,7 +506,7 @@ public WebApp start(WebApp webapp, WebAppContext ui2Context) { return webApp; } - private void addFiltersForNewContext(WebAppContext ui2Context) { + private void addFiltersForNewContext(WebAppContext uiContext) { Map params = getConfigParameters(csrfConfigPrefix); if (hasCSRFEnabled(params)) { @@ -508,7 +514,7 @@ private void addFiltersForNewContext(WebAppContext ui2Context) { + "Please ensure that there is an authentication mechanism " + "enabled (kerberos, custom, etc).", name); String restCsrfClassName = RestCsrfPreventionFilter.class.getName(); - HttpServer2.defineFilter(ui2Context, restCsrfClassName, + HttpServer2.defineFilter(uiContext, restCsrfClassName, restCsrfClassName, params, new String[]{"/*"}); } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml index 8c453cbeb8918..efef1e30e3df6 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml @@ -336,6 +336,30 @@ + + To enable YARN Capacity Scheduler UI. + yarn.webapp.scheduler-ui.enable + false + + + + + The WAR file path for the YARN Capacity Scheduler UI. + If not specified, the scheduler UI will be loaded from the classpath. + + yarn.webapp.scheduler-ui.war-file-path + + + + + + Enable read-only mode for YARN Capacity Scheduler UI. + When set to true, the UI will not allow configuration changes. + + yarn.webapp.scheduler-ui.read-only.enable + false + + Enable services rest api on ResourceManager. diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java index f9b410471006d..5f723aba3abab 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java @@ -191,6 +191,11 @@ public class ResourceManager extends CompositeService */ public static final String UI2_WEBAPP_NAME = "/ui2"; + /* + * Scheduler UI webapp name + */ + public static final String SCHEDULER_UI_WEBAPP_NAME = "/scheduler-ui"; + /** * "Always On" services. Services that need to run always irrespective of * the HA state of the RM. @@ -1468,7 +1473,7 @@ protected void startWepApp() { } } if (onDiskPath == null || onDiskPath.isEmpty()) { - LOG.error("No war file or webapps found for ui2 !"); + LOG.error("No war file or webapps found for ui2!"); } else { if (onDiskPath.endsWith(".war")) { uiWebAppContext.setWar(onDiskPath); @@ -1480,6 +1485,38 @@ protected void startWepApp() { } } + WebAppContext schedulerUiWebAppContext = null; + if (getConfig().getBoolean(YarnConfiguration.YARN_WEBAPP_SCHEDULER_UI_ENABLE, + YarnConfiguration.DEFAULT_YARN_WEBAPP_SCHEDULER_UI_ENABLE)) { + String onDiskPath = getConfig() + .get(YarnConfiguration.YARN_WEBAPP_SCHEDULER_UI_WARFILE_PATH); + + schedulerUiWebAppContext = new WebAppContext(); + schedulerUiWebAppContext.setContextPath(SCHEDULER_UI_WEBAPP_NAME); + + if (onDiskPath == null) { + String war = "hadoop-yarn-capacity-scheduler-ui-" + VersionInfo.getVersion() + ".war"; + URL url = getClass().getClassLoader().getResource(war); + + if (url == null) { + onDiskPath = getWebAppsPath("scheduler-ui"); + } else { + onDiskPath = url.getFile(); + } + } + if (onDiskPath == null || onDiskPath.isEmpty()) { + LOG.error("No war file or webapps found for scheduler-ui!"); + } else { + if (onDiskPath.endsWith(".war")) { + schedulerUiWebAppContext.setWar(onDiskPath); + LOG.info("Using war file at: {}.", onDiskPath); + } else { + schedulerUiWebAppContext.setResourceBase(onDiskPath); + LOG.info("Using webapps at: {}.", onDiskPath); + } + } + } + builder.withAttribute(IsResourceManagerActiveServlet.RM_ATTRIBUTE, this); builder.withServlet(IsResourceManagerActiveServlet.SERVLET_NAME, IsResourceManagerActiveServlet.PATH_SPEC, @@ -1488,7 +1525,7 @@ protected void startWepApp() { try { RMWebApp rmWebApp = new RMWebApp(this); builder.withResourceConfig(rmWebApp.resourceConfig()); - webApp = builder.start(rmWebApp, uiWebAppContext); + webApp = builder.start(rmWebApp, uiWebAppContext, schedulerUiWebAppContext); } catch (WebAppException e) { webApp = e.getWebApp(); throw e; diff --git a/hadoop-yarn-project/pom.xml b/hadoop-yarn-project/pom.xml index 657b6ba79c2f5..437339185f8f9 100644 --- a/hadoop-yarn-project/pom.xml +++ b/hadoop-yarn-project/pom.xml @@ -155,6 +155,13 @@ ${yarn.ui.packaging} provided + + org.apache.hadoop + hadoop-yarn-capacity-scheduler-ui + ${project.version} + ${yarn.ui.packaging} + provided + org.apache.hadoop hadoop-yarn-server-timelineservice-hbase-server-2