mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(services): clone/move ops + owner-scoped registry credentials (C4)
Service lifecycle: POST /services/:id/clone duplicates config, volumes, domains, access gates, builder settings, and variables (secret ciphertext copied verbatim); POST /services/:id/move re-parents to another owned project. Both enforce ownership, reject name conflicts, and audit. Registries: CRUD at /registries — host-matched pull credentials for prebuilt-image deploys, encrypted at rest via secrets pkg, never returned (has_password flag). Empty password on update keeps the stored credential; host normalized (scheme/case/trailing slash). Credentials thread through BuildConfig → BuildRequest → pull auth config, used only for the pull. Surfaces: CLI services clone|move + registries group, 7 MCP tools, OpenAPI paths/schemas, frontend Lifecycle card (clone/move/delete) and Settings registries section, llms.txt + CLI.md. 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
912f6f5142
commit
3664eaed1a
20 files changed
+1591
-13
No files matched your search
@@ -1185,6 +1185,68 @@ export async function deleteService(serviceId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export type CloneServiceInput = {
|
||||
name?: string;
|
||||
project_id?: string;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
export async function cloneService(serviceId: string, input: CloneServiceInput): Promise<{ service_id: string; name: string }> {
|
||||
return requestJson<{ service_id: string; name: string }>(`/services/${serviceId}/clone`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export async function moveService(serviceId: string, projectId: string): Promise<void> {
|
||||
await requestJson(`/services/${serviceId}/move`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ project_id: projectId }),
|
||||
});
|
||||
}
|
||||
|
||||
export type Registry = {
|
||||
id: string;
|
||||
name: string;
|
||||
host: string;
|
||||
username?: string;
|
||||
has_password: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type RegistryInput = {
|
||||
name: string;
|
||||
host: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export async function listRegistries(): Promise<Registry[]> {
|
||||
const payload = await requestJson<{ registries?: Registry[] }>('/registries');
|
||||
return payload.registries ?? [];
|
||||
}
|
||||
|
||||
export async function createRegistry(input: RegistryInput): Promise<Registry> {
|
||||
const payload = await requestJson<{ registry: Registry }>('/registries', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return payload.registry;
|
||||
}
|
||||
|
||||
export async function updateRegistry(id: string, input: RegistryInput): Promise<Registry> {
|
||||
const payload = await requestJson<{ registry: Registry }>(`/registries/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return payload.registry;
|
||||
}
|
||||
|
||||
export async function deleteRegistry(id: string): Promise<void> {
|
||||
await requestJson(`/registries/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
function normalizeRuntime(runtime?: ServiceRuntime): ServiceRuntime {
|
||||
const state = runtime ?? { desired: 0, status: 'stopped' as const, urls: [], ports: [], containers: [] };
|
||||
// The API emits null (not []) for empty collections — coerce at the boundary.
|
||||
|
||||
Reference in new issue
Block a user