diff --git a/dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaConnectorVerifier.java b/dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaConnectorVerifier.java new file mode 100644 index 000000000..7ff7b78fb --- /dev/null +++ b/dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaConnectorVerifier.java @@ -0,0 +1,84 @@ +/* + * Copyright 2022-2025 Google LLC + * Copyright 2013-2021 CompilerWorks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.edwmigration.dumper.application.dumper.connector.cloudera.manager; + +import com.google.edwmigration.dumper.application.dumper.ConnectorArguments; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.util.EntityUtils; + +/** Utility class for verifying the preconditions and configuration of a Cloudera Connector. */ +public final class ClouderaConnectorVerifier { + + private ClouderaConnectorVerifier() {} + + /** + * Verifies the validity of the connector configuration and environment. + * + *

This involves checking connectivity to the API, verifying the existence of the specified + * cluster (if provided), and ensuring other preconditions are met. + * + * @param handle The handle to the Cloudera Manager API. + * @param arguments The connector arguments containing target cluster details. + * @throws ClouderaConnectorException If verification fails due to missing resources, API errors, + * or connectivity issues. + */ + public static void verify( + @Nonnull ClouderaManagerHandle handle, @Nonnull ConnectorArguments arguments) + throws ClouderaConnectorException { + verifyClusterExists(handle, arguments.getCluster()); + } + + private static void verifyClusterExists(ClouderaManagerHandle handle, String clusterName) + throws ClouderaConnectorException { + if (clusterName == null) { + return; + } + String endpoint = String.format("%s/clusters/%s", handle.getApiURI(), clusterName); + HttpGet httpGet = new HttpGet(endpoint); + + try (CloseableHttpResponse response = handle.getHttpClient().execute(httpGet)) { + + int statusCode = response.getStatusLine().getStatusCode(); + + if (statusCode == 404) { + throw new ClouderaConnectorException( + String.format("Specified cluster '%s' not found.", clusterName)); + } + + if (!isHttpStatusSuccess(statusCode)) { + String errorMsg = EntityUtils.toString(response.getEntity()); + throw new ClouderaConnectorException( + String.format( + "Unexpected API error checking cluster '%s'. Code: %d. Message: %s", + clusterName, statusCode, errorMsg)); + } + } catch (IOException e) { + throw new ClouderaConnectorException( + String.format( + "Failed to communicate with Cloudera Manager API while checking cluster '%s'.", + clusterName), + e); + } + } + + private static boolean isHttpStatusSuccess(int statusCode) { + return statusCode >= 200 && statusCode < 300; + } +} diff --git a/dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnector.java b/dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnector.java index 178c08e45..aa9e34b91 100644 --- a/dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnector.java +++ b/dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnector.java @@ -129,6 +129,9 @@ public ClouderaManagerHandle open(@Nonnull ConnectorArguments arguments) throws String user = arguments.getUser(); String password = arguments.getPasswordOrPrompt(); doClouderaManagerLogin(handle.getBaseURI(), httpClient, user, password); + + ClouderaConnectorVerifier.verify(handle, arguments); + return handle; } diff --git a/dumper/app/src/test/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaConnectorVerifierTest.java b/dumper/app/src/test/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaConnectorVerifierTest.java new file mode 100644 index 000000000..c33ab469d --- /dev/null +++ b/dumper/app/src/test/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaConnectorVerifierTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2022-2025 Google LLC + * Copyright 2013-2021 CompilerWorks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.edwmigration.dumper.application.dumper.connector.cloudera.manager; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.edwmigration.dumper.application.dumper.ConnectorArguments; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ClouderaConnectorVerifierTest { + + private final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + private final ClouderaManagerHandle handle = mock(ClouderaManagerHandle.class); + private final ConnectorArguments arguments = mock(ConnectorArguments.class); + + @Before + public void setUp() throws URISyntaxException { + when(handle.getHttpClient()).thenReturn(httpClient); + when(handle.getApiURI()).thenReturn(new URI("http://localhost:7183/api/v57")); + } + + @Test + public void verify_clusterIsNull_doesNothing() { + when(arguments.getCluster()).thenReturn(null); + + ClouderaConnectorVerifier.verify(handle, arguments); + + // No exception thrown expected + } + + @Test + public void verify_clusterExists_doesNothing() throws Exception { + when(arguments.getCluster()).thenReturn("cluster-1"); + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getStatusLine()).thenReturn(statusLine); + when(httpClient.execute(any())).thenReturn(response); + + ClouderaConnectorVerifier.verify(handle, arguments); + + // No exception thrown expected + } + + @Test + public void verify_clusterNotFound_throwsException() throws Exception { + String clusterName = "cluster-1"; + when(arguments.getCluster()).thenReturn(clusterName); + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(404); + when(response.getStatusLine()).thenReturn(statusLine); + when(httpClient.execute(any())).thenReturn(response); + + ClouderaConnectorException e = + assertThrows( + ClouderaConnectorException.class, + () -> ClouderaConnectorVerifier.verify(handle, arguments)); + + assertThat(e.getMessage(), containsString("Specified cluster 'cluster-1' not found.")); + } + + @Test + public void verify_unexpectedApiError_throwsException() throws Exception { + // Arrange + String clusterName = "cluster-1"; + when(arguments.getCluster()).thenReturn(clusterName); + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getStatusLine()).thenReturn(statusLine); + when(response.getEntity()).thenReturn(new StringEntity("Internal Server Error")); + when(httpClient.execute(any())).thenReturn(response); + + ClouderaConnectorException e = + assertThrows( + ClouderaConnectorException.class, + () -> ClouderaConnectorVerifier.verify(handle, arguments)); + + assertThat( + e.getMessage(), + containsString( + "Unexpected API error checking cluster 'cluster-1'. Code: 500. Message: Internal Server Error")); + } + + @Test + public void verify_ioException_throwsException() throws Exception { + String clusterName = "cluster-1"; + when(arguments.getCluster()).thenReturn(clusterName); + when(httpClient.execute(any())).thenThrow(new IOException("some error")); + + ClouderaConnectorException e = + assertThrows( + ClouderaConnectorException.class, + () -> ClouderaConnectorVerifier.verify(handle, arguments)); + + assertThat( + e.getMessage(), + containsString( + "Failed to communicate with Cloudera Manager API while checking cluster 'cluster-1'.")); + } +} diff --git a/dumper/app/src/test/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnectorTest.java b/dumper/app/src/test/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnectorTest.java index c862339fc..4646a0e1e 100644 --- a/dumper/app/src/test/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnectorTest.java +++ b/dumper/app/src/test/java/com/google/edwmigration/dumper/application/dumper/connector/cloudera/manager/ClouderaManagerConnectorTest.java @@ -19,6 +19,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; import com.google.common.collect.ImmutableMap; import com.google.edwmigration.dumper.application.dumper.ConnectorArguments; @@ -27,11 +30,14 @@ import com.google.edwmigration.dumper.application.dumper.task.FormatTask; import com.google.edwmigration.dumper.application.dumper.task.Task; import com.google.edwmigration.dumper.application.dumper.task.TaskCategory; +import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.apache.http.impl.client.CloseableHttpClient; import org.junit.Test; +import org.mockito.MockedStatic; public class ClouderaManagerConnectorTest { private static final String clouderaRequiredArgs = @@ -255,6 +261,73 @@ public void addTasksTo_containsDefaultTasks() throws Exception { assertEquals("One FormatTask is expected", formatCount, 1); } + @Test + public void open_success() throws Exception { + try (MockedStatic loginHelper = + mockStatic(ClouderaManagerLoginHelper.class); + MockedStatic connectorVerifier = + mockStatic(ClouderaConnectorVerifier.class)) { + ConnectorArguments arguments = + new ConnectorArguments( + "--connector", + "cloudera-manager", + "--url", + "http://localhost", + "--user", + "user", + "--password", + "password"); + ClouderaManagerConnector connector = new ClouderaManagerConnector(); + + // Act + ClouderaManagerHandle handle = connector.open(arguments); + + // Assert + assertNotNull(handle); + loginHelper.verify( + () -> + ClouderaManagerLoginHelper.login( + any(URI.class), any(CloseableHttpClient.class), eq("user"), eq("password"))); + connectorVerifier.verify(() -> ClouderaConnectorVerifier.verify(any(), any())); + } + } + + @Test + public void open_verifierFails_throwsException() throws Exception { + try (MockedStatic loginHelper = + mockStatic(ClouderaManagerLoginHelper.class); + MockedStatic connectorVerifier = + mockStatic(ClouderaConnectorVerifier.class)) { + ConnectorArguments arguments = + new ConnectorArguments( + "--connector", + "cloudera-manager", + "--url", + "http://localhost", + "--user", + "user", + "--password", + "password"); + ClouderaManagerConnector connector = new ClouderaManagerConnector(); + + RuntimeException expectedException = new RuntimeException("Verification failed"); + connectorVerifier + .when(() -> ClouderaConnectorVerifier.verify(any(), any())) + .thenThrow(expectedException); + + // Act & Assert + RuntimeException actualException = + assertThrows(RuntimeException.class, () -> connector.open(arguments)); + assertEquals(expectedException, actualException); + + loginHelper.verify( + () -> + ClouderaManagerLoginHelper.login( + any(URI.class), any(CloseableHttpClient.class), eq("user"), eq("password"))); + connectorVerifier.verify(() -> ClouderaConnectorVerifier.verify(any(), any())); + } + } + private static ConnectorArguments args(String s) throws Exception { return new ConnectorArguments(s.split(" ")); }