Commit Graph
14 Commits
Author SHA1 Message Date
Tomas DvorakandDevin b4baa853b2 feat(templates): v2 — multi-service graph templates, expressions, compose import
Config v2 adds services[] to TemplateConfig: members declare key, type
(web|worker|cron|database), runtime, environment, volumes and depends_on.
Deploy topo-sorts the graph, creates databases first (managed DBs attached
to the project network under their key as DNS alias), then services with
resolved env, and enqueues a real deployment per service.

Expressions: {{secret}}, {{secret(N)}}, {{random:N}}, {{VAR}},
{{VAR:-default}} and {{service.<key>.<prop>}} with props host, port, url,
user, password, database. Credential-bearing refs (password/url) mark the
consuming variable secret — stored encrypted, masked in plan output.
Single-service template secret vars are now encrypted at rest too.

New endpoints: POST /templates/:id/plan (dry-resolve with missing/
unresolved reporting), POST /templates/import/compose (compose YAML →
v2 config; known db images become managed databases, build: flagged in
warnings), POST /templates/deploy (ad-hoc graph deploy — the compose
import companion).

Surfaces: CLI gains templates plan/import-compose/deploy-graph plus
--name/--var on deploy; 3 new MCP tools (88 total); OpenAPI + generated
frontend types; TemplatesPage shows stack members, deploys graphs without
a name field, renders the created resource list, and ships an
Import Compose modal feeding the template editor.

Seeded: WordPress + MySQL and n8n + PostgreSQL graph templates.

Partial-failure cleanup: resources created by a failed graph deploy are
removed (rows, containers, volumes).

Verified live: wordpress-stack deploy created a managed MySQL attached to
the project network as `db`; an inline pg+redis+nginx graph deployed with
DATABASE_URL=postgresql://app:<gen>@pg:5432/shop resolvable in-container.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 23:42:08 +02:00
Tomas DvorakandDevin 15045b8dd3 feat(databases): backup import — upload .tar.gz as a restore point
D5. POST /databases/:id/backups/import accepts a gzipped tar archive
(multipart `file` field or raw application/gzip body, 2 GiB ceiling),
stages it to a temp file, streams it into the backup volume via the
started holder container, and registers a completed backup row — a
first-class restore point that also ships offsite when a target is
assigned. External databases rejected.

Also fixes `containr databases restore` — it never sent database_id,
which the API requires, so every CLI restore failed validation.

CLI: databases import-backup <id> <file|-> (stdin support via new
client.DoRaw). UI: Import button in the backups row. OpenAPI + docs.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 23:07:27 +02:00
Tomas DvorakandDevin 6f43bbf8f6 feat(databases): S3-compatible backup targets — offsite archives + remote restore
Phase D3. Managed databases can ship backup archives to a configured
S3-compatible destination (AWS S3, MinIO, Garage, …) while the local
copy remains intact.

- backup_targets table: owner-scoped endpoint/bucket/region/prefix +
  encrypted access/secret keys (enc:v1:, never returned, has_credentials)
- create/update probe bucket access before storing; empty keys on update
  keep stored credentials; delete refused (409) while referenced
- databases.backup_target_id assigns a target; external DBs rejected
- backup pipeline uploads via a holder container over docker cp streams;
  remote failure keeps the local archive (logged), row gets remote:true
- restore prefers the local archive; on a missing local copy it downloads
  the remote object into the backup volume first
- holder containers must be started before archive ops — docker cp does
  not resolve volume mounts on never-started (Created) containers, and
  removing the source mid-stream stalls the reader without EOF
- CLI: backup-targets list|add|update|remove|test + databases update
  --backup-target; MCP: 5 new tools; Settings UI card + per-database
  target picker + offsite badge on backups; OpenAPI + llms.txt + CLI.md

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 22:54:36 +02:00
Tomas DvorakandDevin b7131965b1 feat: public_port for managed databases — opt-in 0.0.0.0 bind
Managed databases have always published their port on 127.0.0.1 only.
public_port opts into binding on all interfaces so the database is
reachable off-host. Toggling via PUT recreates the container — the
named data volume survives.

- create accepts public_port; PUT /databases/{id} toggles it
- external rows reject the flag (no container exists)
- CLI: databases create --public, databases update --public/--public=false
- MCP: databases_update tool; UI: create checkbox + per-row toggle

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 22:19:29 +02:00
Tomas DvorakandDevin 5dc9b5f115 feat: external databases — register, probe, encrypted credentials
Databases hosted outside Containr can now be registered alongside
managed ones. The connection is probed before storing (protocol-level
for postgres/redis-family via real drivers, TCP dial for the rest),
credentials are encrypted at rest and never returned, and external rows
skip managed lifecycle actions, backups, and Docker metrics.

