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:
Tomas DvorakandDevin committed 2026-10-08 17:21:27 +02:00
1 parent 912f6f5142
commit 3664eaed1a
20 files changed
+1591 -13

No files matched your search

+201
View File
@@ -667,6 +667,167 @@ paths:
'200':
description: Reconciled runtime state
/services/{id}/clone:
post:
tags:
- Services
summary: Clone service
description: Duplicates config, volumes, domains, access gates, builder settings, and variables into the same or another project. Containers are not cloned — the copy starts stopped.
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Name for the clone (default "<name>-copy")
project_id:
type: string
format: uuid
description: Target project (default same project; caller must own it)
environment:
type: string
enum: [production, preview, development]
responses:
'201':
description: Clone created
content:
application/json:
schema:
type: object
properties:
service_id:
type: string
format: uuid
name:
type: string
message:
type: string
'409':
description: Name already exists in target project
/services/{id}/move:
post:
tags:
- Services
summary: Move service
description: Re-parents the service to another project the caller owns. Variables, domains, and volumes follow the service; running containers keep running and join the new project's network on next deploy.
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [project_id]
properties:
project_id:
type: string
format: uuid
responses:
'200':
description: Service moved
'409':
description: Name conflict in target project
/registries:
get:
tags:
- Registries
summary: List registry credentials
description: Private-image pull credentials for the caller. Passwords are never returned — `has_password` signals presence.
security:
- bearerAuth: []
responses:
'200':
description: Registry list
content:
application/json:
schema:
type: object
properties:
registries:
type: array
items:
$ref: '#/components/schemas/Registry'
post:
tags:
- Registries
summary: Add registry credential
description: Credentials are matched by image host (e.g. ghcr.io, registry.example.com, docker.io for Docker Hub) on prebuilt-image deploys. Password stored encrypted at rest.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegistryRequest'
responses:
'201':
description: Registry created
'409':
description: A registry for this host already exists
/registries/{id}:
put:
tags:
- Registries
summary: Update registry credential
description: Empty password keeps the stored credential — delete and recreate to remove a password.
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegistryRequest'
responses:
'200':
description: Registry updated
'404':
description: Registry not found
delete:
tags:
- Registries
summary: Remove registry credential
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Registry removed
'404':
description: Registry not found
/services/{id}/domains:
get:
tags:
@@ -5413,6 +5574,44 @@ components:
type: string
description: Plaintext on write — stored as bcrypt htpasswd; never returned
Registry:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
host:
type: string
description: Registry host matched against the image reference — ghcr.io, registry.example.com, docker.io for Docker Hub
username:
type: string
has_password:
type: boolean
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
RegistryRequest:
type: object
required:
- name
- host
properties:
name:
type: string
host:
type: string
username:
type: string
password:
type: string
description: Plaintext on write — stored encrypted; never returned. Empty on update keeps the stored credential.
ServiceVolume:
type: object
required:
@@ -7732,5 +7931,7 @@ tags:
description: Vulnerability scans and compliance reporting
- name: Cron
description: Scheduled jobs
- name: Registries
description: Private-image pull credentials
- name: Gateway
description: API gateway — upstream services, keys, analytics