feat: add exposed port routing and route URL to app API
Adds domain config to RuntimeConfig/application.yml, expands AppResponse
with exposedPort and computed routeUrl, adds updateRouting to AppService,
and adds PATCH /{appId}/routing endpoint to AppController.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,14 +3,19 @@ package net.siegeln.cameleer.saas.app;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import net.siegeln.cameleer.saas.app.dto.AppResponse;
|
import net.siegeln.cameleer.saas.app.dto.AppResponse;
|
||||||
import net.siegeln.cameleer.saas.app.dto.CreateAppRequest;
|
import net.siegeln.cameleer.saas.app.dto.CreateAppRequest;
|
||||||
|
import net.siegeln.cameleer.saas.environment.EnvironmentService;
|
||||||
|
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
|
||||||
|
import net.siegeln.cameleer.saas.tenant.TenantRepository;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
import org.springframework.web.bind.annotation.RequestPart;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -26,10 +31,19 @@ public class AppController {
|
|||||||
|
|
||||||
private final AppService appService;
|
private final AppService appService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final EnvironmentService environmentService;
|
||||||
|
private final RuntimeConfig runtimeConfig;
|
||||||
|
private final TenantRepository tenantRepository;
|
||||||
|
|
||||||
public AppController(AppService appService, ObjectMapper objectMapper) {
|
public AppController(AppService appService, ObjectMapper objectMapper,
|
||||||
|
EnvironmentService environmentService,
|
||||||
|
RuntimeConfig runtimeConfig,
|
||||||
|
TenantRepository tenantRepository) {
|
||||||
this.appService = appService;
|
this.appService = appService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
|
this.environmentService = environmentService;
|
||||||
|
this.runtimeConfig = runtimeConfig;
|
||||||
|
this.tenantRepository = tenantRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data")
|
@PostMapping(consumes = "multipart/form-data")
|
||||||
@@ -103,6 +117,21 @@ public class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{appId}/routing")
|
||||||
|
public ResponseEntity<AppResponse> updateRouting(
|
||||||
|
@PathVariable UUID environmentId,
|
||||||
|
@PathVariable UUID appId,
|
||||||
|
@RequestBody net.siegeln.cameleer.saas.observability.dto.UpdateRoutingRequest request,
|
||||||
|
Authentication authentication) {
|
||||||
|
try {
|
||||||
|
var actorId = resolveActorId(authentication);
|
||||||
|
var app = appService.updateRouting(appId, request.exposedPort(), actorId);
|
||||||
|
return ResponseEntity.ok(toResponse(app));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private UUID resolveActorId(Authentication authentication) {
|
private UUID resolveActorId(Authentication authentication) {
|
||||||
String sub = authentication.getName();
|
String sub = authentication.getName();
|
||||||
try {
|
try {
|
||||||
@@ -112,19 +141,23 @@ public class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppResponse toResponse(AppEntity entity) {
|
private AppResponse toResponse(AppEntity app) {
|
||||||
|
String routeUrl = null;
|
||||||
|
if (app.getExposedPort() != null) {
|
||||||
|
var env = environmentService.getById(app.getEnvironmentId()).orElse(null);
|
||||||
|
if (env != null) {
|
||||||
|
var tenant = tenantRepository.findById(env.getTenantId()).orElse(null);
|
||||||
|
if (tenant != null) {
|
||||||
|
routeUrl = "http://" + app.getSlug() + "." + env.getSlug() + "."
|
||||||
|
+ tenant.getSlug() + "." + runtimeConfig.getDomain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return new AppResponse(
|
return new AppResponse(
|
||||||
entity.getId(),
|
app.getId(), app.getEnvironmentId(), app.getSlug(), app.getDisplayName(),
|
||||||
entity.getEnvironmentId(),
|
app.getJarOriginalFilename(), app.getJarSizeBytes(), app.getJarChecksum(),
|
||||||
entity.getSlug(),
|
app.getExposedPort(), routeUrl,
|
||||||
entity.getDisplayName(),
|
app.getCurrentDeploymentId(), app.getPreviousDeploymentId(),
|
||||||
entity.getJarOriginalFilename(),
|
app.getCreatedAt(), app.getUpdatedAt());
|
||||||
entity.getJarSizeBytes(),
|
|
||||||
entity.getJarChecksum(),
|
|
||||||
entity.getCurrentDeploymentId(),
|
|
||||||
entity.getPreviousDeploymentId(),
|
|
||||||
entity.getCreatedAt(),
|
|
||||||
entity.getUpdatedAt()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ public class AppService {
|
|||||||
null, null, "SUCCESS", null);
|
null, null, "SUCCESS", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AppEntity updateRouting(UUID appId, Integer exposedPort, UUID actorId) {
|
||||||
|
var app = appRepository.findById(appId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("App not found"));
|
||||||
|
app.setExposedPort(exposedPort);
|
||||||
|
return appRepository.save(app);
|
||||||
|
}
|
||||||
|
|
||||||
public Path resolveJarPath(String relativePath) {
|
public Path resolveJarPath(String relativePath) {
|
||||||
return Path.of(runtimeConfig.getJarStoragePath()).resolve(relativePath);
|
return Path.of(runtimeConfig.getJarStoragePath()).resolve(relativePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,17 @@ import java.time.Instant;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record AppResponse(
|
public record AppResponse(
|
||||||
UUID id, UUID environmentId, String slug, String displayName,
|
UUID id,
|
||||||
String jarOriginalFilename, Long jarSizeBytes, String jarChecksum,
|
UUID environmentId,
|
||||||
UUID currentDeploymentId, UUID previousDeploymentId,
|
String slug,
|
||||||
Instant createdAt, Instant updatedAt
|
String displayName,
|
||||||
|
String jarOriginalFilename,
|
||||||
|
Long jarSizeBytes,
|
||||||
|
String jarChecksum,
|
||||||
|
Integer exposedPort,
|
||||||
|
String routeUrl,
|
||||||
|
UUID currentDeploymentId,
|
||||||
|
UUID previousDeploymentId,
|
||||||
|
Instant createdAt,
|
||||||
|
Instant updatedAt
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package net.siegeln.cameleer.saas.observability.dto;
|
||||||
|
|
||||||
|
public record UpdateRoutingRequest(
|
||||||
|
Integer exposedPort
|
||||||
|
) {}
|
||||||
@@ -39,6 +39,9 @@ public class RuntimeConfig {
|
|||||||
@Value("${cameleer.runtime.cameleer3-server-endpoint:http://cameleer3-server:8081}")
|
@Value("${cameleer.runtime.cameleer3-server-endpoint:http://cameleer3-server:8081}")
|
||||||
private String cameleer3ServerEndpoint;
|
private String cameleer3ServerEndpoint;
|
||||||
|
|
||||||
|
@Value("${cameleer.runtime.domain:localhost}")
|
||||||
|
private String domain;
|
||||||
|
|
||||||
public long getMaxJarSize() { return maxJarSize; }
|
public long getMaxJarSize() { return maxJarSize; }
|
||||||
public String getJarStoragePath() { return jarStoragePath; }
|
public String getJarStoragePath() { return jarStoragePath; }
|
||||||
public String getBaseImage() { return baseImage; }
|
public String getBaseImage() { return baseImage; }
|
||||||
@@ -50,6 +53,7 @@ public class RuntimeConfig {
|
|||||||
public int getContainerCpuShares() { return containerCpuShares; }
|
public int getContainerCpuShares() { return containerCpuShares; }
|
||||||
public String getBootstrapToken() { return bootstrapToken; }
|
public String getBootstrapToken() { return bootstrapToken; }
|
||||||
public String getCameleer3ServerEndpoint() { return cameleer3ServerEndpoint; }
|
public String getCameleer3ServerEndpoint() { return cameleer3ServerEndpoint; }
|
||||||
|
public String getDomain() { return domain; }
|
||||||
|
|
||||||
public long parseMemoryLimitBytes() {
|
public long parseMemoryLimitBytes() {
|
||||||
var limit = containerMemoryLimit.trim().toLowerCase();
|
var limit = containerMemoryLimit.trim().toLowerCase();
|
||||||
|
|||||||
@@ -45,5 +45,6 @@ cameleer:
|
|||||||
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
||||||
bootstrap-token: ${CAMELEER_AUTH_TOKEN:}
|
bootstrap-token: ${CAMELEER_AUTH_TOKEN:}
|
||||||
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
||||||
|
domain: ${DOMAIN:localhost}
|
||||||
clickhouse:
|
clickhouse:
|
||||||
url: ${CLICKHOUSE_URL:jdbc:clickhouse://clickhouse:8123/cameleer}
|
url: ${CLICKHOUSE_URL:jdbc:clickhouse://clickhouse:8123/cameleer}
|
||||||
|
|||||||
Reference in New Issue
Block a user