feat: build-on-node — git checkouts, remote image builds, image transport (#60)

Git-sourced deploys never materialized a checkout: sourcePath was
build_path resolved against the server cwd, so builds consumed the
backend's own directory. Remote placement could only run
registry-pulled images.

- internal/source: per-deployment checkout (init/fetch/checkout) at
  branch or commit; GIT_ASKPASS keeps tokens out of argv, .git/config,
  and the build context; dumb-transport and SHA-refusal fallbacks;
  build_path validated inside the checkout.
- BuildManager.PackageContext: produces the same tar context Build
  ships to the daemon — generated Dockerfile at tar root, .git
  excluded. Fixes nixpacks passing the Dockerfile path as content.
- Agent artifact endpoint: GET /api/agents/artifacts/:id streams
  build contexts and docker-save tarballs behind agent-token auth;
  hex-validated ids, per-deployment cleanup, boot-time purge.
- Agent commands: build_image (docker build -, --build-arg values via
  process env, not argv) and load_image (docker load for containr-*
  rollback tags no registry serves).
- Deployment flow: remote git builds package context → artifact →
  node docker-builds the deployment tag; remote rollbacks of local
  tags export via SaveImage → artifact → node docker-loads.

Verified end-to-end: agent-pinned git service built on the node
(build_image → create_container → deployed) and rollback shipped a
docker-save tarball (load_image → deployed). Artifact auth returns
401/404 correctly; workspaces and blobs are removed after deploy.

Authored By: TDvorak <[email protected]>
This commit is contained in:
Tomáš Dvořák authored and GitHub committed 2026-10-09 20:30:33 +02:00
1 parent d57ac72aa5
commit 3cc84553f4
18 files changed
+1020 -51

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ Base URL: `https://<host>/api/v1`. OpenAPI: `docs/api/openapi.yaml`.
- **Variables**: service env values support `${{service.KEY}}` cross-service refs (builtins HOST/PORT) and `${{shared.KEY}}` project shared variables (`GET|PUT /projects/{id}/variables`, CLI `variables shared …`). Resolved at deploy/reconcile, not live-updated.
- **Volumes**: service mounts via `PUT /services/{id}` `volumes: [{type: volume|bind, source, target, read_only}]` — replaces the whole list (`[]` clears), applied on next deploy. `GET /admin/volumes` inventories node volumes with `in_use` flags; `DELETE /admin/volumes/{name}` removes unused ones (admin).
- **Domains**: multiple hostnames per service via `GET|POST /services/{id}/domains`, `DELETE /services/{id}/domains/{domain_id}`, `POST …/{domain_id}/default` (mirrors `services.domain`). `GET /services/{id}/domains/check` returns `ok|wrong-target|pending` per domain vs `app_settings.public_ip`/`PUBLIC_IP`. `PUT` `maintenance_mode` redirects all traffic to `GET /maintenance` (needs `base_url`/`BASE_URL` setting); `basic_auth: [{username,password}]` stores bcrypt htpasswd and gates the route — responses return usernames only.
- **Builders & resources**: service field `builder` = `auto|railpack|nixpacks|dockerfile|static` (auto detects; static builds assets with `static_build_cmd` and serves `static_dir` via nginx:80). `cpu`/`memory` are hard caps; `cpu_reserve`/`memory_reserve` are soft reservations (CPU shares / MemoryReservation) — empty string clears. `app_settings.default_cpu`/`default_memory` set instance defaults; `app_settings.capacity_policy=block` rejects deploys exceeding node memory (default `warn`).
- **Builders & resources**: service field `builder` = `auto|railpack|nixpacks|dockerfile|static` (auto detects; static builds assets with `static_build_cmd` and serves `static_dir` via nginx:80). `cpu`/`memory` are hard caps; `cpu_reserve`/`memory_reserve` are soft reservations (CPU shares / MemoryReservation) — empty string clears. `app_settings.default_cpu`/`default_memory` set instance defaults; `app_settings.capacity_policy=block` rejects deploys exceeding node memory (default `warn`). Services pinned to remote node agents (`node_id`) build git sources on the node itself: the server checks out the repo, packages a Docker context artifact served over the agent-token endpoint (`GET /api/agents/artifacts/{id}`), and the node's `build_image` command runs `docker build` — no registry needed. Rollbacks of locally-built tags ship the image as a docker-save artifact consumed by `load_image`.
- **Clone & move**: `POST /services/{id}/clone` `{name?, project_id?, environment?}` copies config, volumes, domains, access gates, and variables (secret ciphertext verbatim; clone starts stopped). `POST /services/{id}/move` `{project_id}` re-parents a service — both require ownership of source and target; name conflicts → `CONFLICT`. Cloned domains are verbatim copies: the same hostname on two services makes routing ambiguous.
- **Registries**: `GET|POST /registries`, `PUT|DELETE /registries/{id}` — owner-scoped private-image pull credentials, encrypted at rest, never returned (`has_password` signals presence). `host` matched against the image reference on image deploys (`docker.io` for Docker Hub; scheme/case normalized). Empty `password` on update keeps the stored credential.
- **External databases**: `POST /databases/register-external` (probes first; `{name,type,host,port,database,username,password,ssl}`), `POST /databases/test-connection` (ad-hoc probe → `{ok,latency_ms,error}`), `POST /databases/{id}/test-connection` (re-probe with stored credentials). Rows carry `provider:"external"` + `external{host,port,database,username,has_password,ssl}`; passwords encrypted at rest, never returned. Lifecycle actions and backups reject external rows — no container exists.