Skip to content

Commit 1871839

Browse files
Update ApplicationConfigController.java
1 parent 43bd3b0 commit 1871839

File tree

1 file changed

+303
-8
lines changed

1 file changed

+303
-8
lines changed

api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java

Lines changed: 303 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,16 @@
1919
import io.kafbat.ui.util.ApplicationRestarter;
2020
import io.kafbat.ui.util.DynamicConfigOperations;
2121
import java.util.Map;
22+
import java.util.concurrent.atomic.AtomicBoolean;
2223
import javax.annotation.Nullable;
2324
import lombok.RequiredArgsConstructor;
2425
import lombok.extern.slf4j.Slf4j;
26+
import org.springframework.boot.actuate.health.Health;
27+
import org.springframework.boot.actuate.health.HealthIndicator;
28+
import org.springframework.boot.context.event.ApplicationReadyEvent;
29+
import org.springframework.context.ApplicationContext;
30+
import org.springframework.context.event.EventListener;
31+
import org.springframework.core.io.Resource;
2532
import org.springframework.http.ResponseEntity;
2633
import org.springframework.http.codec.multipart.FilePart;
2734
import org.springframework.http.codec.multipart.Part;
@@ -31,17 +38,251 @@
3138
import reactor.core.publisher.Mono;
3239
import reactor.util.function.Tuple2;
3340
import reactor.util.function.Tuples;
41+
import org.yaml.snakeyaml.Yaml;
42+
import java.io.InputStream;
43+
import java.util.Set;
3444

