feat: add server upgrade action — force-pull latest images and re-provision
Restart only stops/starts existing containers with the same image. The new upgrade action removes server + UI containers, force-pulls the latest Docker images, then re-provisions (preserving app containers, volumes, and networks). Available to both vendor (tenant detail) and tenant admin (dashboard). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,12 @@ public class TenantPortalController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/server/upgrade")
|
||||
public ResponseEntity<Void> upgradeServer() {
|
||||
portalService.upgradeServer();
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/settings")
|
||||
public ResponseEntity<TenantPortalService.TenantSettingsData> getSettings() {
|
||||
return ResponseEntity.ok(portalService.getSettings());
|
||||
|
||||
@@ -245,4 +245,16 @@ public class TenantPortalService {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void upgradeServer() {
|
||||
TenantEntity tenant = resolveTenant();
|
||||
if (!tenantProvisioner.isAvailable()) return;
|
||||
|
||||
tenantProvisioner.upgrade(tenant.getSlug());
|
||||
|
||||
var license = licenseService.getActiveLicense(tenant.getId()).orElse(null);
|
||||
String token = license != null ? license.getToken() : "";
|
||||
vendorTenantService.provisionAsync(
|
||||
tenant.getId(), tenant.getSlug(), tenant.getTier().name(), token, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public class DisabledTenantProvisioner implements TenantProvisioner {
|
||||
@Override public void start(String slug) { log.warn("Cannot start: provisioning disabled"); }
|
||||
@Override public void stop(String slug) { log.warn("Cannot stop: provisioning disabled"); }
|
||||
@Override public void remove(String slug) { log.warn("Cannot remove: provisioning disabled"); }
|
||||
@Override public void upgrade(String slug) { log.warn("Cannot upgrade: provisioning disabled"); }
|
||||
@Override public ServerStatus getStatus(String slug) { return ServerStatus.notFound(); }
|
||||
@Override public String getServerEndpoint(String slug) { return null; }
|
||||
}
|
||||
|
||||
@@ -140,6 +140,19 @@ public class DockerTenantProvisioner implements TenantProvisioner {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upgrade(String slug) {
|
||||
// 1. Stop and remove server + UI containers (preserve app containers, volumes, networks)
|
||||
stopIfRunning(serverContainerName(slug));
|
||||
stopIfRunning(uiContainerName(slug));
|
||||
removeContainer(serverContainerName(slug));
|
||||
removeContainer(uiContainerName(slug));
|
||||
|
||||
// 2. Force pull latest images
|
||||
forcePull(props.serverImage());
|
||||
forcePull(props.serverUiImage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerStatus getStatus(String slug) {
|
||||
try {
|
||||
@@ -309,6 +322,15 @@ public class DockerTenantProvisioner implements TenantProvisioner {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void forcePull(String image) {
|
||||
log.info("Force pulling image: {}", image);
|
||||
try {
|
||||
docker.pullImageCmd(image).start().awaitCompletion();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to pull image " + image + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void pullIfMissing(String image) {
|
||||
try {
|
||||
docker.inspectImageCmd(image).exec();
|
||||
|
||||
@@ -6,6 +6,7 @@ public interface TenantProvisioner {
|
||||
void start(String slug);
|
||||
void stop(String slug);
|
||||
void remove(String slug);
|
||||
void upgrade(String slug);
|
||||
ServerStatus getStatus(String slug);
|
||||
String getServerEndpoint(String slug);
|
||||
}
|
||||
|
||||
@@ -142,6 +142,16 @@ public class VendorTenantController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/upgrade")
|
||||
public ResponseEntity<Void> upgrade(@PathVariable UUID id) {
|
||||
try {
|
||||
vendorTenantService.upgradeServer(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/suspend")
|
||||
public ResponseEntity<TenantResponse> suspend(@PathVariable UUID id,
|
||||
@AuthenticationPrincipal Jwt jwt) {
|
||||
|
||||
@@ -242,6 +242,19 @@ public class VendorTenantService {
|
||||
}
|
||||
}
|
||||
|
||||
public void upgradeServer(UUID tenantId) {
|
||||
TenantEntity tenant = tenantService.getById(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tenant not found"));
|
||||
if (!tenantProvisioner.isAvailable()) return;
|
||||
|
||||
tenantProvisioner.upgrade(tenant.getSlug());
|
||||
|
||||
// Re-provision with freshly pulled images
|
||||
var license = licenseService.getActiveLicense(tenantId).orElse(null);
|
||||
String token = license != null ? license.getToken() : "";
|
||||
provisionAsync(tenantId, tenant.getSlug(), tenant.getTier().name(), token, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TenantEntity suspend(UUID tenantId, UUID actorId) {
|
||||
TenantEntity tenant = tenantService.getById(tenantId)
|
||||
|
||||
Reference in New Issue
Block a user