- POST /databases/register-external — probe + create
- POST /databases/test-connection — ad-hoc probe (ok/latency_ms/error)
- POST /databases/{id}/test-connection — re-probe stored credentials
- provider/external fields on database services; privileged reads get a
  composed connection URL with loopback hosts rewritten to the caller's
- CLI: databases register + test-connection; MCP: 3 new tools
- UI: managed/external toggle in the create modal, external badge and
  host display, per-row test button, backups section hidden
- Migration widens the plan CHECK to admit 'external'

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 22:09:42 +02:00
Tomas DvorakandDevin 3664eaed1a 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>
2026-10-08 17:21:27 +02:00
Tomas DvorakandDevin 912f6f5142 feat(services): builder selection, resource reservations, static builder (C3)
services.builder picks the build strategy per service: auto detects from
repo contents (Dockerfile → railpack → nixpacks), explicit overrides force
a builder, and static builds assets in a node stage then serves them from
an nginx image (static_build_cmd/static_dir, port 80 default).

- cpu_reserve/memory_reserve → Docker CPU shares + MemoryReservation;
  cpu/memory stay hard caps (NanoCPUs/Memory). Empty string clears.
- app_settings.default_cpu/default_memory override service-create
  fallbacks; capacity_policy=block fails deploys whose requested memory
  exceeds node capacity (default warn logs via slog).
- Resource specs are validated — "512mb" fails instead of silently
  dropping the limit.
- Fixes: deployments.go hardcoded nixpacks (auto-detect was dead code);
  DockerfileBuilder passed the Dockerfile *path* as build-context content;
  BuildImage never drained the daemon stream so builds could abort and
  errors went unseen — stream is now consumed and errorDetail surfaced.
- CLI: --builder/--cpu-reserve/--memory-reserve/--static-cmd/--static-dir
  on create+update; MCP args on services create/update; UI Build &
  Resources card; OpenAPI + llms.txt + CLI.md.

Verified live: static build of a real repo → nginx image → deployed;
reserves reach HostConfig (shares=256, MemoryReservation=128Mi);
validation rejects bad specs; builder persists + clears.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 17:08:35 +02:00
Tomas DvorakandDevin e995c98d05 feat(services): multi-domain, DNS check, maintenance + basic-auth gates (C2)
service_domains table holds the full hostname set; services.domain stays
the derived default and is synced on every domain write. Runtime spec
carries all domains into a multi-Host Traefik rule plus access gates.

- GET|POST /services/:id/domains, DELETE + POST /:domain_id/default
- GET /services/:id/domains/check — DNS preflight vs public_ip/base_url
  (ok | wrong-target | pending), stamps last_checked_at
- maintenance_mode → Traefik redirectregex to /api/v1/maintenance
  (base_url/BASE_URL setting; falls back to noop@internal offline)
- basic_auth [{username,password}] → bcrypt htpasswd on the label;
  responses return usernames only
- First domain auto-defaults; domain="" via PUT clears all domains
- CLI: services domains list|add|remove|default|check, update
  --maintenance/--basic-auth; MCP: 5 domain tools + update args
- Settings UI: Domains card (CRUD + DNS check badges) + Access card
- Fixes latent C1 bug: PUT without volumes 500'd — nil []byte sent as
  bytea, not NULL; COALESCE needs untyped nil + ::jsonb cast

Verified live: domain CRUD + default sync, pending/wrong-target checks,
bcrypt htpasswd and both middlewares on the deployed container's labels.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 16:51:03 +02:00
Tomas DvorakandDevin 2f8f595d6e feat(services): volume mounts + admin volume inventory (C1)
services.volumes JSONB [{type: volume|bind, source, target, read_only}]
mapped into every replica's container config at deploy and reconcile
time, hashed into the runtime spec so mount changes trigger
reconciliation. Mounted via existing docker.HostConfig.Mounts.

- RuntimeSpec.Volumes + specHash + mount.Mount mapping in createReplica
- Create/update service accepts volumes; PUT replaces the whole list
- Replica-count reconcile now goes through the deploy queue — the last
  raw `go` reconcile left that could race a build
- GET /admin/volumes inventories node volumes with in_use flags;
  DELETE /admin/volumes/:name removes unused ones (409 on in-use)
- CLI: services update --volume/--bind/--clear-volumes, volumes
  list|delete; MCP: services_update + volumes_list/delete
- Settings UI: Volumes section with mount editor
- Migration normalizes the nullable baseline column to NOT NULL [].

