feat!: move config & settings under /api/v1/environments/{envSlug}/...
P3A of the taxonomy migration. Env-scoped config and settings endpoints
now live under the env-prefixed URL shape, making env a first-class
path segment instead of a query param. Agent-authoritative config is
split off into a dedicated endpoint so agent env comes from the JWT
only — never spoofable via URL.
Server:
- ApplicationConfigController: @RequestMapping("/api/v1/environments/
{envSlug}"). Handlers use @EnvPath Environment env, appSlug as
@PathVariable. Removed the dual-mode resolveEnvironmentForRead —
user flow only; agent flow moved to AgentConfigController.
- AgentConfigController (new): GET /api/v1/agents/config. Reads
instanceId from JWT subject, resolves (app, env) from registry,
returns AppConfigResponse. Registry miss → falls back to JWT env
claim for environment, but 404s if application cannot be derived
(no other source without registry).
- AppSettingsController: @RequestMapping("/api/v1/environments/
{envSlug}"). List at /app-settings, per-app at /apps/{appSlug}/
settings. Access class-wide PreAuthorize preserved (ADMIN/OPERATOR).
SPA:
- commands.ts: useAllApplicationConfigs, useApplicationConfig,
useUpdateApplicationConfig, useProcessorRouteMapping,
useTestExpression — rewritten URLs to /environments/{env}/apps/
{app}/... shape. environment now required on every call. Query
keys include environment so cache is env-scoped.
- dashboard.ts: useAppSettings, useAllAppSettings, useUpdateAppSettings
rewritten.
- TapConfigModal: new required environment prop; callers updated.
- RouteDetail, ExchangesPage: thread selectedEnv into test-expression
and modal.
Config changes in SecurityConfig for the new shape landed earlier in
P0.2; no security rule changes needed in this commit.
BREAKING CHANGE: /api/v1/config/** and /api/v1/admin/app-settings/**
paths removed. Agents must use /api/v1/agents/config instead of
GET /api/v1/config/{app}; users must use /api/v1/environments/{env}/
apps/{app}/config and /api/v1/environments/{env}/apps/{app}/settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer.common.model.ApplicationConfig;
|
||||
import com.cameleer.server.app.dto.AppConfigResponse;
|
||||
import com.cameleer.server.app.security.JwtAuthenticationFilter;
|
||||
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
|
||||
import com.cameleer.server.core.admin.SensitiveKeysConfig;
|
||||
import com.cameleer.server.core.admin.SensitiveKeysMerger;
|
||||
import com.cameleer.server.core.admin.SensitiveKeysRepository;
|
||||
import com.cameleer.server.core.agent.AgentInfo;
|
||||
import com.cameleer.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer.server.core.security.JwtService.JwtValidationResult;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent-authoritative application config read. Env and application are derived
|
||||
* from the agent's JWT + registry entry — not from the URL or query params, so
|
||||
* agents cannot spoof env.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/agents")
|
||||
@Tag(name = "Agent Config", description = "Agent-authoritative config read (AGENT only)")
|
||||
public class AgentConfigController {
|
||||
|
||||
private final PostgresApplicationConfigRepository configRepository;
|
||||
private final AgentRegistryService registryService;
|
||||
private final SensitiveKeysRepository sensitiveKeysRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public AgentConfigController(PostgresApplicationConfigRepository configRepository,
|
||||
AgentRegistryService registryService,
|
||||
SensitiveKeysRepository sensitiveKeysRepository,
|
||||
ObjectMapper objectMapper) {
|
||||
this.configRepository = configRepository;
|
||||
this.registryService = registryService;
|
||||
this.sensitiveKeysRepository = sensitiveKeysRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/config")
|
||||
@Operation(summary = "Get application config for the calling agent",
|
||||
description = "Resolves (application, environment) from the agent's JWT + registry. "
|
||||
+ "Prefers the registry entry (heartbeat-authoritative); falls back to the JWT env claim. "
|
||||
+ "Returns 404 if neither identifies a valid agent.")
|
||||
@ApiResponse(responseCode = "200", description = "Config returned")
|
||||
@ApiResponse(responseCode = "404", description = "Calling agent could not be resolved")
|
||||
public ResponseEntity<AppConfigResponse> getConfigForAgent(Authentication auth,
|
||||
HttpServletRequest request) {
|
||||
String instanceId = auth != null ? auth.getName() : null;
|
||||
if (instanceId == null || instanceId.isBlank()) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
AgentInfo agent = registryService.findById(instanceId);
|
||||
String application;
|
||||
String environment;
|
||||
if (agent != null) {
|
||||
application = agent.applicationId();
|
||||
environment = agent.environmentId();
|
||||
} else {
|
||||
// Registry miss — fall back to JWT env claim; application can't be
|
||||
// derived from JWT alone, so without a registry entry we 404.
|
||||
environment = environmentFromJwt(request);
|
||||
application = null;
|
||||
}
|
||||
|
||||
if (application == null || application.isBlank() || environment == null || environment.isBlank()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ApplicationConfig config = configRepository.findByApplicationAndEnvironment(application, environment)
|
||||
.orElse(ApplicationConfigController.defaultConfig(application, environment));
|
||||
|
||||
List<String> globalKeys = sensitiveKeysRepository.find()
|
||||
.map(SensitiveKeysConfig::keys)
|
||||
.orElse(null);
|
||||
List<String> merged = SensitiveKeysMerger.merge(globalKeys, extractSensitiveKeys(config));
|
||||
|
||||
return ResponseEntity.ok(new AppConfigResponse(config, globalKeys, merged));
|
||||
}
|
||||
|
||||
private static String environmentFromJwt(HttpServletRequest request) {
|
||||
Object attr = request.getAttribute(JwtAuthenticationFilter.JWT_RESULT_ATTR);
|
||||
if (attr instanceof JwtValidationResult result) {
|
||||
return result.environment();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> extractSensitiveKeys(ApplicationConfig config) {
|
||||
try {
|
||||
JsonNode node = objectMapper.valueToTree(config);
|
||||
JsonNode keysNode = node.get("sensitiveKeys");
|
||||
if (keysNode == null || keysNode.isNull() || !keysNode.isArray()) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.convertValue(keysNode, new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {});
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer.server.app.dto.AppSettingsRequest;
|
||||
import com.cameleer.server.app.web.EnvPath;
|
||||
import com.cameleer.server.core.admin.AppSettings;
|
||||
import com.cameleer.server.core.admin.AppSettingsRepository;
|
||||
import com.cameleer.server.core.admin.AuditCategory;
|
||||
import com.cameleer.server.core.admin.AuditResult;
|
||||
import com.cameleer.server.core.admin.AuditService;
|
||||
import com.cameleer.server.core.runtime.Environment;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -19,7 +21,6 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@@ -27,7 +28,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/app-settings")
|
||||
@RequestMapping("/api/v1/environments/{envSlug}")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'OPERATOR')")
|
||||
@Tag(name = "App Settings", description = "Per-application dashboard settings (ADMIN/OPERATOR)")
|
||||
public class AppSettingsController {
|
||||
@@ -40,25 +41,25 @@ public class AppSettingsController {
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "List application settings in an environment")
|
||||
public ResponseEntity<List<AppSettings>> getAll(@RequestParam String environment) {
|
||||
return ResponseEntity.ok(repository.findByEnvironment(environment));
|
||||
@GetMapping("/app-settings")
|
||||
@Operation(summary = "List application settings in this environment")
|
||||
public ResponseEntity<List<AppSettings>> getAll(@EnvPath Environment env) {
|
||||
return ResponseEntity.ok(repository.findByEnvironment(env.slug()));
|
||||
}
|
||||
|
||||
@GetMapping("/{appId}")
|
||||
@Operation(summary = "Get settings for an application in an environment (returns defaults if not configured)")
|
||||
public ResponseEntity<AppSettings> getByAppId(@PathVariable String appId,
|
||||
@RequestParam String environment) {
|
||||
AppSettings settings = repository.findByApplicationAndEnvironment(appId, environment)
|
||||
.orElse(AppSettings.defaults(appId, environment));
|
||||
@GetMapping("/apps/{appSlug}/settings")
|
||||
@Operation(summary = "Get settings for an application in this environment (returns defaults if not configured)")
|
||||
public ResponseEntity<AppSettings> getByAppId(@EnvPath Environment env,
|
||||
@PathVariable String appSlug) {
|
||||
AppSettings settings = repository.findByApplicationAndEnvironment(appSlug, env.slug())
|
||||
.orElse(AppSettings.defaults(appSlug, env.slug()));
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PutMapping("/{appId}")
|
||||
@Operation(summary = "Create or update settings for an application in an environment")
|
||||
public ResponseEntity<AppSettings> update(@PathVariable String appId,
|
||||
@RequestParam String environment,
|
||||
@PutMapping("/apps/{appSlug}/settings")
|
||||
@Operation(summary = "Create or update settings for an application in this environment")
|
||||
public ResponseEntity<AppSettings> update(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@Valid @RequestBody AppSettingsRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
List<String> errors = request.validate();
|
||||
@@ -66,20 +67,20 @@ public class AppSettingsController {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, String.join("; ", errors));
|
||||
}
|
||||
|
||||
AppSettings saved = repository.save(request.toSettings(appId, environment));
|
||||
auditService.log("update_app_settings", AuditCategory.CONFIG, appId,
|
||||
Map.of("environment", environment, "settings", saved), AuditResult.SUCCESS, httpRequest);
|
||||
AppSettings saved = repository.save(request.toSettings(appSlug, env.slug()));
|
||||
auditService.log("update_app_settings", AuditCategory.CONFIG, appSlug,
|
||||
Map.of("environment", env.slug(), "settings", saved), AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(saved);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{appId}")
|
||||
@Operation(summary = "Delete application settings for an environment (reverts to defaults)")
|
||||
public ResponseEntity<Void> delete(@PathVariable String appId,
|
||||
@RequestParam String environment,
|
||||
@DeleteMapping("/apps/{appSlug}/settings")
|
||||
@Operation(summary = "Delete application settings for this environment (reverts to defaults)")
|
||||
public ResponseEntity<Void> delete(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
HttpServletRequest httpRequest) {
|
||||
repository.delete(appId, environment);
|
||||
auditService.log("delete_app_settings", AuditCategory.CONFIG, appId,
|
||||
Map.of("environment", environment), AuditResult.SUCCESS, httpRequest);
|
||||
repository.delete(appSlug, env.slug());
|
||||
auditService.log("delete_app_settings", AuditCategory.CONFIG, appSlug,
|
||||
Map.of("environment", env.slug()), AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import com.cameleer.server.app.dto.CommandGroupResponse;
|
||||
import com.cameleer.server.app.dto.ConfigUpdateResponse;
|
||||
import com.cameleer.server.app.dto.TestExpressionRequest;
|
||||
import com.cameleer.server.app.dto.TestExpressionResponse;
|
||||
import com.cameleer.server.app.security.JwtAuthenticationFilter;
|
||||
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
|
||||
import com.cameleer.server.core.security.JwtService.JwtValidationResult;
|
||||
import com.cameleer.server.app.web.EnvPath;
|
||||
import com.cameleer.server.core.admin.AuditCategory;
|
||||
import com.cameleer.server.core.admin.AuditResult;
|
||||
import com.cameleer.server.core.admin.AuditService;
|
||||
@@ -20,6 +19,7 @@ import com.cameleer.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer.server.core.agent.AgentState;
|
||||
import com.cameleer.server.core.agent.CommandReply;
|
||||
import com.cameleer.server.core.agent.CommandType;
|
||||
import com.cameleer.server.core.runtime.Environment;
|
||||
import com.cameleer.server.core.storage.DiagramStore;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -43,12 +43,13 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Per-application configuration management.
|
||||
* Agents fetch config at startup; the UI modifies config which is persisted and pushed to agents via SSE.
|
||||
* Per-application configuration for UI/admin callers. Env comes from the path,
|
||||
* app comes from the path. Agents use {@link AgentConfigController} instead —
|
||||
* env is derived from the JWT there, not spoofable via URL.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/config")
|
||||
@Tag(name = "Application Config", description = "Per-application observability configuration")
|
||||
@RequestMapping("/api/v1/environments/{envSlug}")
|
||||
@Tag(name = "Application Config", description = "Per-application observability configuration (user-facing)")
|
||||
public class ApplicationConfigController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApplicationConfigController.class);
|
||||
@@ -74,39 +75,28 @@ public class ApplicationConfigController {
|
||||
this.sensitiveKeysRepository = sensitiveKeysRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "List application configs in an environment",
|
||||
description = "Returns stored configurations for all applications in the given environment")
|
||||
@GetMapping("/config")
|
||||
@Operation(summary = "List application configs in this environment")
|
||||
@ApiResponse(responseCode = "200", description = "Configs returned")
|
||||
public ResponseEntity<List<ApplicationConfig>> listConfigs(@RequestParam String environment,
|
||||
public ResponseEntity<List<ApplicationConfig>> listConfigs(@EnvPath Environment env,
|
||||
HttpServletRequest httpRequest) {
|
||||
auditService.log("view_app_configs", AuditCategory.CONFIG, null,
|
||||
Map.of("environment", environment), AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(configRepository.findByEnvironment(environment));
|
||||
Map.of("environment", env.slug()), AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(configRepository.findByEnvironment(env.slug()));
|
||||
}
|
||||
|
||||
@GetMapping("/{application}")
|
||||
@Operation(summary = "Get application config for an environment",
|
||||
description = "For agents: environment is taken from the JWT env claim; the query param is ignored. "
|
||||
+ "For UI/admin callers: environment must be provided via the `environment` query param. "
|
||||
+ "Returns 404 if the environment cannot be resolved. Includes merged sensitive keys.")
|
||||
@GetMapping("/apps/{appSlug}/config")
|
||||
@Operation(summary = "Get application config for this environment",
|
||||
description = "Returns stored config merged with global sensitive keys. "
|
||||
+ "Falls back to defaults if no row is persisted yet.")
|
||||
@ApiResponse(responseCode = "200", description = "Config returned")
|
||||
@ApiResponse(responseCode = "404", description = "Environment could not be resolved")
|
||||
public ResponseEntity<AppConfigResponse> getConfig(@PathVariable String application,
|
||||
@RequestParam(required = false) String environment,
|
||||
Authentication auth,
|
||||
public ResponseEntity<AppConfigResponse> getConfig(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
HttpServletRequest httpRequest) {
|
||||
String resolved = resolveEnvironmentForRead(auth, httpRequest, environment);
|
||||
if (resolved == null || resolved.isBlank()) {
|
||||
auditService.log("view_app_config", AuditCategory.CONFIG, application,
|
||||
Map.of("reason", "missing_environment"), AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
auditService.log("view_app_config", AuditCategory.CONFIG, application,
|
||||
Map.of("environment", resolved), AuditResult.SUCCESS, httpRequest);
|
||||
ApplicationConfig config = configRepository.findByApplicationAndEnvironment(application, resolved)
|
||||
.orElse(defaultConfig(application, resolved));
|
||||
auditService.log("view_app_config", AuditCategory.CONFIG, appSlug,
|
||||
Map.of("environment", env.slug()), AuditResult.SUCCESS, httpRequest);
|
||||
ApplicationConfig config = configRepository.findByApplicationAndEnvironment(appSlug, env.slug())
|
||||
.orElse(defaultConfig(appSlug, env.slug()));
|
||||
|
||||
List<String> globalKeys = sensitiveKeysRepository.find()
|
||||
.map(SensitiveKeysConfig::keys)
|
||||
@@ -116,51 +106,32 @@ public class ApplicationConfigController {
|
||||
return ResponseEntity.ok(new AppConfigResponse(config, globalKeys, merged));
|
||||
}
|
||||
|
||||
/**
|
||||
* Agents identify themselves via AGENT role and a real JWT env claim — use that,
|
||||
* ignoring any query param (agents can't spoof env). Non-agent callers (admin UI)
|
||||
* must pass the env explicitly; their JWT env claim is a placeholder and not
|
||||
* authoritative.
|
||||
*/
|
||||
private String resolveEnvironmentForRead(Authentication auth,
|
||||
HttpServletRequest request,
|
||||
String queryEnvironment) {
|
||||
boolean isAgent = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> "ROLE_AGENT".equals(a.getAuthority()));
|
||||
if (isAgent) {
|
||||
return environmentFromJwt(request);
|
||||
}
|
||||
return queryEnvironment;
|
||||
}
|
||||
|
||||
@PutMapping("/{application}")
|
||||
@Operation(summary = "Update application config for an environment",
|
||||
@PutMapping("/apps/{appSlug}/config")
|
||||
@Operation(summary = "Update application config for this environment",
|
||||
description = "Saves config and pushes CONFIG_UPDATE to LIVE agents of this application in the given environment")
|
||||
@ApiResponse(responseCode = "200", description = "Config saved and pushed")
|
||||
public ResponseEntity<ConfigUpdateResponse> updateConfig(@PathVariable String application,
|
||||
@RequestParam String environment,
|
||||
public ResponseEntity<ConfigUpdateResponse> updateConfig(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@RequestBody ApplicationConfig config,
|
||||
Authentication auth,
|
||||
HttpServletRequest httpRequest) {
|
||||
String updatedBy = auth != null ? auth.getName() : "system";
|
||||
|
||||
config.setApplication(application);
|
||||
ApplicationConfig saved = configRepository.save(application, environment, config, updatedBy);
|
||||
config.setApplication(appSlug);
|
||||
ApplicationConfig saved = configRepository.save(appSlug, env.slug(), config, updatedBy);
|
||||
|
||||
// Merge global + per-app sensitive keys for the SSE push payload
|
||||
List<String> globalKeys = sensitiveKeysRepository.find()
|
||||
.map(SensitiveKeysConfig::keys)
|
||||
.orElse(null);
|
||||
List<String> perAppKeys = extractSensitiveKeys(saved);
|
||||
List<String> mergedKeys = SensitiveKeysMerger.merge(globalKeys, perAppKeys);
|
||||
|
||||
// Push with merged sensitive keys injected into the payload
|
||||
CommandGroupResponse pushResult = pushConfigToAgentsWithMergedKeys(application, environment, saved, mergedKeys);
|
||||
CommandGroupResponse pushResult = pushConfigToAgentsWithMergedKeys(appSlug, env.slug(), saved, mergedKeys);
|
||||
log.info("Config v{} saved for '{}', pushed to {} agent(s), {} responded",
|
||||
saved.getVersion(), application, pushResult.total(), pushResult.responded());
|
||||
saved.getVersion(), appSlug, pushResult.total(), pushResult.responded());
|
||||
|
||||
auditService.log("update_app_config", AuditCategory.CONFIG, application,
|
||||
Map.of("environment", environment, "version", saved.getVersion(),
|
||||
auditService.log("update_app_config", AuditCategory.CONFIG, appSlug,
|
||||
Map.of("environment", env.slug(), "version", saved.getVersion(),
|
||||
"agentsPushed", pushResult.total(),
|
||||
"responded", pushResult.responded(), "timedOut", pushResult.timedOut().size()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
@@ -168,35 +139,34 @@ public class ApplicationConfigController {
|
||||
return ResponseEntity.ok(new ConfigUpdateResponse(saved, pushResult));
|
||||
}
|
||||
|
||||
@GetMapping("/{application}/processor-routes")
|
||||
@Operation(summary = "Get processor to route mapping for an environment",
|
||||
@GetMapping("/apps/{appSlug}/processor-routes")
|
||||
@Operation(summary = "Get processor to route mapping for this environment",
|
||||
description = "Returns a map of processorId → routeId for all processors seen in this application + environment")
|
||||
@ApiResponse(responseCode = "200", description = "Mapping returned")
|
||||
public ResponseEntity<Map<String, String>> getProcessorRouteMapping(@PathVariable String application,
|
||||
@RequestParam String environment) {
|
||||
return ResponseEntity.ok(diagramStore.findProcessorRouteMapping(application, environment));
|
||||
public ResponseEntity<Map<String, String>> getProcessorRouteMapping(@EnvPath Environment env,
|
||||
@PathVariable String appSlug) {
|
||||
return ResponseEntity.ok(diagramStore.findProcessorRouteMapping(appSlug, env.slug()));
|
||||
}
|
||||
|
||||
@PostMapping("/{application}/test-expression")
|
||||
@Operation(summary = "Test a tap expression against sample data via a live agent in an environment")
|
||||
@PostMapping("/apps/{appSlug}/config/test-expression")
|
||||
@Operation(summary = "Test a tap expression against sample data via a live agent in this environment")
|
||||
@ApiResponse(responseCode = "200", description = "Expression evaluated successfully")
|
||||
@ApiResponse(responseCode = "404", description = "No live agent available for this application in this environment")
|
||||
@ApiResponse(responseCode = "504", description = "Agent did not respond in time")
|
||||
public ResponseEntity<TestExpressionResponse> testExpression(
|
||||
@PathVariable String application,
|
||||
@RequestParam String environment,
|
||||
@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@RequestBody TestExpressionRequest request) {
|
||||
AgentInfo agent = registryService.findByApplicationAndEnvironment(application, environment).stream()
|
||||
AgentInfo agent = registryService.findByApplicationAndEnvironment(appSlug, env.slug()).stream()
|
||||
.filter(a -> a.state() == AgentState.LIVE)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (agent == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(new TestExpressionResponse(null, "No live agent available for application: " + application));
|
||||
.body(new TestExpressionResponse(null, "No live agent available for application: " + appSlug));
|
||||
}
|
||||
|
||||
// Build payload JSON
|
||||
String payloadJson;
|
||||
try {
|
||||
payloadJson = objectMapper.writeValueAsString(Map.of(
|
||||
@@ -211,7 +181,6 @@ public class ApplicationConfigController {
|
||||
.body(new TestExpressionResponse(null, "Failed to serialize request"));
|
||||
}
|
||||
|
||||
// Send command and await reply
|
||||
CompletableFuture<CommandReply> future = registryService.addCommandWithReply(
|
||||
agent.instanceId(), CommandType.TEST_EXPRESSION, payloadJson);
|
||||
|
||||
@@ -233,23 +202,6 @@ public class ApplicationConfigController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the {@code env} claim from the caller's validated JWT (populated by
|
||||
* {@link JwtAuthenticationFilter}). Returns null if no internal JWT was seen
|
||||
* on this request, or the token has no env claim.
|
||||
*/
|
||||
private static String environmentFromJwt(HttpServletRequest request) {
|
||||
Object attr = request.getAttribute(JwtAuthenticationFilter.JWT_RESULT_ATTR);
|
||||
if (attr instanceof JwtValidationResult result) {
|
||||
return result.environment();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts sensitiveKeys from ApplicationConfig via JsonNode to avoid compile-time
|
||||
* dependency on getSensitiveKeys() which may not be in the published cameleer-common jar yet.
|
||||
*/
|
||||
private List<String> extractSensitiveKeys(ApplicationConfig config) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.JsonNode node = objectMapper.valueToTree(config);
|
||||
@@ -263,14 +215,10 @@ public class ApplicationConfigController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push config to agents with merged sensitive keys injected into the JSON payload.
|
||||
*/
|
||||
private CommandGroupResponse pushConfigToAgentsWithMergedKeys(String application, String environment,
|
||||
ApplicationConfig config, List<String> mergedKeys) {
|
||||
String payloadJson;
|
||||
try {
|
||||
// Serialize config to a mutable map, inject merged keys
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> configMap = objectMapper.convertValue(config, Map.class);
|
||||
configMap.put("sensitiveKeys", mergedKeys);
|
||||
@@ -316,7 +264,7 @@ public class ApplicationConfigController {
|
||||
return new CommandGroupResponse(allSuccess, futures.size(), responses.size(), responses, timedOut);
|
||||
}
|
||||
|
||||
private static ApplicationConfig defaultConfig(String application, String environment) {
|
||||
static ApplicationConfig defaultConfig(String application, String environment) {
|
||||
ApplicationConfig config = new ApplicationConfig();
|
||||
config.setApplication(application);
|
||||
config.setEnvironment(environment);
|
||||
|
||||
Reference in New Issue
Block a user