Skip to content

Commit 9f92a91

Browse files
authored
Merge pull request #225 from SourceLabOrg/sp/clusterCustomOptions
Add ability to set custom client options to cluster config
2 parents 9243333 + 791f3c7 commit 9f92a91

File tree

30 files changed

+1172
-32
lines changed

30 files changed

+1172
-32
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
The format is based on [Keep a Changelog](http://keepachangelog.com/)
33
and this project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## 2.7.0 (UNRELEASED)
6+
#### New Features
7+
- [PR-225](https://github.com/SourceLabOrg/kafka-webview/pull/225)
8+
- Adds the ability to set custom kafka client properties when defining a cluster.
9+
- Adds a new debugging tool under `/configuration/cluster` to see the generated kafka client properties.
10+
11+
#### Internal Dependency Updates
12+
- Updated Kafka Client library version from 2.0.1 to 2.2.2.
13+
514
## 2.6.0 (06/21/2020)
615
- [ISSUE-144](https://github.com/SourceLabOrg/kafka-webview/issues/144) Make providing a TrustStore file when setting up a SSL enabled cluster optional. You might not want/need this option if your JVM is already configured to accept the SSL certificate served by the cluster, or if the cluster's certificate can be validated by a publically accessible CA.
716
- [PR-215](https://github.com/SourceLabOrg/kafka-webview/pull/215) Improve errors displayed when using the `test cluster` functionality.

dev-cluster/src/main/java/org/sourcelab/kafka/devcluster/DevCluster.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
import org.slf4j.LoggerFactory;
4848

4949
import java.net.URL;
50-
import java.util.ArrayList;
5150
import java.util.Arrays;
5251
import java.util.Collection;
5352
import java.util.Collections;

dev-cluster/src/main/java/org/sourcelab/kafka/devcluster/LdapServer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
2929
import com.unboundid.ldap.listener.InMemoryListenerConfig;
3030
import com.unboundid.ldap.sdk.LDAPException;
31-
import com.unboundid.ldap.sdk.OperationType;
3231
import org.slf4j.Logger;
3332
import org.slf4j.LoggerFactory;
3433

kafka-webview-ui/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<!-- Dependency versions -->
3535
<avro.version>1.8.2</avro.version>
3636
<bootstrap.version>4.0.0-beta</bootstrap.version>
37-
<kafka.version>2.0.1</kafka.version>
37+
<kafka.version>2.2.2</kafka.version>
3838
<protobuf.version>3.6.1</protobuf.version>
3939
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
4040
</properties>

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/configuration/PluginConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.kafka.common.serialization.Deserializer;
3131
import org.slf4j.Logger;
3232
import org.slf4j.LoggerFactory;
33+
import org.sourcelab.kafka.webview.ui.manager.SensitiveConfigScrubber;
3334
import org.sourcelab.kafka.webview.ui.manager.encryption.SecretManager;
3435
import org.sourcelab.kafka.webview.ui.manager.kafka.KafkaAdminFactory;
3536
import org.sourcelab.kafka.webview.ui.manager.kafka.KafkaClientConfigUtil;
@@ -202,4 +203,14 @@ public KafkaClientConfigUtil getKafkaClientConfigUtil(final AppProperties appPro
202203
public SaslUtility getSaslUtility(final SecretManager secretManager) {
203204
return new SaslUtility(secretManager);
204205
}
206+
207+
/**
208+
* For scrubbing sensitive values from client configs.
209+
* @param saslUtility instance.
210+
* @return instance.
211+
*/
212+
@Bean
213+
public SensitiveConfigScrubber getSensitiveConfigScrubber(final SaslUtility saslUtility) {
214+
return new SensitiveConfigScrubber(saslUtility);
215+
}
205216
}

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/cluster/ClusterController.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,9 @@
2929
import org.sourcelab.kafka.webview.ui.manager.ui.FlashMessage;
3030
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.Datatable;
3131
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.DatatableColumn;
32-
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.DatatableFilter;
3332
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.LinkTemplate;
3433
import org.sourcelab.kafka.webview.ui.manager.ui.datatable.YesNoBadgeTemplate;
3534
import org.sourcelab.kafka.webview.ui.model.Cluster;
36-
import org.sourcelab.kafka.webview.ui.model.View;
3735
import org.sourcelab.kafka.webview.ui.repository.ClusterRepository;
3836
import org.sourcelab.kafka.webview.ui.repository.ViewRepository;
3937
import org.springframework.beans.factory.annotation.Autowired;

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/configuration/cluster/ClusterConfigController.java

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@
2424

2525
package org.sourcelab.kafka.webview.ui.controller.configuration.cluster;
2626

27+
import com.fasterxml.jackson.core.JsonProcessingException;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
2729
import org.apache.kafka.common.errors.TimeoutException;
2830
import org.slf4j.Logger;
2931
import org.slf4j.LoggerFactory;
3032
import org.sourcelab.kafka.webview.ui.controller.BaseController;
3133
import org.sourcelab.kafka.webview.ui.controller.configuration.cluster.forms.ClusterForm;
34+
import org.sourcelab.kafka.webview.ui.manager.SensitiveConfigScrubber;
3235
import org.sourcelab.kafka.webview.ui.manager.encryption.SecretManager;
36+
import org.sourcelab.kafka.webview.ui.manager.kafka.KafkaClientConfigUtil;
3337
import org.sourcelab.kafka.webview.ui.manager.kafka.KafkaOperations;
3438
import org.sourcelab.kafka.webview.ui.manager.kafka.KafkaOperationsFactory;
3539
import org.sourcelab.kafka.webview.ui.manager.plugin.UploadManager;
@@ -51,7 +55,11 @@
5155

5256
import javax.validation.Valid;
5357
import java.io.IOException;
58+
import java.util.HashMap;
59+
import java.util.LinkedHashMap;
60+
import java.util.Map;
5461
import java.util.Optional;
62+
import java.util.stream.Collectors;
5563

5664
/**
5765
* Controller for Cluster CRUD operations.
@@ -76,6 +84,9 @@ public class ClusterConfigController extends BaseController {
7684
@Autowired
7785
private SaslUtility saslUtility;
7886

87+
@Autowired
88+
private SensitiveConfigScrubber sensitiveConfigScrubber;
89+
7990
/**
8091
* GET Displays main configuration index.
8192
*/
@@ -98,10 +109,16 @@ public String index(final Model model) {
98109
public String createClusterForm(final ClusterForm clusterForm, final Model model) {
99110
// Setup breadcrumbs
100111
setupBreadCrumbs(model, "Create", "/configuration/cluster/create");
112+
setupCreateForm(model);
101113

102114
return "configuration/cluster/create";
103115
}
104116

117+
private void setupCreateForm(final Model model) {
118+
// Load all available properties
119+
model.addAttribute("kafkaSettings", KafkaClientConfigUtil.getAllKafkaConsumerProperties());
120+
}
121+
105122
/**
106123
* GET Displays edit cluster form.
107124
*/
@@ -112,6 +129,9 @@ public String editClusterForm(
112129
final RedirectAttributes redirectAttributes,
113130
final Model model) {
114131

132+
// Initial setup
133+
setupCreateForm(model);
134+
115135
// Retrieve by id
116136
final Optional<Cluster> clusterOptional = clusterRepository.findById(id);
117137
if (!clusterOptional.isPresent()) {
@@ -152,6 +172,23 @@ public String editClusterForm(
152172
clusterForm.setSaslPassword(saslProperties.getPlainPassword());
153173
clusterForm.setSaslCustomJaas(saslProperties.getJaas());
154174

175+
// Deserialize message parameters json string into a map
176+
final ObjectMapper objectMapper = new ObjectMapper();
177+
Map<String, String> customOptions;
178+
try {
179+
customOptions = objectMapper.readValue(cluster.getOptionParameters(), Map.class);
180+
} catch (final IOException e) {
181+
// Fail safe?
182+
customOptions = new HashMap<>();
183+
}
184+
185+
// Update form object with properties.
186+
for (final Map.Entry<String, String> entry : customOptions.entrySet()) {
187+
clusterForm.getCustomOptionNames().add(entry.getKey());
188+
clusterForm.getCustomOptionValues().add(entry.getValue());
189+
}
190+
clusterForm.setCustomOptionsEnabled(!customOptions.entrySet().isEmpty());
191+
155192
// Display template
156193
return "configuration/cluster/create";
157194
}
@@ -163,7 +200,11 @@ public String editClusterForm(
163200
public String clusterUpdate(
164201
@Valid final ClusterForm clusterForm,
165202
final BindingResult bindingResult,
166-
final RedirectAttributes redirectAttributes) {
203+
final RedirectAttributes redirectAttributes,
204+
final Model model) {
205+
206+
// Initial Setup.
207+
setupCreateForm(model);
167208

168209
final boolean updateExisting = clusterForm.exists();
169210

@@ -368,9 +409,13 @@ else if (!clusterForm.exists() || (clusterForm.getTrustStoreFile() != null && !c
368409
cluster.setSaslConfig("");
369410
}
370411

412+
// Handle custom options, convert into a JSON string.
413+
final String jsonStr = handleCustomOptions(clusterForm);
414+
371415
// Update properties
372416
cluster.setName(clusterForm.getName());
373417
cluster.setBrokerHosts(clusterForm.getBrokerHosts());
418+
cluster.setOptionParameters(jsonStr);
374419
cluster.setValid(false);
375420
clusterRepository.save(cluster);
376421

@@ -382,6 +427,30 @@ else if (!clusterForm.exists() || (clusterForm.getTrustStoreFile() != null && !c
382427
return "redirect:/configuration/cluster";
383428
}
384429

430+
/**
431+
* Handles getting custom defined options and values.
432+
* @param clusterForm The submitted form.
433+
*/
434+
private String handleCustomOptions(final ClusterForm clusterForm) {
435+
// If the checkbox is unselected, then just return "{}"
436+
if (!clusterForm.getCustomOptionsEnabled()) {
437+
return "{}";
438+
}
439+
440+
// Build a map of Name => Value
441+
final Map<String, String> mappedOptions = clusterForm.getCustomOptionsAsMap();
442+
443+
// For converting map to json string
444+
final ObjectMapper objectMapper = new ObjectMapper();
445+
446+
try {
447+
return objectMapper.writeValueAsString(mappedOptions);
448+
} catch (final JsonProcessingException e) {
449+
// Fail safe?
450+
return "{}";
451+
}
452+
}
453+
385454
/**
386455
* POST deletes the selected cluster.
387456
*/
@@ -463,6 +532,47 @@ public String testCluster(@PathVariable final Long id, final RedirectAttributes
463532
return "redirect:/configuration/cluster";
464533
}
465534

535+
/**
536+
* GET for getting client configuration.
537+
*/
538+
@RequestMapping(path = "/config/{id}", method = RequestMethod.GET)
539+
public String getClientConfig(
540+
@PathVariable final Long id,
541+
final RedirectAttributes redirectAttributes,
542+
final Model model
543+
) {
544+
// Retrieve it
545+
final Optional<Cluster> clusterOptional = clusterRepository.findById(id);
546+
if (!clusterOptional.isPresent()) {
547+
// Set flash message & redirect
548+
redirectAttributes.addFlashAttribute("FlashMessage", FlashMessage.newWarning("Unable to find cluster!"));
549+
550+
// redirect to cluster index
551+
return "redirect:/configuration/cluster";
552+
}
553+
final Cluster cluster = clusterOptional.get();
554+
555+
// Setup breadcrumbs
556+
setupBreadCrumbs(model, "Client Config: " + cluster.getName(), null);
557+
558+
// Generate configs with sensitive fields scrubbed.
559+
final Map<String, Object> configs = sensitiveConfigScrubber.filterSensitiveOptions(
560+
kafkaOperationsFactory.getConsumerConfig(cluster, getLoggedInUserId()),
561+
cluster
562+
)
563+
// Sort by key for easier display.
564+
.entrySet().stream()
565+
.sorted(Map.Entry.comparingByKey())
566+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
567+
568+
// Render
569+
model.addAttribute("configs", configs);
570+
model.addAttribute("cluster", cluster);
571+
572+
// Render cluster config template
573+
return "configuration/cluster/config";
574+
}
575+
466576
private void setupBreadCrumbs(final Model model, final String name, final String url) {
467577
// Setup breadcrumbs
468578
final BreadCrumbManager manager = new BreadCrumbManager(model)

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/configuration/cluster/forms/ClusterForm.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828

2929
import javax.validation.constraints.NotNull;
3030
import javax.validation.constraints.Size;
31+
import java.util.ArrayList;
32+
import java.util.HashMap;
33+
import java.util.Iterator;
34+
import java.util.List;
35+
import java.util.Map;
3136

3237
/**
3338
* Represents the form for creating/updating the Cluster entity.
@@ -91,6 +96,20 @@ public class ClusterForm {
9196
*/
9297
private String saslCustomJaas;
9398

99+
// Custom Client Properties
100+
private Boolean customOptionsEnabled;
101+
102+
/**
103+
* Names of custom options.
104+
*/
105+
private List<String> customOptionNames = new ArrayList<>();
106+
107+
/**
108+
* Values of custom options.
109+
*/
110+
private List<String> customOptionValues = new ArrayList<>();
111+
112+
94113
public Long getId() {
95114
return id;
96115
}
@@ -261,6 +280,66 @@ public boolean isPlainSaslMechanism() {
261280
return "PLAIN".equals(saslMechanism);
262281
}
263282

283+
/**
284+
* Utility method to return custom options as a map.
285+
*/
286+
public Map<String, String> getCustomOptionsAsMap() {
287+
// Build a map of Name => Value
288+
final Map<String, String> mappedOptions = new HashMap<>();
289+
290+
final Iterator<String> names = getCustomOptionNames().iterator();
291+
final Iterator<String> values = getCustomOptionValues().iterator();
292+
293+
while (names.hasNext()) {
294+
final String name = names.next();
295+
final String value;
296+
if (values.hasNext()) {
297+
value = values.next();
298+
} else {
299+
value = "";
300+
}
301+
mappedOptions.put(name, value);
302+
}
303+
return mappedOptions;
304+
}
305+
306+
public List<String> getCustomOptionNames() {
307+
return customOptionNames;
308+
}
309+
310+
public void setCustomOptionNames(final List<String> customOptionNames) {
311+
this.customOptionNames = customOptionNames;
312+
}
313+
314+
public List<String> getCustomOptionValues() {
315+
return customOptionValues;
316+
}
317+
318+
public void setCustomOptionValues(final List<String> customOptionValues) {
319+
this.customOptionValues = customOptionValues;
320+
}
321+
322+
/**
323+
* Enable/Disable flag for custom client options.
324+
*/
325+
public Boolean getCustomOptionsEnabled() {
326+
if (customOptionsEnabled == null) {
327+
return customOptionsEnabled = false;
328+
}
329+
return customOptionsEnabled;
330+
}
331+
332+
/**
333+
* Enable/Disable flag for custom client options.
334+
*/
335+
public void setCustomOptionsEnabled(final Boolean customOptionsEnabled) {
336+
if (customOptionsEnabled == null) {
337+
this.customOptionsEnabled = false;
338+
} else {
339+
this.customOptionsEnabled = customOptionsEnabled;
340+
}
341+
}
342+
264343
@Override
265344
public String toString() {
266345
return "ClusterForm{"

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/view/ViewController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.sourcelab.kafka.webview.ui.model.Cluster;
3535
import org.sourcelab.kafka.webview.ui.model.View;
3636
import org.sourcelab.kafka.webview.ui.repository.ClusterRepository;
37-
import org.sourcelab.kafka.webview.ui.repository.MessageFormatRepository;
3837
import org.sourcelab.kafka.webview.ui.repository.ViewRepository;
3938
import org.springframework.beans.factory.annotation.Autowired;
4039
import org.springframework.data.domain.Pageable;

0 commit comments

Comments
 (0)