Skip to content

Commit 2d929d1

Browse files
[b456377401] Refactor Cloudera Manager authentication (#1062)
1. Replace ClouderaManagerLoginHelper with ClouderaHttpClientFactory to manage both session-based (Cloudera Manager) and basic-auth (Knox/YARN) HTTP clients. 2. Update ClouderaManagerHandle to manage separate clients for different authentication needs, ensuring correct credentials are used for both CM and cluster services. 3. Update Cloudera Manager tasks and tests to utilize the refactored HTTP client management and ensure proper resource cleanup.
1 parent 7b6f6ba commit 2d929d1

29 files changed

+451
-370
lines changed

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/AbstractClouderaTimeSeriesTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ protected JsonNode requestTimeSeriesChart(ClouderaManagerHandle handle, String q
7474
uriBuilder.addParameter("to", endDate.format(isoDateTimeFormatter));
7575
URI tsURI = uriBuilder.build();
7676

77-
CloseableHttpClient httpClient = handle.getHttpClient();
77+
CloseableHttpClient httpClient = handle.getClouderaManagerHttpClient();
7878
JsonNode chartInJson;
7979
try (CloseableHttpResponse chart = httpClient.execute(new HttpGet(tsURI))) {
8080
int statusCode = chart.getStatusLine().getStatusCode();

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/AbstractClouderaYarnApplicationTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class PaginatedClouderaYarnApplicationsLoader {
7575

7676
public PaginatedClouderaYarnApplicationsLoader(ClouderaManagerHandle handle, int limit) {
7777
this.apiURI = handle.getApiURI();
78-
this.httpClient = handle.getHttpClient();
78+
this.httpClient = handle.getClouderaManagerHttpClient();
7979
this.limit = limit;
8080

8181
final DateTimeFormatter dtFormatter = DateTimeFormatter.ofPattern(ISO_DATETIME_FORMAT);

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaAPIHostsTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public ClouderaAPIHostsTask() {
4848
protected void doRun(
4949
TaskRunContext context, @Nonnull ByteSink sink, @Nonnull ClouderaManagerHandle handle)
5050
throws Exception {
51-
CloseableHttpClient httpClient = handle.getHttpClient();
51+
CloseableHttpClient httpClient = handle.getClouderaManagerHttpClient();
5252
List<ClouderaClusterDTO> clusters = handle.getClusters();
5353
if (clusters == null) {
5454
throw new MetadataDumperUsageException(

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaCMFHostsTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public TaskCategory getCategory() {
5555
protected void doRun(
5656
TaskRunContext context, @Nonnull ByteSink sink, @Nonnull ClouderaManagerHandle handle)
5757
throws Exception {
58-
CloseableHttpClient httpClient = handle.getHttpClient();
58+
CloseableHttpClient httpClient = handle.getClouderaManagerHttpClient();
5959
List<ClouderaClusterDTO> clusters = handle.getClusters();
6060
if (clusters == null) {
6161
throw new IllegalStateException(

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaClustersTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public ClouderaClustersTask() {
5050
protected void doRun(
5151
TaskRunContext context, @Nonnull ByteSink sink, @Nonnull ClouderaManagerHandle handle)
5252
throws Exception {
53-
CloseableHttpClient httpClient = handle.getHttpClient();
53+
CloseableHttpClient httpClient = handle.getClouderaManagerHttpClient();
5454

5555
ApiClusterListDTO clusterList;
5656

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaConnectorVerifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private static void verifyClusterExists(ClouderaManagerHandle handle, String clu
5353
String endpoint = String.format("%s/clusters/%s", handle.getApiURI(), clusterName);
5454
HttpGet httpGet = new HttpGet(endpoint);
5555

56-
try (CloseableHttpResponse response = handle.getHttpClient().execute(httpGet)) {
56+
try (CloseableHttpResponse response = handle.getClouderaManagerHttpClient().execute(httpGet)) {
5757

5858
int statusCode = response.getStatusLine().getStatusCode();
5959

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaHostComponentsTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public TaskCategory getCategory() {
5151
protected void doRun(
5252
TaskRunContext context, @Nonnull ByteSink sink, @Nonnull ClouderaManagerHandle handle)
5353
throws Exception {
54-
CloseableHttpClient httpClient = handle.getHttpClient();
54+
CloseableHttpClient httpClient = handle.getClouderaManagerHttpClient();
5555
List<ClouderaHostDTO> hosts = handle.getHosts();
5656
if (hosts == null) {
5757
throw new IllegalStateException(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Copyright 2022-2025 Google LLC
3+
* Copyright 2013-2021 CompilerWorks
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.google.edwmigration.dumper.application.dumper.connector.cloudera.manager;
18+
19+
import com.google.edwmigration.dumper.application.dumper.MetadataDumperUsageException;
20+
import java.net.URI;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import org.apache.http.Header;
24+
import org.apache.http.HttpHeaders;
25+
import org.apache.http.HttpStatus;
26+
import org.apache.http.NameValuePair;
27+
import org.apache.http.client.CredentialsProvider;
28+
import org.apache.http.client.config.RequestConfig;
29+
import org.apache.http.client.entity.UrlEncodedFormEntity;
30+
import org.apache.http.client.methods.CloseableHttpResponse;
31+
import org.apache.http.client.methods.HttpGet;
32+
import org.apache.http.client.methods.HttpPost;
33+
import org.apache.http.conn.ssl.NoopHostnameVerifier;
34+
import org.apache.http.conn.ssl.TrustAllStrategy;
35+
import org.apache.http.impl.client.CloseableHttpClient;
36+
import org.apache.http.impl.client.HttpClientBuilder;
37+
import org.apache.http.impl.client.HttpClients;
38+
import org.apache.http.message.BasicHeader;
39+
import org.apache.http.message.BasicNameValuePair;
40+
import org.apache.http.ssl.SSLContextBuilder;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
43+
44+
/**
45+
* Factory for creating specialized {@link CloseableHttpClient} instances for Cloudera ecosystem
46+
* interactions.
47+
*
48+
* <p>This factory provides two distinct client types to handle the different authentication
49+
* mechanisms present in Cloudera environments:
50+
*
51+
* <ul>
52+
* <li><b>Cloudera Manager Client:</b> Stateful, cookie-aware client that performs a Form-Based
53+
* login handshake.
54+
* <li><b>Basic Auth Client:</b> Stateless, isolated client for services like Spark History Server
55+
* or YARN.
56+
* </ul>
57+
*
58+
* <p>Both clients are configured to trust all SSL certificates by default, as internal cluster
59+
* certificates are frequently self-signed.
60+
*/
61+
public final class ClouderaHttpClientFactory {
62+
63+
private static final Logger logger = LoggerFactory.getLogger(ClouderaHttpClientFactory.class);
64+
65+
/**
66+
* Connection Timeout (5 seconds) Time to wait for the TCP handshake. Since Cloudera clusters are
67+
* on internal networks, if a host is reachable, it should connect almost instantly. If it takes
68+
* >5s, the host is likely down or firewalled, and we shouldn't block the thread longer.
69+
*/
70+
private static final int CONNECTION_TIMEOUT_MS = 5000;
71+
72+
/**
73+
* Socket Timeout (30 seconds) Time to wait for data packets after connection. Spark History
74+
* Server and YARN are heavy Java applications prone to Garbage Collection pauses and slow I/O
75+
* (parsing large HDFS logs). We give them 30s to respond before giving up.
76+
*/
77+
private static final int SOCKET_TIMEOUT_MS = 30000;
78+
79+
/**
80+
* Creates a {@link CloseableHttpClient} configured for Cloudera Manager and performs the login
81+
* handshake.
82+
*
83+
* <p>This method builds a client with cookie management enabled, executes the Spring Security
84+
* form login request (`/j_spring_security_check`), and verifies the session by accessing the home
85+
* page.
86+
*
87+
* @param apiUri the API URI of the Cloudera Manager instance
88+
* @param username the username for Cloudera Manager
89+
* @param password the password for Cloudera Manager
90+
* @return a ready-to-use, authenticated http client
91+
* @throws Exception if the login request fails, the credentials are invalid, or the home page is
92+
* inaccessible
93+
*/
94+
public static CloseableHttpClient createClouderaManagerClient(
95+
URI apiUri, String username, String password) throws Exception {
96+
97+
HttpClientBuilder builder = HttpClients.custom();
98+
configureTrustAllSSL(builder);
99+
CloseableHttpClient client = builder.build();
100+
101+
try {
102+
authenticateViaForm(apiUri, client, username, password);
103+
} catch (Exception e) {
104+
// If login fails, close the client to avoid leaking resources
105+
try {
106+
client.close();
107+
} catch (Exception suppressed) {
108+
e.addSuppressed(suppressed);
109+
}
110+
throw e;
111+
}
112+
113+
return client;
114+
}
115+
116+
/**
117+
* Creates a dedicated {@link CloseableHttpClient} configured with Basic Authentication.
118+
*
119+
* <p>This client is isolated from the application's global state and uses a dedicated {@link
120+
* CredentialsProvider}. It is suitable for stateless REST APIs such as the Spark History Server
121+
* or YARN ResourceManager.
122+
*
123+
* <p>Timeouts are explicitly configured (5s connect, 30s read) to handle potential latency in
124+
* overloaded cluster services.
125+
*
126+
* @param username the username for the Basic Auth header
127+
* @param password the password for the Basic Auth header
128+
* @return a configured http client that sends credentials with every request
129+
* @throws Exception if SSL configuration fails
130+
*/
131+
public static CloseableHttpClient createBasicAuthClient(String username, String password)
132+
throws Exception {
133+
HttpClientBuilder builder = HttpClients.custom();
134+
configureTrustAllSSL(builder);
135+
136+
String authHeader =
137+
"Basic "
138+
+ java.util.Base64.getEncoder()
139+
.encodeToString(
140+
(username + ":" + password).getBytes(java.nio.charset.StandardCharsets.UTF_8));
141+
List<Header> headers = new ArrayList<>();
142+
headers.add(new BasicHeader(HttpHeaders.AUTHORIZATION, authHeader));
143+
builder.setDefaultHeaders(headers);
144+
145+
RequestConfig config =
146+
RequestConfig.custom()
147+
.setConnectTimeout(CONNECTION_TIMEOUT_MS)
148+
.setSocketTimeout(SOCKET_TIMEOUT_MS)
149+
.setCircularRedirectsAllowed(true) // Helpful for some SSO redirects
150+
.build();
151+
builder.setDefaultRequestConfig(config);
152+
153+
return builder.build();
154+
}
155+
156+
private static HttpClientBuilder configureTrustAllSSL(HttpClientBuilder builder)
157+
throws Exception {
158+
builder.setSSLContext(
159+
new SSLContextBuilder().loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build());
160+
builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
161+
return builder;
162+
}
163+
164+
private static void authenticateViaForm(
165+
URI apiUri, CloseableHttpClient httpClient, String username, String password)
166+
throws Exception {
167+
URI baseUri = apiUri.resolve("/");
168+
HttpPost post = new HttpPost(baseUri + "/j_spring_security_check");
169+
List<NameValuePair> urlParameters = new ArrayList<>();
170+
urlParameters.add(new BasicNameValuePair("j_username", username));
171+
urlParameters.add(new BasicNameValuePair("j_password", password));
172+
173+
post.setEntity(new UrlEncodedFormEntity(urlParameters));
174+
175+
try (CloseableHttpResponse loginResponse = httpClient.execute(post)) {
176+
// Check Home Page to verify the session cookie is valid
177+
try (CloseableHttpResponse homeResponse =
178+
httpClient.execute(new HttpGet(baseUri + "/cmf/home"))) {
179+
if (HttpStatus.SC_OK != homeResponse.getStatusLine().getStatusCode()) {
180+
logger.error("Login failed. Home Page response: {}", homeResponse.getStatusLine());
181+
throw new MetadataDumperUsageException("Cloudera Manager login failed: " + loginResponse);
182+
}
183+
}
184+
}
185+
logger.info("Successfully logged into Cloudera Manager.");
186+
}
187+
}

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnector.java

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,7 @@
3939
import java.util.List;
4040
import java.util.function.Supplier;
4141
import javax.annotation.Nonnull;
42-
import org.apache.http.conn.ssl.NoopHostnameVerifier;
43-
import org.apache.http.conn.ssl.TrustAllStrategy;
4442
import org.apache.http.impl.client.CloseableHttpClient;
45-
import org.apache.http.impl.client.HttpClientBuilder;
46-
import org.apache.http.impl.client.HttpClients;
47-
import org.apache.http.ssl.SSLContextBuilder;
4843

4944
@AutoService({Connector.class})
5045
@Description("Dumps metadata from Cloudera Manager.")
@@ -122,13 +117,16 @@ public void addTasksTo(@Nonnull List<? super Task<?>> out, @Nonnull ConnectorArg
122117
@Nonnull
123118
@Override
124119
public ClouderaManagerHandle open(@Nonnull ConnectorArguments arguments) throws Exception {
125-
URI uri = new URI(arguments.getUri());
126-
CloseableHttpClient httpClient = disableSSLVerification(HttpClients.custom()).build();
127-
ClouderaManagerHandle handle = new ClouderaManagerHandle(uri, httpClient);
128-
120+
URI apiUri = new URI(arguments.getUri());
129121
String user = arguments.getUser();
130122
String password = arguments.getPasswordOrPrompt();
131-
doClouderaManagerLogin(handle.getBaseURI(), httpClient, user, password);
123+
124+
CloseableHttpClient clouderaManagerClient =
125+
ClouderaHttpClientFactory.createClouderaManagerClient(apiUri, user, password);
126+
CloseableHttpClient basicAuthClient =
127+
ClouderaHttpClientFactory.createBasicAuthClient(user, password);
128+
ClouderaManagerHandle handle =
129+
new ClouderaManagerHandle(apiUri, clouderaManagerClient, basicAuthClient);
132130

133131
ClouderaConnectorVerifier.verify(handle, arguments);
134132

@@ -146,19 +144,4 @@ public final void validate(@Nonnull ConnectorArguments arguments) {
146144

147145
validateDateRange(arguments);
148146
}
149-
150-
private void doClouderaManagerLogin(
151-
URI baseURI, CloseableHttpClient httpClient, String user, String password) throws Exception {
152-
ClouderaManagerLoginHelper.login(baseURI, httpClient, user, password);
153-
}
154-
155-
private HttpClientBuilder disableSSLVerification(HttpClientBuilder builder) throws Exception {
156-
// Cloudera Manager API SSL certificate is not in list of know certificates.
157-
// So, switch off SSL certificate validation.
158-
// It is expected that Dumper will work in internal private network (probably localhost calls)
159-
builder.setSSLContext(
160-
new SSLContextBuilder().loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build());
161-
builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
162-
return builder;
163-
}
164147
}

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerHandle.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,25 @@
3333
public class ClouderaManagerHandle implements Handle {
3434

3535
private final URI apiURI;
36-
private final CloseableHttpClient httpClient;
36+
private final CloseableHttpClient clouderaManagerHttpClient;
37+
private final CloseableHttpClient basicAuthHttpClient;
3738

3839
private ImmutableList<ClouderaClusterDTO> clusters;
3940
private ImmutableList<ClouderaHostDTO> hosts;
4041
private ImmutableList<ClouderaYarnApplicationDTO> sparkYarnApplications;
4142

42-
public ClouderaManagerHandle(URI apiURI, CloseableHttpClient httpClient) {
43+
public ClouderaManagerHandle(
44+
URI apiURI,
45+
CloseableHttpClient clouderaManagerHttpClient,
46+
CloseableHttpClient basicAuthHttpClient) {
4347
Preconditions.checkNotNull(apiURI, "Cloudera's apiURI can't be null.");
44-
Preconditions.checkNotNull(httpClient, "httpClient can't be null.");
48+
Preconditions.checkNotNull(clouderaManagerHttpClient, "ClouderaManager client can't be null.");
49+
Preconditions.checkNotNull(basicAuthHttpClient, "BasicAuth client can't be null.");
4550

4651
// Always add trailing slash for safety
4752
this.apiURI = unify(apiURI);
48-
this.httpClient = httpClient;
53+
this.clouderaManagerHttpClient = clouderaManagerHttpClient;
54+
this.basicAuthHttpClient = basicAuthHttpClient;
4955
}
5056

5157
/** 1. Remove query params and url fragments 2. Add trailing slash for safety */
@@ -73,8 +79,12 @@ public URI getBaseURI() {
7379
return apiURI.resolve("/");
7480
}
7581

76-
public CloseableHttpClient getHttpClient() {
77-
return httpClient;
82+
public CloseableHttpClient getClouderaManagerHttpClient() {
83+
return clouderaManagerHttpClient;
84+
}
85+
86+
public CloseableHttpClient getBasicAuthHttpClient() {
87+
return basicAuthHttpClient;
7888
}
7989

8090
@CheckForNull
@@ -121,14 +131,13 @@ public synchronized void initSparkYarnApplications(
121131

122132
@Override
123133
public void close() throws IOException {
124-
if (httpClient != null) {
125-
try {
126-
httpClient.close();
127-
} catch (IOException ignore) {
128-
// The intention is to do graceful shutdown and try to release the resource.
129-
// In case of errors we do not need to interrupt the execution flow
130-
// because the e2e use case might be successful
131-
}
134+
try {
135+
clouderaManagerHttpClient.close();
136+
basicAuthHttpClient.close();
137+
} catch (IOException ignore) {
138+
// The intention is to do graceful shutdown and try to release the resource.
139+
// In case of errors we do not need to interrupt the execution flow
140+
// because the e2e use case might be successful
132141
}
133142
}
134143

0 commit comments

Comments
 (0)