mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat: real service runtime — private networks, replicas, lifecycle, rollback
Services now reconcile into actual Docker containers instead of
bookkeeping rows:
- Per-project bridge network with service-name DNS aliases and
${{service.KEY}} env expansion (HOST/PORT builtins)
- Replicas, published ports, Traefik domain routing, health checks,
restart policies persisted on the service spec; spec-hash drift
recreates containers on redeploy
- Lifecycle API: start/stop/restart/redeploy, live runtime state,
real image rollback (replaces the fake sleep)
- Manual scale and PUT replicas reconcile live containers; service
and project delete remove their containers and the project network
- Canvas: real URLs, replica badges, env filter, right-click
deploy/restart/stop/delete menu, empty state
- Service detail: real lifecycle buttons, live replica/URL strip,
Networking editor, ${{ }} variable helper
- Fix prebuilt-image pulls racing the tag (drain the pull stream)
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
c16b32f56a
commit
ff356230fe
20 files changed
+2270
-243
No files matched your search
@@ -48,6 +48,26 @@ export type ServiceEntity = {
|
||||
buildPath?: string;
|
||||
cpu?: string;
|
||||
memory?: string;
|
||||
replicas?: number;
|
||||
port?: number;
|
||||
domain?: string;
|
||||
healthcheckPath?: string;
|
||||
restartPolicy?: string;
|
||||
publicUrl?: string;
|
||||
};
|
||||
|
||||
export type RuntimeContainer = {
|
||||
id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
replica: number;
|
||||
};
|
||||
|
||||
export type ServiceRuntime = {
|
||||
desired: number;
|
||||
status: 'running' | 'degraded' | 'stopped';
|
||||
urls: string[];
|
||||
containers: RuntimeContainer[];
|
||||
};
|
||||
|
||||
export type ServiceVariable = {
|
||||
@@ -343,6 +363,7 @@ type RawNodeAgent = components['schemas']['NodeAgent'];
|
||||
|
||||
export type CreateProjectInput = components['schemas']['CreateProjectRequest'];
|
||||
export type CreateServiceInput = components['schemas']['CreateServiceRequest'];
|
||||
export type UpdateServiceInput = components['schemas']['UpdateServiceRequest'];
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
@@ -515,6 +536,12 @@ function normalizeService(service: RawService): ServiceEntity | null {
|
||||
buildPath: service.build_path,
|
||||
cpu: service.cpu,
|
||||
memory: service.memory,
|
||||
replicas: service.replicas,
|
||||
port: service.port,
|
||||
domain: service.domain,
|
||||
healthcheckPath: service.healthcheck_path,
|
||||
restartPolicy: service.restart_policy,
|
||||
publicUrl: service.public_url,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -998,12 +1025,55 @@ export async function createService(projectId: string, input: CreateServiceInput
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function updateService(serviceId: string, input: UpdateServiceInput): Promise<ServiceEntity> {
|
||||
const payload = await requestJson<RawService | { service?: RawService }>(`/services/${serviceId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
const row = (payload as { service?: RawService }).service ?? (payload as RawService);
|
||||
const parsed = normalizeService(row);
|
||||
|
||||
if (!parsed) {
|
||||
throw new ApiError('Update service response is invalid', 500);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function deleteService(serviceId: string): Promise<void> {
|
||||
await requestJson<{ message?: string }>(`/services/${serviceId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getServiceRuntime(serviceId: string): Promise<ServiceRuntime> {
|
||||
const payload = await requestJson<{ runtime?: ServiceRuntime }>(`/services/${serviceId}/runtime`);
|
||||
return (
|
||||
payload.runtime ?? {
|
||||
desired: 0,
|
||||
status: 'stopped',
|
||||
urls: [],
|
||||
containers: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function serviceAction(serviceId: string, action: 'start' | 'stop' | 'restart'): Promise<void> {
|
||||
await requestJson<{ message?: string }>(`/services/${serviceId}/${action}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export const startService = (serviceId: string) => serviceAction(serviceId, 'start');
|
||||
export const stopService = (serviceId: string) => serviceAction(serviceId, 'stop');
|
||||
export const restartService = (serviceId: string) => serviceAction(serviceId, 'restart');
|
||||
|
||||
export async function redeployService(serviceId: string): Promise<ServiceRuntime> {
|
||||
const payload = await requestJson<{ runtime?: ServiceRuntime }>(`/services/${serviceId}/redeploy`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return payload.runtime ?? { desired: 0, status: 'stopped', urls: [], containers: [] };
|
||||
}
|
||||
|
||||
function normalizeServiceVariables(rows: RawServiceVariable[] | undefined): ServiceVariable[] {
|
||||
const result: ServiceVariable[] = [];
|
||||
for (const row of rows ?? []) {
|
||||
|
||||
Reference in new issue
Block a user