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 6871e4b2a219c..6064967ad15d6 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 @@ -3026,6 +3026,8 @@ public static boolean isAclEnabled(Configuration conf) { public static final String PROXY_BIND_HOST = PROXY_PREFIX + "bind-host"; + public static final String PROXY_REDIRECT_FLAG = PROXY_PREFIX + "redirect-flag"; + /** * YARN Service Level Authorization */ 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 830cade703dee..b128d0f0ef14f 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 @@ -2832,6 +2832,15 @@ 60000 + + Optional query parameter name that signals the YARN WebAppProxy to redirect + the user to the application's tracking URL instead of proxying the request. + When the tracking URL contains this flag with the value "true", the proxy + performs an HTTP redirect (302) to the tracking URL. + yarn.web-proxy.redirect-flag + + + diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java index 7817362885064..44be1b1513eb9 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java @@ -529,6 +529,35 @@ private void methodAction(final HttpServletRequest req, default: // fall out of the switch } + + /* + * If the application registered its tracking URL with the configured + * redirect flag, the proxy should not attempt + * to fetch the resource itself. Instead, it performs an HTTP redirect + * to the tracking URL. + * + * This is required for deployments where the tracking URL is served + * behind an external reverse proxy (for example Apache Knox) that is + * responsible for routing requests to multiple backend services + * such as Spark History Server instances in an HA setup. + * + * In such environments the YARN WebAppProxy cannot correctly proxy the + * request because the reverse proxy expects the request to originate + * directly from the user's browser and may require authentication + * context (e.g. a JWT) that the YARN proxy must not forward for + * security reasons. + * + * By redirecting the user instead of proxying the request, the browser + * sends a new request to the external reverse proxy which can then + * handle authentication and route the request to the appropriate + * backend service. + */ + String redirectFlagName = conf.get(YarnConfiguration.PROXY_REDIRECT_FLAG, ""); + if (!redirectFlagName.isBlank() && toFetch.getQuery().equals(redirectFlagName + "=true")) { + ProxyUtils.sendRedirect(req, resp, toFetch.toString()); + return; + } + Cookie c = null; if (userWasWarned && userApproved) { c = makeCheckCookie(id, true); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java index 49b6a7954ba9d..33f7198e49380 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java @@ -38,6 +38,7 @@ import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -51,6 +52,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -333,6 +335,47 @@ void testWebAppProxyConnectionTimeout() } + @Test + void testRedirectFlagProxyServlet() throws IOException, ServletException { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getRemoteUser()).thenReturn("dr.who"); + when(request.getPathInfo()).thenReturn("/application_00_0"); + when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration()); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getOutputStream()).thenReturn(mock(ServletOutputStream.class)); + when(response.getWriter()).thenReturn(mock(PrintWriter.class)); + WebAppProxyServlet servlet = new WebAppProxyServlet(); + ServletConfig config = mock(ServletConfig.class); + ServletContext context = mock(ServletContext.class); + when(config.getServletContext()).thenReturn(context); + AppReportFetcherForTest appReportFetcher = + new AppReportFetcherForTest(new YarnConfiguration()); + servlet.init(config); + when(config.getServletContext() + .getAttribute(WebAppProxy.FETCHER_ATTRIBUTE)) + .thenReturn(appReportFetcher); + + appReportFetcher.answer = 8; + + //Check if flag is on + YarnConfiguration conf = new YarnConfiguration(); + conf.set(YarnConfiguration.PROXY_REDIRECT_FLAG, "yarn_knox_proxy"); + servlet.setConf(conf); + servlet.doGet(request, response); + + //Check if flag is off + conf.set(YarnConfiguration.PROXY_REDIRECT_FLAG, ""); + servlet.setConf(conf); + servlet.doGet(request, response); + + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Integer.class); + Mockito.verify(response, Mockito.times(2)).setStatus(statusCaptor.capture()); + assertEquals(HttpServletResponse.SC_FOUND, statusCaptor.getAllValues().get(0)); + assertEquals(HttpServletResponse.SC_OK, statusCaptor.getAllValues().get(1)); + } + @Test @Timeout(5000) void testAppReportForEmptyTrackingUrl() throws Exception { @@ -654,6 +697,11 @@ public FetchedAppReport getApplicationReport(ApplicationId appId) result.getApplicationReport().setOriginalTrackingUrl("localhost:" + originalPort + "/foo/timeout?a=b#main"); return result; + } else if (answer == 8) { + FetchedAppReport result = getDefaultApplicationReport(appId); + result.getApplicationReport().setOriginalTrackingUrl("localhost:" + + originalPort + "/foo/bar?yarn_knox_proxy=true"); + return result; } return null; }