diff --git a/app/Actions/V5/Server/SyncDevLimaServers.php b/app/Actions/V5/Server/SyncDevLimaServers.php index 183a29c52..bfdfca1d8 100644 --- a/app/Actions/V5/Server/SyncDevLimaServers.php +++ b/app/Actions/V5/Server/SyncDevLimaServers.php @@ -29,7 +29,6 @@ class SyncDevLimaServers User $user, ?PrivateKey $privateKey, string $clusterName, - int $builderCapacity, array $servers, ): Cluster { $cluster = Cluster::query()->updateOrCreate([ @@ -40,9 +39,7 @@ class SyncDevLimaServers 'description' => 'Local Lima development cluster managed by scripts/dev.sh.', ]); - $builderCapacity = max(0, $builderCapacity); - $builderEnabled = $builderCapacity > 0; - $capabilities = $builderEnabled ? ['coold', 'builder'] : ['coold']; + $capabilities = ['coold']; foreach ($servers as $server) { $wireguardManagementIp = $server['wireguard_management_ip'] ?? null; @@ -54,8 +51,8 @@ class SyncDevLimaServers 'ssh_port' => $server['ssh_port'], 'status' => 'installed', 'capabilities' => $capabilities, - 'builder_enabled' => $builderEnabled, - 'builder_capacity' => $builderCapacity, + 'builder_enabled' => false, + 'builder_capacity' => 0, 'node_address' => $wireguardManagementIp ?: $server['host'], 'wireguard_management_ip' => $wireguardManagementIp, 'last_bootstrapped_at' => now(), diff --git a/app/Console/Commands/V5SyncDevLimaServers.php b/app/Console/Commands/V5SyncDevLimaServers.php index fb9a1dd5d..256df7e7f 100644 --- a/app/Console/Commands/V5SyncDevLimaServers.php +++ b/app/Console/Commands/V5SyncDevLimaServers.php @@ -15,7 +15,6 @@ class V5SyncDevLimaServers extends Command {--user-id=0 : User recorded as creator} {--private-key-id= : Optional private key used by the dev servers} {--cluster=Development-Lima : Cluster name for the dev Lima servers} - {--builder-capacity=2 : Builder capacity to record for each dev server} {--server=* : Server as name|host|ssh_user|ssh_port|wireguard_management_ip} {--force : Allow running outside local/development environments}'; @@ -82,7 +81,6 @@ class V5SyncDevLimaServers extends Command user: $user, privateKey: $privateKey, clusterName: (string) $this->option('cluster'), - builderCapacity: (int) $this->option('builder-capacity'), servers: $parsedServers, ); diff --git a/app/Http/Controllers/V5/DashboardController.php b/app/Http/Controllers/V5/DashboardController.php index b5ec28722..efc18a86e 100644 --- a/app/Http/Controllers/V5/DashboardController.php +++ b/app/Http/Controllers/V5/DashboardController.php @@ -1057,26 +1057,6 @@ class DashboardController extends Controller $command[] = '--skip-default-deny'; } - $builderServers = $servers->filter(fn (V5Server $server) => $server->builder_enabled); - if ($cluster->builder_enabled && $builderServers->isNotEmpty()) { - array_push( - $command, - '--enable-builder', - '--builder-hosts', - $builderServers - ->map(fn (V5Server $server) => $this->bootstrapNode($server)) - ->implode(','), - '--builder-capacity', - (string) $cluster->builder_capacity, - '--builder-cpu-quota', - $cluster->builder_cpu_quota, - '--builder-memory-max', - $cluster->builder_memory_max, - '--builder-timeout-secs', - (string) $cluster->builder_timeout_secs, - ); - } - $command[] = '--yes'; return $command; @@ -1519,7 +1499,6 @@ class DashboardController extends Controller private function serverCapabilities(bool $builderEnabled, bool $ingressEnabled): array { return collect(['coold']) - ->when($builderEnabled, fn ($capabilities) => $capabilities->push('builder')) ->when($ingressEnabled, fn ($capabilities) => $capabilities->push('ingress')) ->unique() ->values() diff --git a/app/Jobs/V5BootstrapServerJob.php b/app/Jobs/V5BootstrapServerJob.php index 33e18acc9..d19ea4ccd 100644 --- a/app/Jobs/V5BootstrapServerJob.php +++ b/app/Jobs/V5BootstrapServerJob.php @@ -100,7 +100,7 @@ class V5BootstrapServerJob implements ShouldBeEncrypted, ShouldQueue return; } - $result = Process::timeout(max(60, (int) $cluster->builder_timeout_secs + 120)) + $result = Process::timeout(300) ->run($this->bootstrapCommand($cluster, $servers, $server, $sshConfigLocation, $action)); $output = trim($result->output()."\n".$result->errorOutput()); $successful = $result->successful(); @@ -119,7 +119,6 @@ class V5BootstrapServerJob implements ShouldBeEncrypted, ShouldQueue $capabilities = collect($server->capabilities ?? []) ->push('coold') - ->when($server->builder_enabled, fn ($capabilities) => $capabilities->push('builder')) ->when($server->isIngress(), fn ($capabilities) => $capabilities->push('ingress')) ->unique() ->values() @@ -229,26 +228,6 @@ class V5BootstrapServerJob implements ShouldBeEncrypted, ShouldQueue $command[] = '--skip-default-deny'; } - $builderServers = $servers->filter(fn (V5Server $server) => $server->builder_enabled); - if ($cluster->builder_enabled && $builderServers->isNotEmpty()) { - array_push( - $command, - '--enable-builder', - '--builder-hosts', - $builderServers - ->map(fn (V5Server $server) => $this->bootstrapNode($server)) - ->implode(','), - '--builder-capacity', - (string) $cluster->builder_capacity, - '--builder-cpu-quota', - $cluster->builder_cpu_quota, - '--builder-memory-max', - $cluster->builder_memory_max, - '--builder-timeout-secs', - (string) $cluster->builder_timeout_secs, - ); - } - $command[] = '--yes'; return $command; diff --git a/app/Services/Flux/FluxClient.php b/app/Services/Flux/FluxClient.php index 35dc18062..006cae06f 100644 --- a/app/Services/Flux/FluxClient.php +++ b/app/Services/Flux/FluxClient.php @@ -13,7 +13,7 @@ class FluxClient public function listContainers(string $hostId): array { $payload = $this->dispatch($hostId, [ - 'type' => 'list_containers', + 'type' => 'containers.list', ]); $data = $payload['data'] ?? []; diff --git a/config/coold.php b/config/coold.php index b491077b5..06b47083d 100644 --- a/config/coold.php +++ b/config/coold.php @@ -4,7 +4,5 @@ return [ 'coolify_cli_bin' => env('COOLIFY_CLI_BIN', '/usr/local/bin/coolify'), 'coold_version' => env('COOLIFY_COOLD_VERSION', 'nightly'), 'corrosion_version' => env('COOLIFY_CORROSION_VERSION', 'v1.0.0'), - 'dev_builder_capacity' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2), - 'dev_builder_enabled' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2) > 0, 'dev_ssh_user' => env('COOLIFY_CLI_SSH_USER', 'coolify'), ]; diff --git a/database/seeders/V5DevLimaSeeder.php b/database/seeders/V5DevLimaSeeder.php index 3e5a16b4f..6dbf015c1 100644 --- a/database/seeders/V5DevLimaSeeder.php +++ b/database/seeders/V5DevLimaSeeder.php @@ -30,7 +30,6 @@ class V5DevLimaSeeder extends Seeder ->orderBy('id') ->first(); - $builderCapacity = max(0, (int) config('coold.dev_builder_capacity', 2)); $sshUser = (string) config('coold.dev_ssh_user', 'coolify'); $servers = collect($this->servers()) ->map(fn (array $server): array => [ @@ -44,7 +43,6 @@ class V5DevLimaSeeder extends Seeder user: $user, privateKey: $privateKey, clusterName: self::CLUSTER_NAME, - builderCapacity: $builderCapacity, servers: $servers, ); } diff --git a/docs/v5/architecture/README.md b/docs/v5/architecture/README.md index 8d7daa9e6..d9b2c9dd2 100644 --- a/docs/v5/architecture/README.md +++ b/docs/v5/architecture/README.md @@ -25,6 +25,7 @@ Podman / networks / firewall / DNS / Corrosion / builder | [Responsibility split](responsibility-split.md) | What belongs in Coolify, Flux, and coold. | | [Primitives](primitives.md) | Canonical host primitive surface Coolify may dispatch. | | [Deploy flows](deploy-flows.md) | User-functionality flows, starting with `nginx:alpine`. | +| [ADRs](adr/README.md) | Architecture decision records for Coolify, Flux, coold, and their contracts. | | [ADR 0001](adr/0001-control-plane-flux-coold-split.md) | Decision record for the v5 split. | ## Core rule diff --git a/docs/v5/architecture/adr/0001-control-plane-flux-coold-split.md b/docs/v5/architecture/adr/0001-control-plane-flux-coold-split.md index 57c6f3d36..0fad334d8 100644 --- a/docs/v5/architecture/adr/0001-control-plane-flux-coold-split.md +++ b/docs/v5/architecture/adr/0001-control-plane-flux-coold-split.md @@ -12,8 +12,10 @@ must own product behavior and durable state, but it should not hold thousands of long-lived agent streams or directly expose host runtime sockets. Host operations also need a narrow privileged boundary. Podman, firewall, DNS, -Corrosion, and build supervision require local host privileges that should not -be spread across the Laravel app or arbitrary scripts. +and Corrosion require local host privileges that should not be spread across the +Laravel app or arbitrary scripts. Future builder supervision belongs behind the +same boundary, but its active primitive/API shape is deferred to a separate +decision. ## Decision @@ -27,7 +29,7 @@ Coolify v5 uses three distinct building blocks: primitive requests to connected hosts and resolves pending responses. 3. **coold** runs once per host. It exposes a closed set of host primitives and owns privileged local execution through Podman, firewall, DNS, Corrosion, - host facts, and builder supervision. + and host facts. coold must not expose raw Podman passthrough. Every supported operation must be an explicit primitive with validation and a stable protocol shape. diff --git a/docs/v5/architecture/adr/0002-coold-host-agent-boundary.md b/docs/v5/architecture/adr/0002-coold-host-agent-boundary.md new file mode 100644 index 000000000..b4b5a3d00 --- /dev/null +++ b/docs/v5/architecture/adr/0002-coold-host-agent-boundary.md @@ -0,0 +1,66 @@ +# ADR 0002: Keep coold as a narrow host agent + +## Status + +Accepted. + +## Context + +Coolify v5 needs a process on each managed host that can reach local runtime +surfaces such as Podman, Corrosion, DNS bind addresses, and firewall state. That +process must run close to privileged host APIs, but Coolify's product model, +RBAC, deployment history, billing, and audit state belong in the Laravel control +plane. + +If coold grows app-aware behavior, it becomes a second control plane with local +copies of product rules. If Coolify reaches around coold with raw host access, +the privileged boundary disappears and host behavior becomes harder to validate. + +## Decision + +coold is a per-host agent with a narrow, explicit primitive surface. It executes +host-local operations requested through Flux, reports typed results, and owns +host safety checks for those operations. + +coold owns local runtime integration: Podman access, service-discovery sync, +embedded DNS, Corrosion writes for this host's endpoints, host facts, and the +firewall mutation/reconciliation surface when those primitives are active. + +coold must not own Coolify product concepts. It does not decide what an +application, project, team, deployment, domain, rollback, billing event, or audit +record means. Those concepts remain in Coolify Laravel. coold also must not +expose raw Podman passthrough; every operation needs an explicit primitive with +validation and a stable protocol shape. + +Builder supervision is intentionally deferred. When it returns, it should be +recorded in a separate ADR/API because scheduling, capacity, logs, artifacts, +cancellation, restart adoption, and registry flow need their own trade-off. + +## Consequences + +### Positive + +- coold can be reasoned about as reusable host infrastructure, not a Coolify app + runtime. +- Privileged host access has one narrow boundary with explicit validation. +- The primitive API can evolve through reviewed protocol additions instead of + ad hoc Podman command exposure. +- Coolify Laravel remains the only owner of durable product state and business + audit. + +### Negative + +- New deploy features may require new coold primitives before they can ship. +- Some operations are more verbose than raw Podman passthrough. +- Cross-repo changes must keep Coolify, Flux, and coold protocol expectations in + sync. + +## Boundary rules + +- If the question is "is this user/team allowed to do this?", Coolify answers. +- If the question is "is this host operation safe to execute here?", coold may + reject it. +- If the operation only makes sense because of Coolify's product model, it stays + out of coold. +- If another orchestrator could safely reuse the same host operation, it is a + candidate coold primitive. diff --git a/docs/v5/architecture/adr/0003-flux-connection-broker-boundary.md b/docs/v5/architecture/adr/0003-flux-connection-broker-boundary.md new file mode 100644 index 000000000..6a832c25b --- /dev/null +++ b/docs/v5/architecture/adr/0003-flux-connection-broker-boundary.md @@ -0,0 +1,65 @@ +# ADR 0003: Keep Flux as the connection broker + +## Status + +Accepted. + +## Context + +Coolify v5 needs to send host primitive requests to many managed hosts, including +hosts behind NAT, firewalls, or corporate networks. coold can solve the host +inbound problem by dialing out, but Laravel request workers should not own +thousands of long-lived HTTP/2 agent streams or in-memory pending response maps. + +The system also needs a place to translate between Coolify's short-lived local +request/response lane and coold's long-lived outbound stream without turning that +place into another product control plane. + +## Decision + +Flux is the central connection broker between Coolify Laravel and coold agents. +Laravel talks to Flux over a local Unix socket. coold agents dial Flux over an +outbound authenticated gRPC stream. Flux keeps the connected-host stream +registry, routes requests to the selected host stream, tracks pending request IDs, +and resolves typed responses back to Laravel. + +Flux owns transport concerns: stream lifecycle, request correlation, timeouts, +disconnected-host responses, pending-request caps, late-result handling, and +host-agent authentication for inbound coold streams. + +Flux must not own Coolify product concepts. It does not decide which user may +deploy, which host should run an application, what a domain means, how rollback +works, or how deployment state advances. Flux treats `containers.start` or +`images.pull` as protocol frames routed to a host, not as product actions. + +## Consequences + +### Positive + +- Hosts can remain behind NAT because coold dials out to Flux. +- Laravel stays focused on durable product state and short-lived request work. +- Long-lived stream management can scale and fail independently from PHP-FPM + workers. +- Backpressure, timeouts, and disconnected-host behavior have one transport + boundary. +- Flux can be tested as a protocol router without needing Coolify's app model. + +### Negative + +- The architecture has one more runtime component to deploy and monitor. +- Protocol changes must stay coordinated across Coolify, Flux, and coold. +- Flux outage blocks dispatch to connected hosts even when Laravel and hosts are + otherwise healthy. +- Flux must stay intentionally narrow; adding product decisions would create a + second control plane. + +## Boundary rules + +- Coolify chooses the target host; Flux only routes to that host's connected + stream. +- coold authenticates to Flux as a host agent; Laravel reaches Flux through the + local Unix-socket lane. +- Flux may answer transport failures such as disconnected host, timeout, or + pending-cap overflow. +- Flux may not inspect product ownership, RBAC, billing, deployment state, + domains, secrets, or audit meaning. diff --git a/docs/v5/architecture/adr/0004-coolify-control-plane-boundary.md b/docs/v5/architecture/adr/0004-coolify-control-plane-boundary.md new file mode 100644 index 000000000..8fb5c8d81 --- /dev/null +++ b/docs/v5/architecture/adr/0004-coolify-control-plane-boundary.md @@ -0,0 +1,70 @@ +# ADR 0004: Keep Coolify Laravel as the product control plane + +## Status + +Accepted. + +## Context + +Coolify v5 manages user intent across teams, projects, environments, +applications, services, databases, servers, domains, deployments, secrets, +notifications, billing, and audit history. Those concepts require durable state, +RBAC, validation, UI/API workflows, and user-facing status. + +The v5 runtime also needs Flux and coold so hosts behind NAT can receive work and +privileged host operations stay close to the host. If product decisions move into +Flux or coold, the system gains multiple control planes with duplicated rules and +unclear ownership. If Coolify directly mutates hosts, the Flux/coold boundary is +bypassed and host safety checks become optional. + +## Decision + +Coolify Laravel is the product control plane for v5. It owns user intent, +durable product state, RBAC, API tokens, sessions, SSO/OAuth, projects, +environments, resources, deployment state machines, placement decisions, proxy +and ingress intent, secret resolution, notifications, billing/subscriptions, +business audit, deployment logs, and user-facing status. + +Coolify chooses what should happen and which host should receive the work. It +then turns product intent into ordered host primitives and dispatches them +through Flux to coold. Coolify records primitive results and advances the durable +resource or deployment state. + +Coolify must not hold long-lived coold streams or directly expose privileged host +runtime sockets as part of normal v5 operation. It also must not rely on Flux or +coold to understand product concepts such as teams, applications, domains, +rollbacks, billing, or audit meaning. + +## Consequences + +### Positive + +- Product behavior has one durable source of truth. +- RBAC, validation, audit, and user-facing status stay near the UI/API and + database model. +- Flux and coold remain reusable infrastructure boundaries instead of becoming + hidden product layers. +- Deployment state machines can be tested against typed primitive results rather + than raw host side effects. + +### Negative + +- Coolify must model enough deployment state to recover from partial host + failures and retries. +- New user-facing features may require both product changes in Coolify and new + primitives in coold. +- Coolify cannot shortcut missing primitives by using raw host access without + weakening the architecture boundary. + +## Boundary rules + +- If the decision involves a user, team, project, environment, resource, + deployment, domain, secret, notification, billing event, or audit record, + Coolify owns it. +- If the decision is which connected host should receive work, Coolify chooses + and Flux routes. +- If the work is a concrete host operation, Coolify dispatches an explicit + primitive instead of mutating the host directly. +- If Coolify cannot express a required host operation as an existing primitive, + the protocol needs a reviewed primitive addition instead of a product-layer + bypass. diff --git a/docs/v5/architecture/adr/README.md b/docs/v5/architecture/adr/README.md new file mode 100644 index 000000000..7b2adec0e --- /dev/null +++ b/docs/v5/architecture/adr/README.md @@ -0,0 +1,52 @@ +# Architecture Decision Records + +This directory records architecture decisions for the Coolify v5 ecosystem: +Coolify Laravel, Flux, coold, and the contracts between them. + +ADRs are for decisions that are hard to reverse, surprising without context, and +made after a real trade-off. If a note is just reference material, put it in the +main architecture docs instead. + +## Index + +| ADR | Decision | +| --- | --- | +| [0001](0001-control-plane-flux-coold-split.md) | Split v5 into Coolify control plane, Flux broker, and coold host agent. | +| [0002](0002-coold-host-agent-boundary.md) | Keep coold as a narrow host agent with explicit primitives. | +| [0003](0003-flux-connection-broker-boundary.md) | Keep Flux as the connection broker between Coolify and coold. | +| [0004](0004-coolify-control-plane-boundary.md) | Keep Coolify Laravel as the product control plane. | + +## Format + +Use the next sequential number and a short slug: + +```text +0005-short-decision-slug.md +``` + +Start small. A useful ADR can be one paragraph: + +```md +# ADR 0005: Short title of the decision + +Coolify v5 will ... because ... This trades ... for ... +``` + +Add optional sections only when they clarify the decision: + +- **Status**: `Proposed`, `Accepted`, `Deprecated`, or `Superseded by ADR NNNN`. +- **Context**: what forced the decision. +- **Decision**: what we chose. +- **Consequences**: non-obvious benefits, costs, or follow-up constraints. +- **Considered options**: rejected alternatives worth remembering. + +## Scope guide + +- Coolify ADRs cover product/control-plane state, RBAC, deployments, billing, + APIs, UI-visible behavior, and durable audit/history. +- Flux ADRs cover connection brokerage, host stream routing, request/response + correlation, backpressure, and transport-level authentication. +- coold ADRs cover host primitives, privileged execution, Podman/firewall/DNS, + local safety rules, and agent lifecycle. +- Cross-cutting ADRs belong here when the decision changes contracts between + Coolify, Flux, and coold. diff --git a/docs/v5/architecture/deploy-flows.md b/docs/v5/architecture/deploy-flows.md deleted file mode 100644 index 61bab3935..000000000 --- a/docs/v5/architecture/deploy-flows.md +++ /dev/null @@ -1,76 +0,0 @@ -# v5 Deploy Flows - -These flows are written from the user-functionality view. Coolify owns the -state machine; Flux routes primitive requests; coold executes host operations. - -## Flow: deploy Docker image app `nginx:alpine` - -User intent: - -```text -Run nginx:alpine on host H1, expose port 80, and route nginx.example.com to it. -``` - -### Steps - -| Step | Coolify | Flux | coold | -| --- | --- | --- | --- | -| Create app | Stores app, source type `docker_image`, image `nginx:alpine`, target host, port, domain. | No action. | No action. | -| Start deploy | Creates deployment record and enters deploy state machine. | No action until dispatch. | No action. | -| Pull image | Sends `images.pull` to H1. | Routes request to H1's stream. | Pulls image through Podman. | -| Prepare network/volume | Sends required `networks.create` / `volumes.create` operations. | Routes requests. | Creates idempotent host resources. | -| Create container | Sends `containers.create` with image, name, env, labels, network, port, health check, mounts, DNS. | Routes request. | Applies deny filter and creates container through Podman. | -| Start container | Sends `containers.start`. | Routes request. | Starts container. | -| Check status | Sends `containers.inspect` or reads status stream. | Routes request. | Reads local Podman state. | -| Register endpoint | Sends `services.register`. | Routes request. | Writes this host's endpoint row to Corrosion. | -| Allow traffic | Sends `firewall.allow` for proxy-to-container traffic when required. | Routes request. | Writes iptables and nft allow rules. | -| Configure ingress | Renders proxy config from Coolify domain/resource state. If reload is needed, sends `containers.exec` or equivalent primitive for the proxy container. | Routes runtime reload request. | Executes the reload primitive only. | -| Mark running | Stores final status, deployment result, and user-facing logs. | Resolves final responses. | Keeps container running and reports future state. | - -### Expected data locations - -| Data | Owner | -| --- | --- | -| App name, team, project, env, domain, image ref, desired port | Coolify database | -| Deployment state and history | Coolify database | -| Open host stream and pending request IDs | Flux memory | -| Pulled image and running container | Host Podman storage | -| Endpoint rows for this host | Corrosion via coold | -| Firewall tuples and snapshots | Host kernel + `/etc/coolify` via coold | - -### No build path - -`nginx:alpine` is a prebuilt image. This flow does not use Git clone, -Dockerfile, buildpacks, Railpack, or the builder subprocess. - -## Flow: delete the nginx app - -| Step | Coolify | Flux | coold | -| --- | --- | --- | --- | -| User deletes resource | Validates permission and starts cleanup state machine. | No action until dispatch. | No action. | -| Stop container | Sends `containers.stop`. | Routes request. | Stops container. | -| Remove container | Sends `containers.delete`. | Routes request. | Deletes container. | -| Remove endpoint | Sends `services.unregister`. | Routes request. | Removes service endpoint row. | -| Remove firewall rule | Sends `firewall.revoke`. | Routes request. | Removes iptables and nft allow rules. | -| Update proxy | Removes rendered route and reloads proxy if needed. | Routes reload primitive. | Executes proxy reload primitive. | -| Finish cleanup | Marks resource deleted and records outcome. | Resolves responses. | No product state retained. | - -## Flow: Git app with build - -A Git app adds a build phase before container creation. - -```text -Coolify resolves source + secrets + build config - ↓ -Flux routes build request to a host with builder capability - ↓ -coold supervises builder subprocess - ↓ -builder writes image/result - ↓ -Coolify deploy state machine continues with image/container primitives -``` - -coold supervises the builder process. The builder owns build implementation. -Coolify owns the decision to build, the build configuration, and the deployment -state transitions around the build. diff --git a/docs/v5/architecture/overview.md b/docs/v5/architecture/overview.md deleted file mode 100644 index f7298773b..000000000 --- a/docs/v5/architecture/overview.md +++ /dev/null @@ -1,38 +0,0 @@ -# v5 Architecture Overview - -Coolify v5 separates product intent from host execution. - -- **Coolify Laravel** is the control plane. It owns users, teams, projects, - environments, resources, deployments, domains, secrets, RBAC, audit-worthy - state, and the deployment state machines. -- **Flux** is the connection broker. Laravel talks to Flux over a Unix socket. - coold agents dial Flux over outbound gRPC streams. Flux maps host IDs to - connected streams and maps request IDs to pending responses. -- **coold** is the per-host executor. It owns local runtime access: Podman, - host networks, DNS, firewall mutations, service-discovery sync, host facts, - and builder subprocess supervision. - -## Data flow - -```text -1. A user/API/webhook asks Coolify to do something. -2. Coolify validates permissions and stores desired state in its database. -3. Coolify's state machine turns that intent into ordered host primitives. -4. Coolify sends each primitive to Flux over /run/coolify/flux.sock. -5. Flux forwards the primitive to the target host's open coold stream. -6. coold executes the primitive locally and returns a typed result. -7. Flux resolves the pending request. -8. Coolify records the result and moves the deployment/resource state forward. -``` - -## Non-goals for Flux and coold - -Flux and coold are not product layers. They do not decide which app to deploy, -which user is allowed to deploy, how to roll back, what domains mean, or where -business audit belongs. They only handle routing and host execution. - -## Current implementation note - -This directory describes the target v5 architecture. Some primitives are not -implemented in coold yet. Until implemented, docs should label them as target -primitives and code should not pretend they exist. diff --git a/docs/v5/architecture/primitives.md b/docs/v5/architecture/primitives.md index 3f768afa2..2051ea8ae 100644 --- a/docs/v5/architecture/primitives.md +++ b/docs/v5/architecture/primitives.md @@ -82,16 +82,11 @@ traffic and nft bridge rules for same-bridge traffic. | `host.stats` | Return CPU, memory, disk, and container stats snapshot. | | `host.containers` | Return host container summaries. | -## Builder +## Builder (deferred) -Builds are routed through Flux to a host with builder capability. coold should -supervise the builder process but should not implement build logic inline. - -| Primitive | Purpose | -| --- | --- | -| `build.dispatch` | Start a builder request on a capable host. | -| `build.cancel` | Cancel a running build request. | -| `build.result` | Return or long-poll build result. | +Builder is intentionally not part of the active v5 Flux/coold primitive +surface. Before reintroducing it, add an ADR/API covering scheduling, capacity, +logs, artifacts, cancellation, restart adoption, and registry flow. ## Not primitives diff --git a/scripts/coold-vm.sh b/scripts/coold-vm.sh index 627419535..0461f796e 100755 --- a/scripts/coold-vm.sh +++ b/scripts/coold-vm.sh @@ -28,7 +28,6 @@ INSTANCE="$(read_coolify_env COOLIFY_COOLD_LIMA_INSTANCE coold-dev)" VERSION="$(read_coolify_env COOLIFY_COOLD_VERSION nightly)" CORROSION_VERSION="$(read_coolify_env COOLIFY_CORROSION_VERSION v1.0.0)" FLUX_URL="$(read_coolify_env COOLIFY_COOLD_VM_FLUX_URL http://host.lima.internal:6443)" -BUILDER_CAPACITY="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)" START_TIMEOUT="$(read_coolify_env COOLIFY_COOLD_VM_START_TIMEOUT 300)" WG_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_IP "")" WG_PEER_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_IP "")" @@ -36,10 +35,6 @@ WG_PEER_ENDPOINT="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_ENDPOINT "")" WG_PEER_PUBLIC_KEY="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_PUBLIC_KEY "")" CONTAINER_SUBNET="$(read_coolify_env COOLIFY_COOLD_VM_CONTAINER_SUBNET 10.210.0.0/24)" CONTAINER_GATEWAY="$(read_coolify_env COOLIFY_COOLD_VM_CONTAINER_GATEWAY 10.210.0.1)" -BUILDER_ENABLED="true" -if [ "$BUILDER_CAPACITY" = "0" ]; then - BUILDER_ENABLED="false" -fi TEMPLATE="$ROOT/dev/lima/coold.yaml" GUEST_COOLIFY_ROOT="/workspace/coolify" @@ -68,7 +63,6 @@ Environment: COOLIFY_COOLD_VERSION coold release tag to install (default: nightly) COOLIFY_CORROSION_VERSION corrosion release tag to install (default: v1.0.0) COOLIFY_COOLD_VM_FLUX_URL Flux gRPC URL visible from the VM (default: http://host.lima.internal:6443) - COOLIFY_COOLD_VM_BUILDER_CAPACITY VM builder capacity to advertise (default: 2; set 0 to disable) COOLIFY_COOLD_VM_START_TIMEOUT Seconds to wait for Lima SSH/provisioning (default: 300) COOLIFY_COOLD_VM_WG_IP Optional WireGuard mgmt IP for this host COOLIFY_COOLD_VM_CONTAINER_SUBNET Podman mesh subnet for this host @@ -270,7 +264,7 @@ write_runtime_config() { bootstrap="\"$WG_PEER_IP:8787\"" fi - lima_shell sudo install -d -m 0755 /etc/corrosion/schemas /etc/coolify /run/coolify /var/lib/corrosion /var/run/corrosion /var/lib/coolify-dev /var/lib/coolify-builder/work + lima_shell sudo install -d -m 0755 /etc/corrosion/schemas /etc/coolify /run/coolify /var/lib/corrosion /var/run/corrosion /var/lib/coolify-dev lima_shell sudo tee /etc/corrosion/schemas/coolify.sql >/dev/null <<'SQL' CREATE TABLE service_endpoints ( @@ -315,14 +309,11 @@ run_foreground() { (cd /tmp && limactl shell "$INSTANCE" -- sudo \ env COOLIFY_COOLD_HOST_MGMT_IP="${WG_IP:-127.0.0.1}" \ COOLIFY_COOLD_FLUX_URL="$FLUX_URL" \ - COOLIFY_COOLD_BUILDER_ENABLED="$BUILDER_ENABLED" \ - COOLIFY_COOLD_BUILDER_CAPACITY="$BUILDER_CAPACITY" \ CONTAINER_GATEWAY="$CONTAINER_GATEWAY" \ bash -s) <<'RUNNER' set -euo pipefail echo "coold: $(/usr/local/bin/coold --version)" -echo "builder: $(/usr/local/bin/builder --version)" echo "corrosion: $(/usr/local/bin/corrosion --version 2>/dev/null || cat /usr/local/bin/corrosion.version)" echo "starting packaged coold endpoint with corrosion in local mode" @@ -340,10 +331,6 @@ COOLIFY_COOLD_NAMESPACES="${COOLIFY_COOLD_NAMESPACES:-default:coolify-default-me COOLIFY_COOLD_DNS_ZONE="${COOLIFY_COOLD_DNS_ZONE:-coolify.internal}" \ COOLIFY_COOLD_FLUX_URL="${COOLIFY_COOLD_FLUX_URL:-http://host.lima.internal:6443}" \ COOLIFY_COOLD_HOST_JWT_PATH="${COOLIFY_COOLD_HOST_JWT_PATH:-/etc/coolify/host-jwt}" \ -COOLIFY_COOLD_BUILDER_ENABLED="${COOLIFY_COOLD_BUILDER_ENABLED:-true}" \ -COOLIFY_COOLD_BUILDER_CAPACITY="${COOLIFY_COOLD_BUILDER_CAPACITY:-2}" \ -COOLIFY_COOLD_BUILDER_BIN="${COOLIFY_COOLD_BUILDER_BIN:-/usr/local/bin/builder}" \ -COOLIFY_COOLD_BUILDER_WORK_DIR="${COOLIFY_COOLD_BUILDER_WORK_DIR:-/var/lib/coolify-builder/work}" \ /usr/local/bin/coold & wait @@ -442,10 +429,6 @@ Environment=COOLIFY_COOLD_NAMESPACES=default:coolify-default-mesh:$CONTAINER_GAT Environment=COOLIFY_COOLD_DNS_ZONE=coolify.internal Environment=COOLIFY_COOLD_FLUX_URL=$FLUX_URL Environment=COOLIFY_COOLD_HOST_JWT_PATH=/etc/coolify/host-jwt -Environment=COOLIFY_COOLD_BUILDER_ENABLED=$BUILDER_ENABLED -Environment=COOLIFY_COOLD_BUILDER_CAPACITY=$BUILDER_CAPACITY -Environment=COOLIFY_COOLD_BUILDER_BIN=/usr/local/bin/builder -Environment=COOLIFY_COOLD_BUILDER_WORK_DIR=/var/lib/coolify-builder/work ExecStart=/usr/local/bin/coold AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_NET_RAW Restart=on-failure diff --git a/scripts/dev.sh b/scripts/dev.sh index 309145b9c..b9a081614 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -413,13 +413,8 @@ mint_host_jwt_for_host() { local attempts=60 local output local caps - local builder_capacity - builder_capacity="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)" caps="coold" - if [ "$builder_capacity" != "0" ]; then - caps="coold,builder" - fi for attempt in $(seq 1 "$attempts"); do if output="$(spin exec -T coolify php artisan flux:dev "$host_id" --caps="$caps" 2>&1)"; then @@ -496,7 +491,6 @@ sync_v5_dev_lima_servers() { spin exec -T \ -e COOLIFY_CLI_SSH_USER="$ssh_user" \ coolify php artisan v5:sync-dev-lima-servers \ - --builder-capacity="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)" \ "${server_args[@]}" } diff --git a/tests/Feature/DevEnvironmentExampleTest.php b/tests/Feature/DevEnvironmentExampleTest.php index 8b93adc90..386060548 100644 --- a/tests/Feature/DevEnvironmentExampleTest.php +++ b/tests/Feature/DevEnvironmentExampleTest.php @@ -14,7 +14,6 @@ it('does not include coold dev tooling defaults in the development env example', 'coold VM WireGuard IP 2 default' => 'COOLIFY_COOLD_VM_WG_IP_2', 'coold VM WireGuard port 1 default' => 'COOLIFY_COOLD_VM_WG_PORT_1', 'coold VM WireGuard port 2 default' => 'COOLIFY_COOLD_VM_WG_PORT_2', - 'coold VM builder capacity default' => 'COOLIFY_COOLD_VM_BUILDER_CAPACITY', 'coold VM enabled default' => 'COOLIFY_COOLD_VM_ENABLED', 'coold VM stop on down default' => 'COOLIFY_COOLD_VM_STOP_ON_DOWN', 'dev follow logs default' => 'COOLIFY_DEV_FOLLOW_LOGS', @@ -25,8 +24,7 @@ it('defaults coold dev VM settings in Laravel config', function () { ->and(config('coold.coold_version'))->toBe('nightly') ->and(config('coold.corrosion_version'))->toBe('v1.0.0') ->and(config('coold.dev_ssh_user'))->toBe('coolify') - ->and(config('coold.dev_builder_capacity'))->toBe(2) - ->and(config('coold.dev_builder_enabled'))->toBeTrue(); + ->and(config('coold.dev_ssh_user'))->toBe('coolify'); }); it('runs the v5 dev Lima seeder with the normal development database seeder', function () { diff --git a/tests/Feature/FluxDevCommandTest.php b/tests/Feature/FluxDevCommandTest.php index 6c9a3c810..58279582c 100644 --- a/tests/Feature/FluxDevCommandTest.php +++ b/tests/Feature/FluxDevCommandTest.php @@ -12,7 +12,7 @@ it('mints a host jwt signed by the configured flux private key', function () { $exitCode = Artisan::call('flux:dev', [ 'host_id' => 'coold-dev', - '--caps' => 'coold,builder', + '--caps' => 'coold', '--ttl' => '600', ]); @@ -23,7 +23,7 @@ it('mints a host jwt signed by the configured flux private key', function () { expect($claims->sub)->toBe('coold-dev') ->and($claims->aud)->toBe('coold') - ->and($claims->caps)->toBe(['coold', 'builder']) + ->and($claims->caps)->toBe(['coold']) ->and($claims->exp)->toBeGreaterThan(time()); }); diff --git a/tests/Feature/V5/DashboardTest.php b/tests/Feature/V5/DashboardTest.php index 7603eb9fc..5b063af71 100644 --- a/tests/Feature/V5/DashboardTest.php +++ b/tests/Feature/V5/DashboardTest.php @@ -2291,7 +2291,7 @@ it('adds a v5 server to a cluster for the current team', function () { ->assertJsonPath('cluster.servers.0.builderCpuQuota', '200%') ->assertJsonPath('cluster.servers.0.ingressEnabled', false) ->assertJsonPath('cluster.servers.0.ingressType', null) - ->assertJsonPath('cluster.servers.0.capabilities', ['coold', 'builder']) + ->assertJsonPath('cluster.servers.0.capabilities', ['coold']) ->assertJsonPath('cluster.servers.0.wireguardListenPortOverride', 51821) ->assertJsonPath('cluster.servers.0.wireguardEndpointOverride', 'prod-01.example.com:51821') ->assertJsonPath('cluster.servers.0.wireguardManagementIp', null) @@ -2310,7 +2310,7 @@ it('adds a v5 server to a cluster for the current team', function () { ->exists())->toBeTrue(); expect(V5Server::query()->where('name', 'prod-01')->first()->capabilities) - ->toBe(['coold', 'builder']); + ->toBe(['coold']); }); it('adds a v5 server with caddy ingress enabled', function () { @@ -2720,7 +2720,7 @@ it('bootstraps a single v5 server with the Coolify CLI', function () { && cliFlagValue($command, '--corrosion-version') === 'v1.1.0' && cliFlagValue($command, '--wg-listen-port-overrides') === $node.'=51831' && cliFlagValue($command, '--wg-endpoint-overrides') === $node.'=prod-01.example.com:51831' - && in_array('--enable-builder', $command, true) + && ! in_array('--enable-builder', $command, true) && in_array('--yes', $command, true) && ! in_array('--new-nodes', $command, true); }); @@ -3143,7 +3143,7 @@ it('updates editable v5 server builder details without changing networking', fun expect($server->builder_enabled)->toBeTrue() ->and($server->builder_capacity)->toBe(5) ->and($server->builder_cpu_quota)->toBe('350%') - ->and($server->capabilities)->toBe(['coold', 'builder']) + ->and($server->capabilities)->toBe(['coold']) ->and($server->host)->toBe('203.0.113.10') ->and($server->ssh_user)->toBe('root') ->and($server->ssh_port)->toBe(22) @@ -4640,7 +4640,6 @@ it('syncs dev Lima VMs into v5 clusters and servers', function () { '--team-id' => $team->id, '--user-id' => $user->id, '--cluster' => 'Development-Lima', - '--builder-capacity' => 2, '--server' => [ 'coold-dev|host.docker.internal|developer|61332|100.64.0.1', 'coold-dev-2|host.docker.internal|developer|61379|100.64.0.2', @@ -4683,7 +4682,6 @@ it('updates legacy dev Lima hostnames to Docker reachable SSH endpoints', functi '--team-id' => $team->id, '--user-id' => $user->id, '--cluster' => 'Development-Lima', - '--builder-capacity' => 2, '--server' => [ 'coold-dev|host.docker.internal|developer|61332', ], @@ -4699,7 +4697,6 @@ it('seeds dev Lima VMs into v5 clusters and servers idempotently', function () { createSharedUserAndTeamTables(); [$user, $team] = createV5UserWithTeam(); createV5PrivateKey($team, 'Dev Lima Key'); - config()->set('coold.dev_builder_capacity', 2); (new V5DevLimaSeeder)->run(); (new V5DevLimaSeeder)->run(); @@ -4715,7 +4712,7 @@ it('seeds dev Lima VMs into v5 clusters and servers idempotently', function () { ->and(V5Server::query()->where('name', 'coold-dev')->where('node_address', '100.64.0.1')->where('wireguard_management_ip', '100.64.0.1')->exists())->toBeTrue() ->and(V5Server::query()->where('name', 'coold-dev-2')->where('node_address', '100.64.0.2')->where('wireguard_management_ip', '100.64.0.2')->exists())->toBeTrue() ->and(V5Server::query()->where('status', 'installed')->count())->toBe(2) - ->and(V5Server::query()->where('builder_enabled', true)->where('builder_capacity', 2)->count())->toBe(2) + ->and(V5Server::query()->where('builder_enabled', false)->where('builder_capacity', 0)->count())->toBe(2) ->and(V5Server::query()->where('cluster_id', $cluster->id)->count())->toBe(2); }); diff --git a/tests/Unit/V5/CaddyIngressConfigurationTest.php b/tests/Unit/V5/CaddyIngressConfigurationTest.php index 581454556..bdf153d57 100644 --- a/tests/Unit/V5/CaddyIngressConfigurationTest.php +++ b/tests/Unit/V5/CaddyIngressConfigurationTest.php @@ -240,3 +240,87 @@ function withFakeFluxSocket(string $response, Closure $callback): void @unlink($socketPath); } } + +it('dispatches container inventory through the containers list primitive', function () { + if (! function_exists('pcntl_fork')) { + $this->markTestSkipped('pcntl is required to fake a Flux Unix socket.'); + } + + $body = json_encode([ + 'request_id' => 'test-request', + 'status' => 'ok', + 'data' => [], + ], JSON_THROW_ON_ERROR); + $requestPath = storage_path('framework/testing/flux-request-'.bin2hex(random_bytes(8)).'.txt'); + + withFakeFluxSocketCapturingRequest( + "HTTP/1.1 200 OK\r\n". + "Content-Type: application/json\r\n". + 'Content-Length: '.strlen($body)."\r\n". + "\r\n". + $body, + $requestPath, + fn () => (new FluxClient)->listContainers('100.64.0.10') + ); + + $request = file_get_contents($requestPath) ?: ''; + @unlink($requestPath); + + expect($request)->toContain('"type":"containers.list"') + ->not->toContain('list_containers'); +}); + +function withFakeFluxSocketCapturingRequest(string $response, string $requestPath, Closure $callback): void +{ + $directory = storage_path('framework/testing'); + + if (! is_dir($directory)) { + mkdir($directory, 0777, true); + } + + $socketPath = $directory.'/flux-'.bin2hex(random_bytes(8)).'.sock'; + $server = stream_socket_server("unix://{$socketPath}", $errorCode, $errorMessage); + + expect($server)->not->toBeFalse("Could not create fake Flux socket: {$errorMessage} ({$errorCode})"); + + $pid = pcntl_fork(); + + if ($pid === 0) { + $connection = stream_socket_accept($server, 5); + + if ($connection !== false) { + $request = ''; + + while (! str_contains($request, "\r\n\r\n") && ! feof($connection)) { + $request .= fread($connection, 8192); + } + + if (preg_match('/Content-Length: (\d+)/i', $request, $matches) === 1) { + $remaining = (int) $matches[1] - strlen(substr($request, strpos($request, "\r\n\r\n") + 4)); + + while ($remaining > 0 && ! feof($connection)) { + $chunk = fread($connection, $remaining); + $request .= $chunk; + $remaining -= strlen($chunk); + } + } + + file_put_contents($requestPath, $request); + fwrite($connection, $response); + fclose($connection); + } + + fclose($server); + exit(0); + } + + fclose($server); + + try { + config(['flux.unix_socket_path' => $socketPath]); + $callback(); + pcntl_waitpid($pid, $status); + } finally { + @unlink($socketPath); + } +}