3545
@Slf4j
3646
@RestController
3747
@RequiredArgsConstructor
38-
public class ApplicationConfigController extends AbstractController implements ApplicationConfigApi {
48+
public class ApplicationConfigController extends AbstractController implements ApplicationConfigApi, HealthIndicator {
3949

4050
private final DynamicConfigOperations dynamicConfigOperations;
4151
private final ApplicationRestarter restarter;
4252
private final KafkaClusterFactory kafkaClusterFactory;
4353
private final ApplicationInfoService applicationInfoService;
4454
private final DynamicConfigMapper configMapper;
55+
private final ApplicationContext applicationContext;
56+
57+
private final AtomicBoolean configValid = new AtomicBoolean(false);
58+
private final AtomicBoolean validationInProgress = new AtomicBoolean(false);
59+
60+
@jakarta.annotation.PostConstruct
61+
public void validateInitialConfig() {
62+
try {
63+
log.info("Starting initial configuration validation...");
64+
validateConfigOnStartup();
65+
configValid.set(true);
66+
log.info("Configuration validation passed");
67+
} catch (Exception e) {
68+
configValid.set(false);
69+
log.error("CRITICAL: Initial configuration validation failed. Application will exit.", e);
70+
System.exit(1);
71+
}
72+
}
73+
74+
@Override
75+
public Health health() {
76+
if (!configValid.get()) {
77+
return Health.down()
78+
.withDetail("reason", "Configuration validation failed")
79+
.withDetail("action", "Pod will restart automatically")
80+
.build();
81+
}
82+
83+
if (validationInProgress.get()) {
84+
return Health.down()
85+
.withDetail("reason", "Configuration validation in progress")
86+
.build();
87+
}
88+
89+
return Health.up().build();
90+
}
91+
92+
@EventListener(ApplicationReadyEvent.class)
93+
public void validateConfigOnStartup() {
94+
validationInProgress.set(true);
95+
try {
96+
log.info("Performing comprehensive configuration validation...");
97+
98+
// Validate YAML structure first (catches typos like 'rabc' instead of 'rbac')
99+
validateYamlStructure();
100+
101+
DynamicConfigOperations.PropertiesStructure currentConfig = dynamicConfigOperations.getCurrentProperties();
102+
validateRequiredSections(currentConfig);
103+
104+
if (currentConfig.getKafka() != null) {
105+
ClustersProperties clustersProperties = convertToClustersProperties(currentConfig.getKafka());
106+
validateClustersConfig(clustersProperties)
107+
.doOnNext(validations -> {
108+
validations.forEach((clusterName, validation) -> {
109+
if (validation != null && !isValidationSuccessful(validation)) {
110+
throw new IllegalStateException("Cluster validation failed for: " + clusterName);
111+
}
112+
});
113+
})
114+
.block();
115+
}
116+
117+
validateRbacConfig(currentConfig);
118+
119+
log.info("Configuration validation completed successfully");
120+
configValid.set(true);
121+
122+
} catch (Exception e) {
123+
configValid.set(false);
124+
log.error("Configuration validation failed: {}", e.getMessage(), e);
125+
throw new RuntimeException("Configuration validation failed", e);
126+
} finally {
127+
validationInProgress.set(false);
128+
}
129+
}
130+
131+
// Enhanced YAML structure validation to catch typos
132+
private void validateYamlStructure() {
133+
try {
134+
Yaml yaml = new Yaml();
135+
// Try multiple possible config file locations
136+
String[] configPaths = {
137+
"file:./kafka-ui/config.yml"
138+
};
139+
140+
for (String configPath : configPaths) {
141+
try {
142+
Resource configResource = applicationContext.getResource(configPath);
143+
if (configResource.exists()) {
144+
try (InputStream inputStream = configResource.getInputStream()) {
145+
Map<String, Object> configMap = yaml.load(inputStream);
146+
147+
// Check for correct section names
148+
if (!configMap.containsKey("rbac")) {
149+
// Look for common typos
150+
boolean foundTypo = false;
151+
for (String key : configMap.keySet()) {
152+
if (key.toLowerCase().contains("rbac") || key.toLowerCase().contains("role") ||
153+
key.toLowerCase().contains("access") || key.toLowerCase().contains("auth")) {
154+
if (!key.equals("rbac")) {
155+
foundTypo = true;
156+
throw new IllegalArgumentException("Configuration error: Found section '" + key +
157+
"' instead of 'rbac'. Please correct the section name to 'rbac'. " +
158+
"Available sections: " + configMap.keySet());
159+
}
160+
}
161+
}
162+
163+
if (!foundTypo) {
164+
throw new IllegalArgumentException("Missing 'rbac' section in configuration. " +
165+
"Available sections: " + configMap.keySet());
166+
}
167+
}
168+
169+
// Validate other required sections
170+
if (!configMap.containsKey("auth")) {
171+
throw new IllegalArgumentException("Missing 'auth' section in configuration");
172+
}
173+
174+
if (!configMap.containsKey("kafka")) {
175+
throw new IllegalArgumentException("Missing 'kafka' section in configuration");
176+
}
177+
178+
log.debug("YAML structure validation passed for: {}", configPath);
179+
return; // Stop after first successful validation
180+
}
181+
}
182+
} catch (Exception e) {
183+
if (e instanceof IllegalArgumentException) {
184+
throw e; // Re-throw validation errors
185+
}
186+
// Continue to next config path if this one fails
187+
log.debug("Config path {} not available: {}", configPath, e.getMessage());
188+
}
189+
}
190+
191+
log.warn("Could not find config file for YAML structure validation");
192+
193+
} catch (IllegalArgumentException e) {
194+
throw e; // Re-throw validation errors
195+
} catch (Exception e) {
196+
log.warn("YAML structure validation failed: {}", e.getMessage());
197+
// Don't fail completely - rely on object validation as fallback
198+
}
199+
}
200+
201+
// Helper method to check if validation was successful
202+
private boolean isValidationSuccessful(ClusterConfigValidationDTO validation) {
203+
try {
204+
// Try to use reflection to check validation status
205+
// First try isValid() method
206+
try {
207+
return (Boolean) validation.getClass().getMethod("isValid").invoke(validation);
208+
} catch (NoSuchMethodException e) {
209+
// If isValid() doesn't exist, try getValid() method
210+
try {
211+
return (Boolean) validation.getClass().getMethod("getValid").invoke(validation);
212+
} catch (NoSuchMethodException ex) {
213+
// If neither method exists, check for error fields
214+
try {
215+
Object errors = validation.getClass().getMethod("getErrors").invoke(validation);
216+
if (errors instanceof java.util.Collection) {
217+
return ((java.util.Collection<?>) errors).isEmpty();
218+
}
219+
} catch (NoSuchMethodException exc) {
220+
// If no validation methods found, assume it's valid
221+
return true;
222+
}
223+
}
224+
}
225+
} catch (Exception e) {
226+
log.warn("Failed to check validation status: {}", e.getMessage());
227+
return false;
228+
}
229+
return true;
230+
}
231+
232+
private ClustersProperties convertToClustersProperties(Object kafkaProperties) {
233+
if (kafkaProperties instanceof ClustersProperties) {
234+
return (ClustersProperties) kafkaProperties;
235+
}
236+
return new ClustersProperties();
237+
}
238+
239+
private void validateRbacConfig(DynamicConfigOperations.PropertiesStructure config) {
240+
if (config.getRbac() == null) {
241+
throw new IllegalArgumentException("Missing required section: rbac");
242+
}
243+
244+
// Enhanced RBAC content validation
245+
try {
246+
Object rbac = config.getRbac();
247+
248+
// Check if RBAC has roles
249+
boolean hasRoles = false;
250+
try {
251+
Object roles = rbac.getClass().getMethod("getRoles").invoke(rbac);
252+
if (roles instanceof java.util.Collection) {
253+
hasRoles = !((java.util.Collection<?>) roles).isEmpty();
254+
if (!hasRoles) {
255+
throw new IllegalArgumentException("RBAC section must contain at least one role definition");
256+
}
257+
}
258+
} catch (NoSuchMethodException e) {
259+
throw new IllegalArgumentException("RBAC section is missing required 'roles' property");
260+
}
261+
262+
} catch (IllegalArgumentException e) {
263+
throw e;
264+
} catch (Exception e) {
265+
throw new IllegalArgumentException("Invalid RBAC configuration structure: " + e.getMessage(), e);
266+
}
267+
268+
log.debug("RBAC configuration validation passed");
269+
}
270+
271+
private void validateRequiredSections(DynamicConfigOperations.PropertiesStructure config) {
272+
if (config.getAuth() == null) {
273+
throw new IllegalArgumentException("Missing required section: auth");
274+
}
275+
276+
if (config.getKafka() == null) {
277+
throw new IllegalArgumentException("Missing required section: kafka");
278+
}
279+
280+
if (config.getRbac() == null) {
281+
throw new IllegalArgumentException("Missing required section: rbac");
282+
}
283+
}
284+
285+
// Your existing methods below
45286

46287
@Override
47288
public Mono<ResponseEntity<ApplicationInfoDTO>> getApplicationInfo(ServerWebExchange exchange) {
@@ -79,8 +320,34 @@ public Mono<ResponseEntity<Void>> restartWithConfig(Mono<RestartRequestDTO> rest
79320
return validateAccess(context)
80321
.then(restartRequestDto)
81322
.doOnNext(restartDto -> {
82-
var newConfig = configMapper.fromDto(restartDto.getConfig().getProperties());
83-
dynamicConfigOperations.persist(newConfig);
323+
validationInProgress.set(true);
324+
try {
325+
var newConfig = configMapper.fromDto(restartDto.getConfig().getProperties());
326+
validateRequiredSections(newConfig);
327+
validateRbacConfig(newConfig);
328+
329+
ClustersProperties clustersProperties = convertToClustersProperties(newConfig.getKafka());
330+
validateClustersConfig(clustersProperties)
331+
.doOnNext(validations -> {
332+
boolean allValid = validations.values().stream()
333+
.allMatch(validation -> validation != null && isValidationSuccessful(validation));
334+
335+
if (!allValid) {
336+
throw new IllegalArgumentException("Cluster validation failed");
337+
}
338+
})
339+
.block();
340+
341+
dynamicConfigOperations.persist(newConfig);
342+
configValid.set(true);
343+
344+
} catch (Exception e) {
345+
configValid.set(false);
346+
log.error("Config validation failed: {}", e.getMessage(), e);
347+
throw new RuntimeException("Configuration validation failed", e);
348+
} finally {
349+
validationInProgress.set(false);
350+
}
84351
})
85352
.doOnEach(sig -> audit(context, sig))
86353
.doOnSuccess(dto -> restarter.requestRestart())
@@ -110,16 +377,44 @@ public Mono<ResponseEntity<ApplicationConfigValidationDTO>> validateConfig(Mono<
110377
.applicationConfigActions(EDIT)
111378
.operationName("validateConfig")
112379
.build();
380+
113381
return validateAccess(context)
114382
.then(configDto)
115383
.flatMap(config -> {
116-
DynamicConfigOperations.PropertiesStructure newConfig = configMapper.fromDto(config.getProperties());
117-
ClustersProperties clustersProperties = newConfig.getKafka();
118-
return validateClustersConfig(clustersProperties)
119-
.map(validations -> new ApplicationConfigValidationDTO().clusters(validations));
384+
validationInProgress.set(true);
385+
try {
386+
DynamicConfigOperations.PropertiesStructure newConfig = configMapper.fromDto(config.getProperties());
387+
validateRequiredSections(newConfig);
388+
validateRbacConfig(newConfig);
389+
390+
ClustersProperties clustersProperties = convertToClustersProperties(newConfig.getKafka());
391+
return validateClustersConfig(clustersProperties)
392+
.map(validations -> {
393+
boolean allValid = validations.values().stream()
394+
.allMatch(validation -> validation != null && isValidationSuccessful(validation));
395+
396+
ApplicationConfigValidationDTO result = new ApplicationConfigValidationDTO()
397+
.clusters(validations);
398+
399+
// Set valid field if it exists in your DTO
400+
try {
401+
result.getClass().getMethod("setValid", Boolean.class).invoke(result, allValid);
402+
} catch (Exception e) {
403+
// If setValid method doesn't exist, ignore
404+
}
405+
406+
return result;
407+
});
408+
} finally {
409+
validationInProgress.set(false);
410+
}
120411
})
121412
.map(ResponseEntity::ok)
122-
.doOnEach(sig -> audit(context, sig));
413+
.doOnEach(sig -> audit(context, sig))
414+
.onErrorResume(e -> {
415+
log.error("Configuration validation failed: {}", e.getMessage(), e);
416+
return Mono.error(e);
417+
});
123418
}
124419

125420
private Mono<Map<String, ClusterConfigValidationDTO>> validateClustersConfig(

0 commit comments

Comments
 (0)