/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.kafka.clients.admin;
import org.apache.kafka.clients.Metadata;
import org.apache.kafka.clients.MockClient;
import org.apache.kafka.clients.NodeApiVersions;
import org.apache.kafka.clients.admin.DeleteAclsResults.FilterResults;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.KafkaFuture;
import org.apache.kafka.common.Node;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.errors.SecurityDisabledException;
import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.common.requests.ApiError;
import org.apache.kafka.common.requests.CreateAclsResponse;
import org.apache.kafka.common.requests.CreateAclsResponse.AclCreationResponse;
import org.apache.kafka.common.requests.CreateTopicsResponse;
import org.apache.kafka.common.requests.DeleteAclsResponse;
import org.apache.kafka.common.requests.DeleteAclsResponse.AclDeletionResult;
import org.apache.kafka.common.requests.DeleteAclsResponse.AclFilterResponse;
import org.apache.kafka.common.requests.DescribeAclsResponse;
import org.apache.kafka.common.utils.Time;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* A unit test for KafkaAdminClient.
*
* See KafkaAdminClientIntegrationTest for an integration test of the KafkaAdminClient.
*/
public class KafkaAdminClientTest {
@Rule
final public Timeout globalTimeout = Timeout.millis(120000);
@Test
public void testGetOrCreateListValue() {
Map<String, List<String>> map = new HashMap<>();
List<String> fooList = KafkaAdminClient.getOrCreateListValue(map, "foo");
assertNotNull(fooList);
fooList.add("a");
fooList.add("b");
List<String> fooList2 = KafkaAdminClient.getOrCreateListValue(map, "foo");
assertEquals(fooList, fooList2);
assertTrue(fooList2.contains("a"));
assertTrue(fooList2.contains("b"));
List<String> barList = KafkaAdminClient.getOrCreateListValue(map, "bar");
assertNotNull(barList);
assertTrue(barList.isEmpty());
}
@Test
public void testCalcTimeoutMsRemainingAsInt() {
assertEquals(0, KafkaAdminClient.calcTimeoutMsRemainingAsInt(1000, 1000));
assertEquals(100, KafkaAdminClient.calcTimeoutMsRemainingAsInt(1000, 1100));
assertEquals(Integer.MAX_VALUE, KafkaAdminClient.calcTimeoutMsRemainingAsInt(0, Long.MAX_VALUE));
assertEquals(Integer.MIN_VALUE, KafkaAdminClient.calcTimeoutMsRemainingAsInt(Long.MAX_VALUE, 0));
}
@Test
public void testPrettyPrintException() {
assertEquals("Null exception.", KafkaAdminClient.prettyPrintException(null));
assertEquals("TimeoutException", KafkaAdminClient.prettyPrintException(new TimeoutException()));
assertEquals("TimeoutException: The foobar timed out.",
KafkaAdminClient.prettyPrintException(new TimeoutException("The foobar timed out.")));
}
private static Map<String, Object> newStrMap(String... vals) {
Map<String, Object> map = new HashMap<>();
map.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:8121");
map.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, "1000");
if (vals.length % 2 != 0) {
throw new IllegalStateException();
}
for (int i = 0; i < vals.length; i += 2) {
map.put(vals[i], vals[i + 1]);
}
return map;
}
private static AdminClientConfig newConfMap(String... vals) {
return new AdminClientConfig(newStrMap(vals));
}
@Test
public void testGenerateClientId() {
Set<String> ids = new HashSet<>();
for (int i = 0; i < 10; i++) {
String id = KafkaAdminClient.generateClientId(newConfMap(AdminClientConfig.CLIENT_ID_CONFIG, ""));
assertTrue("Got duplicate id " + id, !ids.contains(id));
ids.add(id);
}
assertEquals("myCustomId",
KafkaAdminClient.generateClientId(newConfMap(AdminClientConfig.CLIENT_ID_CONFIG, "myCustomId")));
}
private static class MockKafkaAdminClientContext implements AutoCloseable {
final static String CLUSTER_ID = "mockClusterId";
final AdminClientConfig adminClientConfig;
final Metadata metadata;
final HashMap<Integer, Node> nodes;
final MockClient mockClient;
final AdminClient client;
Cluster cluster;
MockKafkaAdminClientContext(Map<String, Object> config) {
this.adminClientConfig = new AdminClientConfig(config);
this.metadata = new Metadata(adminClientConfig.getLong(AdminClientConfig.RETRY_BACKOFF_MS_CONFIG),
adminClientConfig.getLong(AdminClientConfig.METADATA_MAX_AGE_CONFIG));
this.nodes = new HashMap<Integer, Node>();
this.nodes.put(0, new Node(0, "localhost", 8121));
this.nodes.put(1, new Node(1, "localhost", 8122));
this.nodes.put(2, new Node(2, "localhost", 8123));
this.mockClient = new MockClient(Time.SYSTEM, this.metadata);
this.client = KafkaAdminClient.create(adminClientConfig, mockClient, metadata);
this.cluster = new Cluster(CLUSTER_ID, nodes.values(),
Collections.<PartitionInfo>emptySet(), Collections.<String>emptySet(),
Collections.<String>emptySet(), nodes.get(0));
}
@Override
public void close() {
this.client.close();
}
}
@Test
public void testCloseAdminClient() throws Exception {
new MockKafkaAdminClientContext(newStrMap()).close();
}
private static void assertFutureError(Future<?> future, Class<? extends Throwable> exceptionClass)
throws InterruptedException {
try {
future.get();
fail("Expected a " + exceptionClass.getSimpleName() + " exception, but got success.");
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
assertEquals("Expected a " + exceptionClass.getSimpleName() + " exception, but got " +
cause.getClass().getSimpleName(),
exceptionClass, cause.getClass());
}
}
/**
* Test that the client properly times out when we don't receive any metadata.
*/
@Test
public void testTimeoutWithoutMetadata() throws Exception {
try (MockKafkaAdminClientContext ctx = new MockKafkaAdminClientContext(newStrMap(
AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, "10"))) {
ctx.mockClient.setNodeApiVersions(NodeApiVersions.create());
ctx.mockClient.setNode(new Node(0, "localhost", 8121));
ctx.mockClient.prepareResponse(new CreateTopicsResponse(new HashMap<String, ApiError>() {{
put("myTopic", new ApiError(Errors.NONE, ""));
}}));
KafkaFuture<Void> future = ctx.client.
createTopics(Collections.singleton(new NewTopic("myTopic", new HashMap<Integer, List<Integer>>() {{
put(0, Arrays.asList(new Integer[]{0, 1, 2}));
}})), new CreateTopicsOptions().timeoutMs(1000)).all();
assertFutureError(future, TimeoutException.class);
}
}
@Test
public void testCreateTopics() throws Exception {
try (MockKafkaAdminClientContext ctx = new MockKafkaAdminClientContext(newStrMap())) {
ctx.mockClient.setNodeApiVersions(NodeApiVersions.create());
ctx.mockClient.prepareMetadataUpdate(ctx.cluster, Collections.<String>emptySet());
ctx.mockClient.setNode(ctx.nodes.get(0));
ctx.mockClient.prepareResponse(new CreateTopicsResponse(new HashMap<String, ApiError>() {{
put("myTopic", new ApiError(Errors.NONE, ""));
}}));
KafkaFuture<Void> future = ctx.client.
createTopics(Collections.singleton(new NewTopic("myTopic", new HashMap<Integer, List<Integer>>() {{
put(0, Arrays.asList(new Integer[]{0, 1, 2}));
}})), new CreateTopicsOptions().timeoutMs(10000)).all();
future.get();
}
}
private static final AclBinding ACL1 = new AclBinding(new Resource(ResourceType.TOPIC, "mytopic3"),
new AccessControlEntry("User:ANONYMOUS", "*", AclOperation.DESCRIBE, AclPermissionType.ALLOW));
private static final AclBinding ACL2 = new AclBinding(new Resource(ResourceType.TOPIC, "mytopic4"),
new AccessControlEntry("User:ANONYMOUS", "*", AclOperation.DESCRIBE, AclPermissionType.DENY));
private static final AclBindingFilter FILTER1 = new AclBindingFilter(new ResourceFilter(ResourceType.ANY, null),
new AccessControlEntryFilter("User:ANONYMOUS", null, AclOperation.ANY, AclPermissionType.ANY));
private static final AclBindingFilter FILTER2 = new AclBindingFilter(new ResourceFilter(ResourceType.ANY, null),
new AccessControlEntryFilter("User:bob", null, AclOperation.ANY, AclPermissionType.ANY));
@Test
public void testDescribeAcls() throws Exception {
try (MockKafkaAdminClientContext ctx = new MockKafkaAdminClientContext(newStrMap())) {
ctx.mockClient.setNodeApiVersions(NodeApiVersions.create());
ctx.mockClient.prepareMetadataUpdate(ctx.cluster, Collections.<String>emptySet());
ctx.mockClient.setNode(ctx.nodes.get(0));
// Test a call where we get back ACL1 and ACL2.
ctx.mockClient.prepareResponse(new DescribeAclsResponse(0, null,
new ArrayList<AclBinding>() {{
add(ACL1);
add(ACL2);
}}));
assertCollectionIs(ctx.client.describeAcls(FILTER1).all().get(), ACL1, ACL2);
// Test a call where we get back no results.
ctx.mockClient.prepareResponse(new DescribeAclsResponse(0, null,
Collections.<AclBinding>emptySet()));
assertTrue(ctx.client.describeAcls(FILTER2).all().get().isEmpty());
// Test a call where we get back an error.
ctx.mockClient.prepareResponse(new DescribeAclsResponse(0,
new SecurityDisabledException("Security is disabled"), Collections.<AclBinding>emptySet()));
assertFutureError(ctx.client.describeAcls(FILTER2).all(), SecurityDisabledException.class);
}
}
@Test
public void testCreateAcls() throws Exception {
try (MockKafkaAdminClientContext ctx = new MockKafkaAdminClientContext(newStrMap())) {
ctx.mockClient.setNodeApiVersions(NodeApiVersions.create());
ctx.mockClient.prepareMetadataUpdate(ctx.cluster, Collections.<String>emptySet());
ctx.mockClient.setNode(ctx.nodes.get(0));
// Test a call where we successfully create two ACLs.
ctx.mockClient.prepareResponse(new CreateAclsResponse(0,
new ArrayList<AclCreationResponse>() {{
add(new AclCreationResponse(null));
add(new AclCreationResponse(null));
}}));
CreateAclsResults results = ctx.client.createAcls(new ArrayList<AclBinding>() {{
add(ACL1);
add(ACL2);
}});
assertCollectionIs(results.results().keySet(), ACL1, ACL2);
for (KafkaFuture<Void> future : results.results().values()) {
future.get();
}
results.all().get();
// Test a call where we fail to create one ACL.
ctx.mockClient.prepareResponse(new CreateAclsResponse(0,
new ArrayList<AclCreationResponse>() {{
add(new AclCreationResponse(new SecurityDisabledException("Security is disabled")));
add(new AclCreationResponse(null));
}}));
results = ctx.client.createAcls(new ArrayList<AclBinding>() {{
add(ACL1);
add(ACL2);
}});
assertCollectionIs(results.results().keySet(), ACL1, ACL2);
assertFutureError(results.results().get(ACL1), SecurityDisabledException.class);
results.results().get(ACL2).get();
assertFutureError(results.all(), SecurityDisabledException.class);
}
}
@Test
public void testDeleteAcls() throws Exception {
try (MockKafkaAdminClientContext ctx = new MockKafkaAdminClientContext(newStrMap())) {
ctx.mockClient.setNodeApiVersions(NodeApiVersions.create());
ctx.mockClient.prepareMetadataUpdate(ctx.cluster, Collections.<String>emptySet());
ctx.mockClient.setNode(ctx.nodes.get(0));
// Test a call where one filter has an error.
ctx.mockClient.prepareResponse(new DeleteAclsResponse(0, new ArrayList<AclFilterResponse>() {{
add(new AclFilterResponse(null,
new ArrayList<AclDeletionResult>() {{
add(new AclDeletionResult(null, ACL1));
add(new AclDeletionResult(null, ACL2));
}}));
add(new AclFilterResponse(new SecurityDisabledException("No security"),
Collections.<AclDeletionResult>emptySet()));
}}));
DeleteAclsResults results = ctx.client.deleteAcls(new ArrayList<AclBindingFilter>() {{
add(FILTER1);
add(FILTER2);
}});
Map<AclBindingFilter, KafkaFuture<FilterResults>> filterResults = results.results();
FilterResults filter1Results = filterResults.get(FILTER1).get();
assertEquals(null, filter1Results.acls().get(0).exception());
assertEquals(ACL1, filter1Results.acls().get(0).acl());
assertEquals(null, filter1Results.acls().get(1).exception());
assertEquals(ACL2, filter1Results.acls().get(1).acl());
assertTrue(filterResults.get(FILTER2).isCompletedExceptionally());
assertFutureError(filterResults.get(FILTER2), SecurityDisabledException.class);
assertFutureError(results.all(), SecurityDisabledException.class);
// Test a call where one deletion result has an error.
ctx.mockClient.prepareResponse(new DeleteAclsResponse(0, new ArrayList<AclFilterResponse>() {{
add(new AclFilterResponse(null,
new ArrayList<AclDeletionResult>() {{
add(new AclDeletionResult(null, ACL1));
add(new AclDeletionResult(new SecurityDisabledException("No security"), ACL2));
}}));
add(new AclFilterResponse(null, Collections.<AclDeletionResult>emptySet()));
}}));
results = ctx.client.deleteAcls(
new ArrayList<AclBindingFilter>() {{
add(FILTER1);
add(FILTER2);
}});
assertTrue(results.results().get(FILTER2).get().acls().isEmpty());
assertFutureError(results.all(), SecurityDisabledException.class);
// Test a call where there are no errors.
ctx.mockClient.prepareResponse(new DeleteAclsResponse(0, new ArrayList<AclFilterResponse>() {{
add(new AclFilterResponse(null,
new ArrayList<AclDeletionResult>() {{
add(new AclDeletionResult(null, ACL1));
}}));
add(new AclFilterResponse(null,
new ArrayList<AclDeletionResult>() {{
add(new AclDeletionResult(null, ACL2));
}}));
}}));
results = ctx.client.deleteAcls(
new ArrayList<AclBindingFilter>() {{
add(FILTER1);
add(FILTER2);
}});
Collection<AclBinding> deleted = results.all().get();
assertCollectionIs(deleted, ACL1, ACL2);
}
}
private static <T> void assertCollectionIs(Collection<T> collection, T... elements) {
for (T element : elements) {
assertTrue("Did not find " + element, collection.contains(element));
}
assertEquals("There are unexpected extra elements in the collection.",
elements.length, collection.size());
}
}