refactor: deployment infrastructure cleanup (4 fixes)
Some checks failed
CI / build (push) Failing after 46s
CI / docker (push) Has been skipped

1. Docker socket security: remove root group from Dockerfile, use
   group_add in docker-compose.yml for runtime-only socket access

2. M2M server communication: create ServerApiClient using Logto
   client_credentials grant with API resource scope. Add M2M server
   role in bootstrap. Replace hacky admin/admin login in
   AgentStatusService.

3. Async deployment: extract DeploymentExecutor as separate @Service
   so Spring's @Async proxy works (self-invocation bypasses proxy).
   Deploy now returns immediately, health check runs in background.

4. Bootstrap: M2M server role (cameleer-m2m-server) with server:admin
   scope, idempotent creation outside the M2M app creation block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-07 17:08:37 +02:00
parent 8407d8b3c0
commit 5d04a154f9
9 changed files with 314 additions and 188 deletions

View File

@@ -24,7 +24,6 @@ RUN --mount=type=cache,target=/root/.m2/repository ./mvnw package -DskipTests -B
FROM eclipse-temurin:21-jre-alpine FROM eclipse-temurin:21-jre-alpine
WORKDIR /app WORKDIR /app
RUN addgroup -S cameleer && adduser -S cameleer -G cameleer \ RUN addgroup -S cameleer && adduser -S cameleer -G cameleer \
&& addgroup cameleer root \
&& mkdir -p /data/jars && chown -R cameleer:cameleer /data && mkdir -p /data/jars && chown -R cameleer:cameleer /data
COPY --from=build /build/target/*.jar app.jar COPY --from=build /build/target/*.jar app.jar
USER cameleer USER cameleer

View File

@@ -133,6 +133,8 @@ services:
cameleer-saas: cameleer-saas:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest} image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped restart: unless-stopped
group_add:
- "0"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

View File

@@ -305,6 +305,24 @@ else
fi fi
fi fi
fi fi
fi
# Create M2M role for the Cameleer API resource (server:admin access) — idempotent
EXISTING_M2M_SERVER_ROLE=$(api_get "/api/roles" | jq -r '.[] | select(.name == "cameleer-m2m-server") | .id')
if [ -z "$EXISTING_M2M_SERVER_ROLE" ]; then
log "Creating M2M server access role..."
SERVER_M2M_ROLE_RESPONSE=$(api_post "/api/roles" "{
\"name\": \"cameleer-m2m-server\",
\"description\": \"Server API access for SaaS backend (M2M)\",
\"type\": \"MachineToMachine\",
\"scopeIds\": [\"$SCOPE_SERVER_ADMIN\"]
}")
EXISTING_M2M_SERVER_ROLE=$(echo "$SERVER_M2M_ROLE_RESPONSE" | jq -r '.id')
fi
if [ -n "$EXISTING_M2M_SERVER_ROLE" ] && [ "$EXISTING_M2M_SERVER_ROLE" != "null" ] && [ -n "$M2M_ID" ]; then
api_post "/api/roles/$EXISTING_M2M_SERVER_ROLE/applications" "{\"applicationIds\": [\"$M2M_ID\"]}" >/dev/null 2>&1
log "Assigned server API role to M2M app: $EXISTING_M2M_SERVER_ROLE"
fi fi
# ============================================================ # ============================================================

View File

@@ -0,0 +1,191 @@
package net.siegeln.cameleer.saas.deployment;
import net.siegeln.cameleer.saas.app.AppEntity;
import net.siegeln.cameleer.saas.app.AppRepository;
import net.siegeln.cameleer.saas.app.AppService;
import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
import net.siegeln.cameleer.saas.runtime.BuildImageRequest;
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
import net.siegeln.cameleer.saas.runtime.RuntimeOrchestrator;
import net.siegeln.cameleer.saas.runtime.StartContainerRequest;
import net.siegeln.cameleer.saas.tenant.TenantRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Map;
/**
* Executes deployments asynchronously. Separated from DeploymentService
* so that Spring's @Async proxy works (self-invocation bypasses the proxy).
*/
@Service
public class DeploymentExecutor {
private static final Logger log = LoggerFactory.getLogger(DeploymentExecutor.class);
private final DeploymentRepository deploymentRepository;
private final AppRepository appRepository;
private final AppService appService;
private final TenantRepository tenantRepository;
private final RuntimeOrchestrator runtimeOrchestrator;
private final RuntimeConfig runtimeConfig;
public DeploymentExecutor(DeploymentRepository deploymentRepository,
AppRepository appRepository,
AppService appService,
TenantRepository tenantRepository,
RuntimeOrchestrator runtimeOrchestrator,
RuntimeConfig runtimeConfig) {
this.deploymentRepository = deploymentRepository;
this.appRepository = appRepository;
this.appService = appService;
this.tenantRepository = tenantRepository;
this.runtimeOrchestrator = runtimeOrchestrator;
this.runtimeConfig = runtimeConfig;
}
@Async("deploymentExecutor")
public void executeAsync(DeploymentEntity deployment, AppEntity app, EnvironmentEntity env) {
try {
var jarPath = appService.resolveJarPath(app.getJarStoragePath());
runtimeOrchestrator.buildImage(new BuildImageRequest(
runtimeConfig.getBaseImage(),
jarPath,
deployment.getImageRef()
));
deployment.setObservedStatus(ObservedStatus.STARTING);
deploymentRepository.save(deployment);
var tenant = tenantRepository.findById(env.getTenantId()).orElse(null);
var tenantSlug = tenant != null ? tenant.getSlug() : env.getTenantId().toString();
var containerName = tenantSlug + "-" + env.getSlug() + "-" + app.getSlug();
// Stop and remove old container by deployment metadata
if (app.getCurrentDeploymentId() != null) {
deploymentRepository.findById(app.getCurrentDeploymentId()).ifPresent(oldDeployment -> {
var oldMetadata = oldDeployment.getOrchestratorMetadata();
if (oldMetadata != null && oldMetadata.containsKey("containerId")) {
var oldContainerId = (String) oldMetadata.get("containerId");
try {
runtimeOrchestrator.stopContainer(oldContainerId);
runtimeOrchestrator.removeContainer(oldContainerId);
} catch (Exception e) {
log.warn("Failed to stop/remove old container {}: {}", oldContainerId, e.getMessage());
}
}
});
}
// Also remove any orphaned container with the same name
try {
var existing = runtimeOrchestrator.getContainerStatus(containerName);
if (!"not_found".equals(existing.state())) {
runtimeOrchestrator.stopContainer(containerName);
runtimeOrchestrator.removeContainer(containerName);
}
} catch (Exception e) {
// Container doesn't exist — expected for fresh deploys
}
// Build Traefik labels for inbound routing
var labels = new java.util.HashMap<String, String>();
if (app.getExposedPort() != null) {
labels.put("traefik.enable", "true");
labels.put("traefik.http.routers." + containerName + ".rule",
"Host(`" + app.getSlug() + "." + env.getSlug() + "."
+ tenantSlug + "." + runtimeConfig.getDomain() + "`)");
labels.put("traefik.http.services." + containerName + ".loadbalancer.server.port",
String.valueOf(app.getExposedPort()));
}
long memoryBytes = app.getMemoryLimit() != null
? parseMemoryBytes(app.getMemoryLimit())
: runtimeConfig.parseMemoryLimitBytes();
int cpuShares = app.getCpuShares() != null
? app.getCpuShares()
: runtimeConfig.getContainerCpuShares();
var containerId = runtimeOrchestrator.startContainer(new StartContainerRequest(
deployment.getImageRef(),
containerName,
runtimeConfig.getDockerNetwork(),
Map.of(
"CAMELEER_AUTH_TOKEN", runtimeConfig.getBootstrapToken(),
"CAMELEER_EXPORT_TYPE", "HTTP",
"CAMELEER_EXPORT_ENDPOINT", runtimeConfig.getCameleer3ServerEndpoint(),
"CAMELEER_APPLICATION_ID", app.getSlug(),
"CAMELEER_ENVIRONMENT_ID", env.getSlug(),
"CAMELEER_DISPLAY_NAME", containerName
),
memoryBytes,
cpuShares,
runtimeConfig.getAgentHealthPort(),
labels
));
deployment.setOrchestratorMetadata(Map.of("containerId", containerId));
deploymentRepository.save(deployment);
boolean healthy = waitForHealthy(containerId, runtimeConfig.getHealthCheckTimeout());
var previousDeploymentId = app.getCurrentDeploymentId();
if (healthy) {
deployment.setObservedStatus(ObservedStatus.RUNNING);
deployment.setDeployedAt(Instant.now());
deploymentRepository.save(deployment);
app.setPreviousDeploymentId(previousDeploymentId);
app.setCurrentDeploymentId(deployment.getId());
appRepository.save(app);
} else {
deployment.setObservedStatus(ObservedStatus.FAILED);
deployment.setErrorMessage("Container did not become healthy within timeout");
deploymentRepository.save(deployment);
app.setCurrentDeploymentId(deployment.getId());
appRepository.save(app);
}
} catch (Exception e) {
log.error("Deployment {} failed: {}", deployment.getId(), e.getMessage(), e);
deployment.setObservedStatus(ObservedStatus.FAILED);
deployment.setErrorMessage(e.getMessage());
deploymentRepository.save(deployment);
}
}
boolean waitForHealthy(String containerId, int timeoutSeconds) {
var deadline = System.currentTimeMillis() + (timeoutSeconds * 1000L);
while (System.currentTimeMillis() < deadline) {
var status = runtimeOrchestrator.getContainerStatus(containerId);
if (!status.running()) {
return false;
}
if ("healthy".equals(status.state())) {
return true;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
static long parseMemoryBytes(String limit) {
var s = limit.trim().toLowerCase();
if (s.endsWith("g")) {
return Long.parseLong(s.substring(0, s.length() - 1)) * 1024 * 1024 * 1024;
} else if (s.endsWith("m")) {
return Long.parseLong(s.substring(0, s.length() - 1)) * 1024 * 1024;
}
return Long.parseLong(s);
}
}

View File

@@ -1,25 +1,16 @@
package net.siegeln.cameleer.saas.deployment; package net.siegeln.cameleer.saas.deployment;
import net.siegeln.cameleer.saas.app.AppEntity;
import net.siegeln.cameleer.saas.app.AppRepository; import net.siegeln.cameleer.saas.app.AppRepository;
import net.siegeln.cameleer.saas.app.AppService;
import net.siegeln.cameleer.saas.audit.AuditAction; import net.siegeln.cameleer.saas.audit.AuditAction;
import net.siegeln.cameleer.saas.audit.AuditService; import net.siegeln.cameleer.saas.audit.AuditService;
import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
import net.siegeln.cameleer.saas.environment.EnvironmentRepository; import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
import net.siegeln.cameleer.saas.runtime.BuildImageRequest;
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
import net.siegeln.cameleer.saas.runtime.RuntimeOrchestrator; import net.siegeln.cameleer.saas.runtime.RuntimeOrchestrator;
import net.siegeln.cameleer.saas.runtime.StartContainerRequest;
import net.siegeln.cameleer.saas.tenant.TenantRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -30,29 +21,23 @@ public class DeploymentService {
private final DeploymentRepository deploymentRepository; private final DeploymentRepository deploymentRepository;
private final AppRepository appRepository; private final AppRepository appRepository;
private final AppService appService;
private final EnvironmentRepository environmentRepository; private final EnvironmentRepository environmentRepository;
private final TenantRepository tenantRepository;
private final RuntimeOrchestrator runtimeOrchestrator; private final RuntimeOrchestrator runtimeOrchestrator;
private final RuntimeConfig runtimeConfig;
private final AuditService auditService; private final AuditService auditService;
private final DeploymentExecutor deploymentExecutor;
public DeploymentService(DeploymentRepository deploymentRepository, public DeploymentService(DeploymentRepository deploymentRepository,
AppRepository appRepository, AppRepository appRepository,
AppService appService,
EnvironmentRepository environmentRepository, EnvironmentRepository environmentRepository,
TenantRepository tenantRepository,
RuntimeOrchestrator runtimeOrchestrator, RuntimeOrchestrator runtimeOrchestrator,
RuntimeConfig runtimeConfig, AuditService auditService,
AuditService auditService) { DeploymentExecutor deploymentExecutor) {
this.deploymentRepository = deploymentRepository; this.deploymentRepository = deploymentRepository;
this.appRepository = appRepository; this.appRepository = appRepository;
this.appService = appService;
this.environmentRepository = environmentRepository; this.environmentRepository = environmentRepository;
this.tenantRepository = tenantRepository;
this.runtimeOrchestrator = runtimeOrchestrator; this.runtimeOrchestrator = runtimeOrchestrator;
this.runtimeConfig = runtimeConfig;
this.auditService = auditService; this.auditService = auditService;
this.deploymentExecutor = deploymentExecutor;
} }
public DeploymentEntity deploy(UUID appId, UUID actorId) { public DeploymentEntity deploy(UUID appId, UUID actorId) {
@@ -83,128 +68,12 @@ public class DeploymentService {
AuditAction.APP_DEPLOY, app.getSlug(), AuditAction.APP_DEPLOY, app.getSlug(),
env.getSlug(), null, "SUCCESS", null); env.getSlug(), null, "SUCCESS", null);
executeDeploymentAsync(saved.getId(), app, env); // Delegate to separate service so Spring's @Async proxy works
deploymentExecutor.executeAsync(saved, app, env);
return saved; return saved;
} }
@Async("deploymentExecutor")
public void executeDeploymentAsync(UUID deploymentId, AppEntity app, EnvironmentEntity env) {
var deployment = deploymentRepository.findById(deploymentId).orElse(null);
if (deployment == null) {
log.error("Deployment not found for async execution: {}", deploymentId);
return;
}
try {
var jarPath = appService.resolveJarPath(app.getJarStoragePath());
runtimeOrchestrator.buildImage(new BuildImageRequest(
runtimeConfig.getBaseImage(),
jarPath,
deployment.getImageRef()
));
deployment.setObservedStatus(ObservedStatus.STARTING);
deploymentRepository.save(deployment);
var tenant = tenantRepository.findById(env.getTenantId()).orElse(null);
var tenantSlug = tenant != null ? tenant.getSlug() : env.getTenantId().toString();
var containerName = tenantSlug + "-" + env.getSlug() + "-" + app.getSlug();
if (app.getCurrentDeploymentId() != null) {
deploymentRepository.findById(app.getCurrentDeploymentId()).ifPresent(oldDeployment -> {
var oldMetadata = oldDeployment.getOrchestratorMetadata();
if (oldMetadata != null && oldMetadata.containsKey("containerId")) {
var oldContainerId = (String) oldMetadata.get("containerId");
try {
runtimeOrchestrator.stopContainer(oldContainerId);
runtimeOrchestrator.removeContainer(oldContainerId);
} catch (Exception e) {
log.warn("Failed to stop/remove old container {}: {}", oldContainerId, e.getMessage());
}
}
});
}
// Also try removing any container with the same name (handles orphaned containers)
try {
var existing = runtimeOrchestrator.getContainerStatus(containerName);
if (!"not_found".equals(existing.state())) {
runtimeOrchestrator.stopContainer(containerName);
runtimeOrchestrator.removeContainer(containerName);
}
} catch (Exception e) {
// Container doesn't exist — expected for fresh deploys
}
// Build Traefik labels for inbound routing
var labels = new java.util.HashMap<String, String>();
if (app.getExposedPort() != null) {
labels.put("traefik.enable", "true");
labels.put("traefik.http.routers." + containerName + ".rule",
"Host(`" + app.getSlug() + "." + env.getSlug() + "."
+ tenantSlug + "." + runtimeConfig.getDomain() + "`)");
labels.put("traefik.http.services." + containerName + ".loadbalancer.server.port",
String.valueOf(app.getExposedPort()));
}
long memoryBytes = app.getMemoryLimit() != null
? parseMemoryBytes(app.getMemoryLimit())
: runtimeConfig.parseMemoryLimitBytes();
int cpuShares = app.getCpuShares() != null
? app.getCpuShares()
: runtimeConfig.getContainerCpuShares();
var containerId = runtimeOrchestrator.startContainer(new StartContainerRequest(
deployment.getImageRef(),
containerName,
runtimeConfig.getDockerNetwork(),
Map.of(
"CAMELEER_AUTH_TOKEN", runtimeConfig.getBootstrapToken(),
"CAMELEER_EXPORT_TYPE", "HTTP",
"CAMELEER_EXPORT_ENDPOINT", runtimeConfig.getCameleer3ServerEndpoint(),
"CAMELEER_APPLICATION_ID", app.getSlug(),
"CAMELEER_ENVIRONMENT_ID", env.getSlug(),
"CAMELEER_DISPLAY_NAME", containerName
),
memoryBytes,
cpuShares,
runtimeConfig.getAgentHealthPort(),
labels
));
deployment.setOrchestratorMetadata(Map.of("containerId", containerId));
deploymentRepository.save(deployment);
boolean healthy = waitForHealthy(containerId, runtimeConfig.getHealthCheckTimeout());
var previousDeploymentId = app.getCurrentDeploymentId();
if (healthy) {
deployment.setObservedStatus(ObservedStatus.RUNNING);
deployment.setDeployedAt(Instant.now());
deploymentRepository.save(deployment);
app.setPreviousDeploymentId(previousDeploymentId);
app.setCurrentDeploymentId(deployment.getId());
appRepository.save(app);
} else {
deployment.setObservedStatus(ObservedStatus.FAILED);
deployment.setErrorMessage("Container did not become healthy within timeout");
deploymentRepository.save(deployment);
app.setCurrentDeploymentId(deployment.getId());
appRepository.save(app);
}
} catch (Exception e) {
log.error("Deployment {} failed: {}", deploymentId, e.getMessage(), e);
deployment.setObservedStatus(ObservedStatus.FAILED);
deployment.setErrorMessage(e.getMessage());
deploymentRepository.save(deployment);
}
}
public DeploymentEntity stop(UUID appId, UUID actorId) { public DeploymentEntity stop(UUID appId, UUID actorId) {
var app = appRepository.findById(appId) var app = appRepository.findById(appId)
.orElseThrow(() -> new IllegalArgumentException("App not found: " + appId)); .orElseThrow(() -> new IllegalArgumentException("App not found: " + appId));
@@ -249,34 +118,4 @@ public class DeploymentService {
public Optional<DeploymentEntity> getById(UUID deploymentId) { public Optional<DeploymentEntity> getById(UUID deploymentId) {
return deploymentRepository.findById(deploymentId); return deploymentRepository.findById(deploymentId);
} }
static long parseMemoryBytes(String limit) {
var s = limit.trim().toLowerCase();
if (s.endsWith("g")) {
return Long.parseLong(s.substring(0, s.length() - 1)) * 1024 * 1024 * 1024;
} else if (s.endsWith("m")) {
return Long.parseLong(s.substring(0, s.length() - 1)) * 1024 * 1024;
}
return Long.parseLong(s);
}
boolean waitForHealthy(String containerId, int timeoutSeconds) {
var deadline = System.currentTimeMillis() + (timeoutSeconds * 1000L);
while (System.currentTimeMillis() < deadline) {
var status = runtimeOrchestrator.getContainerStatus(containerId);
if (!status.running()) {
return false;
}
if ("healthy".equals(status.state())) {
return true;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
} }

View File

@@ -0,0 +1,79 @@
package net.siegeln.cameleer.saas.identity;
import com.fasterxml.jackson.databind.JsonNode;
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.time.Instant;
/**
* Authenticated client for cameleer3-server API calls.
* Uses Logto M2M client_credentials grant with the Cameleer API resource.
*/
@Service
public class ServerApiClient {
private static final Logger log = LoggerFactory.getLogger(ServerApiClient.class);
private static final String API_RESOURCE = "https://api.cameleer.local";
private final LogtoConfig config;
private final RuntimeConfig runtimeConfig;
private final RestClient tokenClient;
private volatile String cachedToken;
private volatile Instant tokenExpiry = Instant.MIN;
public ServerApiClient(LogtoConfig config, RuntimeConfig runtimeConfig) {
this.config = config;
this.runtimeConfig = runtimeConfig;
this.tokenClient = RestClient.builder().build();
}
public boolean isAvailable() {
return config.isConfigured();
}
/**
* Returns a RestClient pre-configured with server base URL and auth headers.
*/
public RestClient.RequestHeadersSpec<?> get(String uri) {
return RestClient.create(runtimeConfig.getCameleer3ServerEndpoint())
.get()
.uri(uri)
.header("Authorization", "Bearer " + getAccessToken())
.header("X-Cameleer-Protocol-Version", "2");
}
private synchronized String getAccessToken() {
if (cachedToken != null && Instant.now().isBefore(tokenExpiry.minusSeconds(60))) {
return cachedToken;
}
try {
var response = tokenClient.post()
.uri(config.getLogtoEndpoint() + "/oidc/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body("grant_type=client_credentials"
+ "&client_id=" + config.getM2mClientId()
+ "&client_secret=" + config.getM2mClientSecret()
+ "&resource=" + API_RESOURCE
+ "&scope=server:admin")
.retrieve()
.body(JsonNode.class);
cachedToken = response.get("access_token").asText();
long expiresIn = response.get("expires_in").asLong();
tokenExpiry = Instant.now().plusSeconds(expiresIn);
log.info("Acquired cameleer3-server M2M access token");
return cachedToken;
} catch (Exception e) {
log.error("Failed to get server API M2M token", e);
throw new RuntimeException("Server API authentication failed", e);
}
}
}

View File

@@ -2,15 +2,14 @@ package net.siegeln.cameleer.saas.observability;
import net.siegeln.cameleer.saas.app.AppRepository; import net.siegeln.cameleer.saas.app.AppRepository;
import net.siegeln.cameleer.saas.environment.EnvironmentRepository; import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
import net.siegeln.cameleer.saas.identity.ServerApiClient;
import net.siegeln.cameleer.saas.observability.dto.AgentStatusResponse; import net.siegeln.cameleer.saas.observability.dto.AgentStatusResponse;
import net.siegeln.cameleer.saas.observability.dto.ObservabilityStatusResponse; import net.siegeln.cameleer.saas.observability.dto.ObservabilityStatusResponse;
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.sql.Timestamp; import java.sql.Timestamp;
@@ -26,8 +25,7 @@ public class AgentStatusService {
private final AppRepository appRepository; private final AppRepository appRepository;
private final EnvironmentRepository environmentRepository; private final EnvironmentRepository environmentRepository;
private final RuntimeConfig runtimeConfig; private final ServerApiClient serverApiClient;
private final RestClient restClient;
@Autowired(required = false) @Autowired(required = false)
@Qualifier("clickHouseDataSource") @Qualifier("clickHouseDataSource")
@@ -35,13 +33,10 @@ public class AgentStatusService {
public AgentStatusService(AppRepository appRepository, public AgentStatusService(AppRepository appRepository,
EnvironmentRepository environmentRepository, EnvironmentRepository environmentRepository,
RuntimeConfig runtimeConfig) { ServerApiClient serverApiClient) {
this.appRepository = appRepository; this.appRepository = appRepository;
this.environmentRepository = environmentRepository; this.environmentRepository = environmentRepository;
this.runtimeConfig = runtimeConfig; this.serverApiClient = serverApiClient;
this.restClient = RestClient.builder()
.baseUrl(runtimeConfig.getCameleer3ServerEndpoint())
.build();
} }
public AgentStatusResponse getAgentStatus(UUID appId) { public AgentStatusResponse getAgentStatus(UUID appId) {
@@ -51,11 +46,13 @@ public class AgentStatusService {
var env = environmentRepository.findById(app.getEnvironmentId()) var env = environmentRepository.findById(app.getEnvironmentId())
.orElseThrow(() -> new IllegalArgumentException("Environment not found: " + app.getEnvironmentId())); .orElseThrow(() -> new IllegalArgumentException("Environment not found: " + app.getEnvironmentId()));
if (!serverApiClient.isAvailable()) {
return unknownStatus(app.getSlug(), env.getSlug());
}
try { try {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Map<String, Object>> agents = restClient.get() List<Map<String, Object>> agents = serverApiClient.get("/api/v1/agents")
.uri("/api/v1/agents")
.header("Authorization", "Bearer " + "TODO-api-key")
.retrieve() .retrieve()
.body(List.class); .body(List.class);

View File

@@ -60,6 +60,9 @@ class DeploymentServiceTest {
@Mock @Mock
private AuditService auditService; private AuditService auditService;
@Mock
private DeploymentExecutor deploymentExecutor;
private DeploymentService deploymentService; private DeploymentService deploymentService;
private UUID appId; private UUID appId;
@@ -75,12 +78,10 @@ class DeploymentServiceTest {
deploymentService = new DeploymentService( deploymentService = new DeploymentService(
deploymentRepository, deploymentRepository,
appRepository, appRepository,
appService,
environmentRepository, environmentRepository,
tenantRepository,
runtimeOrchestrator, runtimeOrchestrator,
runtimeConfig, auditService,
auditService deploymentExecutor
); );
appId = UUID.randomUUID(); appId = UUID.randomUUID();

View File

@@ -4,7 +4,7 @@ import net.siegeln.cameleer.saas.app.AppEntity;
import net.siegeln.cameleer.saas.app.AppRepository; import net.siegeln.cameleer.saas.app.AppRepository;
import net.siegeln.cameleer.saas.environment.EnvironmentEntity; import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
import net.siegeln.cameleer.saas.environment.EnvironmentRepository; import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
import net.siegeln.cameleer.saas.runtime.RuntimeConfig; import net.siegeln.cameleer.saas.identity.ServerApiClient;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -23,14 +23,14 @@ class AgentStatusServiceTest {
@Mock private AppRepository appRepository; @Mock private AppRepository appRepository;
@Mock private EnvironmentRepository environmentRepository; @Mock private EnvironmentRepository environmentRepository;
@Mock private RuntimeConfig runtimeConfig; @Mock private ServerApiClient serverApiClient;
private AgentStatusService agentStatusService; private AgentStatusService agentStatusService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(runtimeConfig.getCameleer3ServerEndpoint()).thenReturn("http://localhost:9999"); when(serverApiClient.isAvailable()).thenReturn(false);
agentStatusService = new AgentStatusService(appRepository, environmentRepository, runtimeConfig); agentStatusService = new AgentStatusService(appRepository, environmentRepository, serverApiClient);
} }
@Test @Test