Verified live: deploy applies mounts to the container, docker
auto-creates named volumes, in-use deletion is refused.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 16:38:46 +02:00
Tomas DvorakandDevin 7839dc48a3 feat(deploy): per-service deploy queue, cancellable deployments, no-cache builds
- internal/deployqueue: serialized FIFO per service — concurrent deploys
  of one service no longer race (the container-name-conflict failure
  mode); different services still run in parallel.
- Deploys and rollbacks enqueue through the queue; a busy service
  returns status 'queued' and promotes when the active job finishes.
- POST /deployments/:id/cancel — queued jobs drop immediately
  (200 cancelled); active jobs get ctx cancelled so build/reconcile
  abort (202 cancelling, settles 'cancelled'); finished deployments
  return 409 NOT_CANCELLABLE.
- Redeploy and variables save now route through the same queue so they
  can't overlap a build; PUT /services/:id/variables accepts
  redeploy:true to apply env changes to running containers.
- no_cache on deployment create plumbs BuildRequest -> BuildOptions for
  nixpacks, dockerfile and railpack builders.
- CLI: deployments cancel, deploy --no-cache, variables set/unset
  --redeploy. MCP: containr_deployments_cancel; deploy/variables_set
  document no_cache/redeploy. UI: Cancel button + queued/cancelled
  badges on the deployments table. OpenAPI + types regenerated.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 16:23:28 +02:00
Tomas DvorakandDevin 18f78f00a3 feat(api): error codes, idempotency keys, cursor pagination
- Stable machine-readable error codes alongside the error message
  (UNAUTHENTICATED, INVALID_TOKEN, ADMIN_REQUIRED, SCOPE_INSUFFICIENT,
  IDEMPOTENCY_IN_PROGRESS); CLI renders "CODE: message".
- Idempotency middleware on authed/admin groups: POST requests with an
  Idempotency-Key header replay the first response for 24h (Redis-backed,
  caller-scoped); concurrent duplicates get 409. X-Idempotent-Replay
  header marks replays.
- Shared pagination helpers: ?cursor= (opaque, preferred) alongside
  legacy ?page= on projects and deployments lists; pagination meta
  includes next_cursor; invalid cursors fail soft to page 1.
- CLI --idempotency-key flag for safe scripted retries.
- OpenAPI updated: error envelope, cursor params, idempotency contract.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-08 15:38:36 +02:00
Tomas Dvorak e51b20b2d4 feat(cli): v2 — full API coverage, profiles, --json/--yes, containr up
Rewrite the CLI as a thin client over the authenticated API so agents
and scripts reach every resource the UI does (ROADMAP2 phase A2).

- shared Client with bearer auth, error-envelope decoding, and stable
  exit codes: 0 ok / 1 error / 2 auth / 3 not-found / 4 validation /
  5 unreachable
- named profiles in ~/.containr.yaml (current_profile + profiles.*);
  resolution order --flag > env > profile > localhost default;
  CONTAINR_TOKEN / CONTAINR_API_URL / CONTAINR_PROFILE env support
- --json prints raw API responses; --yes skips confirmations;
  destructive commands refuse non-interactively without it
- auth login verifies the token against /user/profile before saving,
  interactive prompt via term.ReadPassword
- commands for every resource: tokens, projects, services, deploy,
  deployments (logs/rollback), logs --follow (SSE), exec, variables
  (set/unset merge with secret masking preserved), databases
  (backup/restore/download), cron, templates, nodes + agent tokens,
  scaling, ha, security, gateway, admin, notifications
- containr up: git remote + branch → create-or-reuse service → deploy

Verified live: login/status/tokens/projects/services, up creating a
service + deployment, non-TTY confirmation refusal, scope-capped admin
rejection (exit 2), revoke.

Authored By: TDvorak <[email protected]>
2026-10-08 15:19:31 +02:00
Tomas DvorakandDevin a99f2e461e feat(ci): release pipeline on merge to main
After tests + image publish, a release job bumps the tag via
conventional commits (github-tag-action, default_bump=false so
docs/chore merges skip releasing), retags :main images in GHCR as
semver+latest (tag pushes via GITHUB_TOKEN do not retrigger CI),
cross-compiles the CLI for 5 platforms, and cuts a GitHub Release
with generated notes and checksums. Manual v* tag pushes get the
same release treatment. CLI --version now reports the tag via the
previously dead -X ldflags.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-23 11:44:37 +02:00
Tomas Dvorak 08cb5754f3 small fix, don't worry about it 2026-04-10 12:02:36 +02:00