diff --git a/.gitignore b/.gitignore index 480066d6a..792578dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,6 @@ tests/v4/Browser/Screenshots # Local generated Lima configs .dev/bin/ -.dev/lima/*.generated.yaml .dev/lima/ssh.config +.dev/lima/ssh_key +.dev/lima/hosts diff --git a/config/coold.php b/config/coold.php index 2468eac40..b491077b5 100644 --- a/config/coold.php +++ b/config/coold.php @@ -6,5 +6,5 @@ return [ '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', get_current_user()), + 'dev_ssh_user' => env('COOLIFY_CLI_SSH_USER', 'coolify'), ]; diff --git a/database/seeders/V5DevLimaSeeder.php b/database/seeders/V5DevLimaSeeder.php index 92bcf573f..3e5a16b4f 100644 --- a/database/seeders/V5DevLimaSeeder.php +++ b/database/seeders/V5DevLimaSeeder.php @@ -31,7 +31,7 @@ class V5DevLimaSeeder extends Seeder ->first(); $builderCapacity = max(0, (int) config('coold.dev_builder_capacity', 2)); - $sshUser = (string) config('coold.dev_ssh_user', get_current_user()); + $sshUser = (string) config('coold.dev_ssh_user', 'coolify'); $servers = collect($this->servers()) ->map(fn (array $server): array => [ ...$server, @@ -57,19 +57,19 @@ class V5DevLimaSeeder extends Seeder return [ [ 'name' => 'coold-dev', - 'host' => 'host.docker.internal', - 'ssh_port' => 60001, + 'host' => 'coold-dev.local', + 'ssh_port' => 22, 'wireguard_management_ip' => '100.64.0.1', 'wireguard_listen_port_override' => 51821, - 'wireguard_endpoint_override' => 'host.lima.internal:51821', + 'wireguard_endpoint_override' => 'coold-dev.local:51821', ], [ 'name' => 'coold-dev-2', - 'host' => 'host.docker.internal', - 'ssh_port' => 60002, + 'host' => 'coold-dev-2.local', + 'ssh_port' => 22, 'wireguard_management_ip' => '100.64.0.2', 'wireguard_listen_port_override' => 51822, - 'wireguard_endpoint_override' => 'host.lima.internal:51822', + 'wireguard_endpoint_override' => 'coold-dev-2.local:51822', ], ]; } diff --git a/dev/coold-dev.md b/dev/coold-dev.md index 8052059e2..751f1569c 100644 --- a/dev/coold-dev.md +++ b/dev/coold-dev.md @@ -49,20 +49,24 @@ scripts/dev.sh coolify bootstrap-command scripts/dev.sh coolify run ``` -On macOS, the helper builds `coolify` from the local coold repo because the -nightly release currently publishes Linux binaries. On Linux, it downloads the -nightly release artifact. +The helper runs the `coolify` CLI from the Coolify development container. Lima +VMs are addressed by their bridged mDNS names (`.local`), so the same +addresses are used by bootstrap and by the v5 dev server seeder. -The generated bootstrap command uses Lima's forwarded SSH ports and dev -WireGuard endpoint overrides, for example: +Because Docker Desktop does not reliably pass mDNS multicast into containers, +`scripts/dev.sh up` resolves each `.local` name on the host and writes the +resolved records into the Coolify container `/etc/hosts` before bootstrapping. + +The generated bootstrap command uses the container CLI, the repo-local copy of +the Lima SSH key, and dev WireGuard endpoint overrides, for example: ```bash -.dev/bin/coolify init bootstrap \ - --nodes "127.0.0.1:,127.0.0.1:" \ - --ssh-key "$HOME/.lima/_config/user" \ - --ssh-user "$USER" \ - --wg-listen-port-overrides "127.0.0.1:=51821,127.0.0.1:=51822" \ - --wg-endpoint-overrides "127.0.0.1:=host.lima.internal:51821,127.0.0.1:=host.lima.internal:51822" \ +spin exec -T coolify /usr/local/bin/coolify init bootstrap \ + --nodes "coold-dev.local,coold-dev-2.local" \ + --ssh-key "/var/www/html/.dev/lima/ssh_key" \ + --ssh-user "coolify" \ + --wg-listen-port-overrides "coold-dev.local=51821,coold-dev-2.local=51822" \ + --wg-endpoint-overrides "coold-dev.local=coold-dev.local:51821,coold-dev-2.local=coold-dev-2.local:51822" \ --coold-version "nightly" \ --corrosion-version "v1.0.0" \ --yes @@ -78,8 +82,8 @@ After `coolify init bootstrap`, defaults are: | VM | WireGuard IP | WireGuard endpoint | Podman subnet | Gateway | | --- | --- | --- | --- | --- | -| `coold-dev` | `100.64.0.1` | `host.lima.internal:51821` | `10.210.0.0/24` | `10.210.0.1` | -| `coold-dev-2` | `100.64.0.2` | `host.lima.internal:51822` | `10.210.1.0/24` | `10.210.1.1` | +| `coold-dev` | `100.64.0.1` | `coold-dev.local:51821` | `10.210.0.0/24` | `10.210.0.1` | +| `coold-dev-2` | `100.64.0.2` | `coold-dev-2.local:51822` | `10.210.1.0/24` | `10.210.1.1` | ## Checking state diff --git a/dev/lima/coold.yaml b/dev/lima/coold.yaml index 327d1589c..d07b078f3 100644 --- a/dev/lima/coold.yaml +++ b/dev/lima/coold.yaml @@ -1,79 +1,53 @@ -# Lima VM for testing Coolify v5 against coolify-provisioned hosts. -# Start with: scripts/coold-vm.sh up -# -# This file is copied into .dev/lima/coold.generated.yaml by the wrapper script, -# with {{COOLIFY_REPO}}, {{COOLIFY_COOLD_VERSION}}, and {{COOLIFY_CORROSION_VERSION}} replaced before Lima sees it. - vmType: "vz" arch: "default" +user: + name: coolify cpus: 4 -memory: "8GiB" -disk: "100GiB" +memory: "2GiB" +disk: "20GiB" containerd: system: false user: false -ssh: - localPort: {{COOLIFY_COOLD_VM_SSH_PORT}} - images: - - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" + - location: "https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img" arch: "x86_64" - - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" + - location: "https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-arm64.img" arch: "aarch64" -mountType: "virtiofs" -mounts: - - location: "{{COOLIFY_REPO}}" - mountPoint: "/workspace/coolify" - writable: true -portForwards: - - guestPort: 5173 - hostPort: 5173 - hostIP: "127.0.0.1" - - guestPort: 3000 - hostPort: 3000 - hostIP: "127.0.0.1" +networks: + - lima: bridged + provision: - mode: system script: | - #!/usr/bin/env bash - set -euxo pipefail - export DEBIAN_FRONTEND=noninteractive - echo "[coold-vm] Updating Ubuntu package index..." - apt-get update - echo "[coold-vm] Installing minimal VM dependencies..." - apt-get install -y --no-install-recommends \ - bash \ - ca-certificates \ - curl \ - iproute2 \ - jq \ - openssl \ - sqlite3 \ - sudo - cat >/usr/local/bin/rtk <<'SH' - #!/usr/bin/env bash - exec "$@" - SH - chmod +x /usr/local/bin/rtk - mkdir -p /etc/coolify /var/lib/coolify-dev - chmod 755 /etc/coolify - coolify_test_public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 sail@76ff66d2e2dd" - install -d -m 700 /root/.ssh - touch /root/.ssh/authorized_keys - grep -qxF "$coolify_test_public_key" /root/.ssh/authorized_keys || echo "$coolify_test_public_key" >>/root/.ssh/authorized_keys - chmod 600 /root/.ssh/authorized_keys - target_home="$(getent passwd 501 | cut -d: -f6 || true)" - if [ -n "$target_home" ] && [ -d "$target_home" ]; then - owner="$(stat -c '%u:%g' "$target_home")" - install -d -m 700 -o "${owner%:*}" -g "${owner#*:}" "$target_home/.ssh" - touch "$target_home/.ssh/authorized_keys" - grep -qxF "$coolify_test_public_key" "$target_home/.ssh/authorized_keys" || echo "$coolify_test_public_key" >>"$target_home/.ssh/authorized_keys" - chown "$owner" "$target_home/.ssh/authorized_keys" - chmod 600 "$target_home/.ssh/authorized_keys" - fi - echo "[coold-vm] Minimal provisioning complete. coolify bootstrap will install WireGuard, Podman, Corrosion, coold, and builder." + + echo "[coold-vm] Installing and configuring mDNS." + apt-get update + apt-get install -y avahi-daemon + hostnamectl set-hostname "{{.Name}}" + systemctl enable --now avahi-daemon + echo "[coold-vm] Installing SSH keys." + + install_public_key() { + user="$1" + home="$(getent passwd "$user" | cut -d: -f6 || true)" + if [ -z "$home" ] || [ ! -d "$home" ]; then + return + fi + + owner="$(stat -c '%u:%g' "$home")" + authorized_keys="$home/.ssh/authorized_keys" + install -d -m 700 -o "${owner%:*}" -g "${owner#*:}" "$home/.ssh" + touch "$authorized_keys" + chown "$owner" "$authorized_keys" + chmod 600 "$authorized_keys" + grep -qxF "$coolify_test_public_key" "$authorized_keys" || echo "$coolify_test_public_key" >>"$authorized_keys" + chown "$owner" "$authorized_keys" + } + + install_public_key root + install_public_key coolify diff --git a/docs/v5/architecture/README.md b/docs/v5/architecture/README.md new file mode 100644 index 000000000..8d7daa9e6 --- /dev/null +++ b/docs/v5/architecture/README.md @@ -0,0 +1,34 @@ +# Coolify v5 Architecture + +This directory is the canonical architecture reference for Coolify v5 from the +Laravel control-plane point of view. + +v5 has three major building blocks: + +```text +User / API / Git webhook + ↓ +Coolify Laravel control plane + ↓ HTTP over /run/coolify/flux.sock +Flux + ↓ outbound gRPC Agent.Stream +coold on each host + ↓ +Podman / networks / firewall / DNS / Corrosion / builder +``` + +## Documents + +| Document | Purpose | +| --- | --- | +| [Overview](overview.md) | High-level mental model and data flow. | +| [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`. | +| [ADR 0001](adr/0001-control-plane-flux-coold-split.md) | Decision record for the v5 split. | + +## Core rule + +Coolify owns user intent and application state. Flux routes requests. coold +executes explicit host primitives. coold must not grow app, team, RBAC, +deployment, billing, or audit concepts. 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 new file mode 100644 index 000000000..57c6f3d36 --- /dev/null +++ b/docs/v5/architecture/adr/0001-control-plane-flux-coold-split.md @@ -0,0 +1,75 @@ +# ADR 0001: Split v5 into Coolify control plane, Flux broker, and coold host agent + +## Status + +Accepted. + +## Context + +Coolify v5 needs to manage many user resources across many user-owned hosts, +including hosts behind NAT or restrictive firewalls. The Laravel application +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. + +## Decision + +Coolify v5 uses three distinct building blocks: + +1. **Coolify Laravel control plane** owns user intent, durable product state, + RBAC, deployment state machines, placement decisions, secrets, proxy config + rendering, notifications, and audit. +2. **Flux** owns long-lived agent connectivity. Laravel talks to Flux over a + Unix socket. coold agents dial Flux over outbound gRPC. Flux routes typed + 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. + +coold must not expose raw Podman passthrough. Every supported operation must be +an explicit primitive with validation and a stable protocol shape. + +## Consequences + +### Positive + +- Coolify keeps product complexity in one durable control plane. +- Hosts can live behind NAT because coold dials out to Flux. +- Privileged host access is isolated to coold. +- Flux can scale connection handling separately from Laravel request workers. +- The primitive surface can be tested independently from Coolify's app model. + +### Negative + +- More moving parts than a direct SSH/Docker model. +- Protocol changes require coordination across Coolify, Flux, and coold. +- Some deploy features require new primitives before the v5 path is complete. + +## Boundary rules + +- Coolify may say: "deploy this nginx resource for this team to host H1." coold + may not. +- Coolify may send: `images.pull`, `containers.create`, `containers.start`, + `services.register`, `firewall.allow`. coold may execute those. +- Flux may route requests but may not make product decisions. +- coold may reject dangerous host operations even when Coolify requested them. + +## First reference flow + +The first validation flow is deploying a Docker image app using `nginx:alpine`. +That flow requires no builder and exercises the minimum runtime path: + +```text +Coolify state machine + → Flux dispatch + → coold image pull + → coold container create/start + → coold status + → coold service registration + → coold firewall + → Coolify proxy config/reload + → Coolify marks deployment running +``` diff --git a/docs/v5/architecture/deploy-flows.md b/docs/v5/architecture/deploy-flows.md new file mode 100644 index 000000000..61bab3935 --- /dev/null +++ b/docs/v5/architecture/deploy-flows.md @@ -0,0 +1,76 @@ +# 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 new file mode 100644 index 000000000..f7298773b --- /dev/null +++ b/docs/v5/architecture/overview.md @@ -0,0 +1,38 @@ +# 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 new file mode 100644 index 000000000..3f768afa2 --- /dev/null +++ b/docs/v5/architecture/primitives.md @@ -0,0 +1,106 @@ +# v5 Host Primitives + +Coolify deploy logic must send explicit primitives to coold through Flux. There +must be no raw Podman passthrough. + +This file is the target primitive catalog. Implementations may lag behind this +catalog; code should only call primitives that exist in the current protocol. + +## Images + +| Primitive | Purpose | +| --- | --- | +| `images.pull` | Pull an OCI image ref onto a host. | +| `images.list` | List local images. | +| `images.delete` | Remove a local image when safe. | + +## Containers + +| Primitive | Purpose | +| --- | --- | +| `containers.create` | Create a container from an explicit spec. | +| `containers.start` | Start an existing container. | +| `containers.stop` | Stop a container with an optional timeout. | +| `containers.restart` | Restart a container. | +| `containers.delete` | Delete a stopped or forced container. | +| `containers.inspect` | Return detailed runtime state. | +| `containers.list` | Return host container summaries. | +| `containers.logs` | Stream or read container logs. | +| `containers.exec` | Run a command inside a container. | +| `containers.healthcheck.run` | Trigger or read a runtime health check. | + +`containers.create` must enforce a deny filter for dangerous host options such +as privileged mode, unsafe host mounts, host networking, and disallowed +capabilities unless the host is explicitly configured to allow them. + +## Volumes + +| Primitive | Purpose | +| --- | --- | +| `volumes.create` | Create an idempotent named volume. | +| `volumes.inspect` | Inspect a host volume. | +| `volumes.delete` | Delete an unused volume. | + +## Networks + +| Primitive | Purpose | +| --- | --- | +| `networks.create` | Create an idempotent Podman network. | +| `networks.list` | List host networks. | +| `networks.delete` | Delete an unused network. | + +Bootstrap-created mesh namespace networks are managed by the v5 cluster init +flow. Per-resource or compose networks are runtime primitives. + +## Firewall + +| Primitive | Purpose | +| --- | --- | +| `firewall.allow` | Add an allow tuple and persist it. | +| `firewall.revoke` | Remove an allow tuple by ID. | +| `firewall.list` | List active/persisted allow rules. | +| `firewall.reconcile` | Flush and restore firewall state from snapshots. | + +coold is the sole writer for both firewall planes: iptables for cross-host +traffic and nft bridge rules for same-bridge traffic. + +## Service discovery and DNS + +| Primitive | Purpose | +| --- | --- | +| `services.register` | Register this host's container endpoint in Corrosion. | +| `services.unregister` | Remove an endpoint from Corrosion. | +| `services.endpoints` | Read known endpoints for diagnostics. | +| `dns.lookup` | Diagnose internal DNS resolution. | +| `dns.stats` | Return DNS server status. | + +## Host facts + +| Primitive | Purpose | +| --- | --- | +| `host.info` | Return host/runtime facts for scheduling and debugging. | +| `host.stats` | Return CPU, memory, disk, and container stats snapshot. | +| `host.containers` | Return host container summaries. | + +## Builder + +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. | + +## Not primitives + +These belong in Coolify, not coold: + +- `deploy.application` +- `rollback.deployment` +- `create.preview` +- `configure.domain` +- `authorize.user` +- `send.notification` +- `render.proxy.config` diff --git a/docs/v5/architecture/responsibility-split.md b/docs/v5/architecture/responsibility-split.md new file mode 100644 index 000000000..fd813c912 --- /dev/null +++ b/docs/v5/architecture/responsibility-split.md @@ -0,0 +1,60 @@ +# v5 Responsibility Split + +Use this as the first check when deciding where a new feature belongs. + +## Rule of thumb + +| Question | Owner | +| --- | --- | +| What should happen for this user, team, app, database, or deployment? | Coolify | +| Which connected host should receive this request? | Coolify chooses, Flux routes | +| How do we get a command to a NATed host? | Flux | +| Do this concrete operation on this host. | coold | +| Is this allowed for this Coolify user/team? | Coolify | +| Is this host operation dangerous even if Coolify asked for it? | coold deny filter | + +## Coolify owns + +- Users, teams, roles, RBAC, API tokens, sessions, SSO/OAuth. +- Projects, environments, applications, services, databases, servers. +- Resource configuration: source, image, build settings, env vars, domains, + ports, health checks, resource limits, volumes, schedules, webhooks. +- Deployment state machines: pending, building, pulling, creating, starting, + health waiting, cutover, running, failed, rollback, cleanup. +- Placement and scheduling decisions. +- Proxy/ingress configuration rendering and TLS intent. +- Secret storage, encryption, resolution, and injection at deploy time. +- Business audit, event history, deployment logs, and user-facing status. +- Notifications, billing/subscriptions, cloud-provider integration. + +## Flux owns + +- Long-lived coold stream registry keyed by host ID. +- Pending request registry keyed by request ID. +- Request routing from Laravel's Unix-socket lane to the selected coold stream. +- Timeouts, disconnected-host responses, pending-cap protection, and late result + handling. +- Host-agent authentication for inbound coold streams. + +Flux does not inspect product meaning. `containers.start` is just a frame to a +host; Flux does not know it is part of an nginx deployment. + +## coold owns + +- Local Podman access. +- Host runtime primitives: images, containers, volumes, networks, logs, exec, + health checks, host facts. +- Firewall mutation and reconciliation as the sole kernel firewall writer. +- Embedded DNS and service-discovery sync. +- Writing this host's service endpoint rows to Corrosion. +- Builder subprocess supervision when the host advertises builder capability. +- Host-level safety checks such as container-create deny filters. + +coold does not store Coolify secrets, users, teams, app ownership, deployment +history, billing data, or business audit. + +## Boundary test + +If a different orchestrator could reuse the same operation with its own app +model, it probably belongs in coold. If the operation only makes sense because +of Coolify's product model, it belongs in Coolify. diff --git a/scripts/coold-vm.sh b/scripts/coold-vm.sh index 0c489ae68..627419535 100755 --- a/scripts/coold-vm.sh +++ b/scripts/coold-vm.sh @@ -30,7 +30,6 @@ 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)" -SSH_PORT="$(read_coolify_env COOLIFY_COOLD_VM_SSH_PORT 60002)" WG_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_IP "")" WG_PEER_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_IP "")" WG_PEER_ENDPOINT="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_ENDPOINT "")" @@ -42,7 +41,6 @@ if [ "$BUILDER_CAPACITY" = "0" ]; then BUILDER_ENABLED="false" fi TEMPLATE="$ROOT/dev/lima/coold.yaml" -GENERATED="$ROOT/.dev/lima/${INSTANCE}.generated.yaml" GUEST_COOLIFY_ROOT="/workspace/coolify" usage() { @@ -72,7 +70,6 @@ Environment: 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_SSH_PORT Host SSH port forwarded to this VM (default: 60002) COOLIFY_COOLD_VM_WG_IP Optional WireGuard mgmt IP for this host COOLIFY_COOLD_VM_CONTAINER_SUBNET Podman mesh subnet for this host COOLIFY_COOLD_VM_CONTAINER_GATEWAY Podman mesh gateway for this host @@ -341,8 +338,6 @@ COOLIFY_COOLD_PODMAN_SOCKET="${COOLIFY_COOLD_PODMAN_SOCKET:-/run/podman/podman.s COOLIFY_COOLD_CORROSION_URL="${COOLIFY_COOLD_CORROSION_URL:-http://127.0.0.1:8080}" \ COOLIFY_COOLD_NAMESPACES="${COOLIFY_COOLD_NAMESPACES:-default:coolify-default-mesh:$CONTAINER_GATEWAY}" \ COOLIFY_COOLD_DNS_ZONE="${COOLIFY_COOLD_DNS_ZONE:-coolify.internal}" \ -COOLIFY_COOLD_API_BIND="${COOLIFY_COOLD_API_BIND:-${WG_IP:-127.0.0.1}:8443}" \ -COOLIFY_COOLD_API_TOKEN_FILE="${COOLIFY_COOLD_API_TOKEN_FILE:-/etc/coolify/api-token}" \ 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}" \ @@ -417,7 +412,6 @@ start_agent() { configure_system_resolved ensure_mesh_dns_anchor install_mesh_firewall - lima_shell sudo sh -c 'if [ ! -s /etc/coolify/api-token ]; then openssl rand -hex 32 > /etc/coolify/api-token.tmp && chmod 600 /etc/coolify/api-token.tmp && mv /etc/coolify/api-token.tmp /etc/coolify/api-token; fi' lima_shell sudo tee /etc/systemd/system/corrosion.service >/dev/null <<'UNIT' [Unit] @@ -446,8 +440,6 @@ Environment=COOLIFY_COOLD_PODMAN_SOCKET=/run/podman/podman.sock Environment=COOLIFY_COOLD_CORROSION_URL=http://127.0.0.1:8080 Environment=COOLIFY_COOLD_NAMESPACES=default:coolify-default-mesh:$CONTAINER_GATEWAY Environment=COOLIFY_COOLD_DNS_ZONE=coolify.internal -Environment=COOLIFY_COOLD_API_BIND=${WG_IP:-127.0.0.1}:8443 -Environment=COOLIFY_COOLD_API_TOKEN_FILE=/etc/coolify/api-token Environment=COOLIFY_COOLD_FLUX_URL=$FLUX_URL Environment=COOLIFY_COOLD_HOST_JWT_PATH=/etc/coolify/host-jwt Environment=COOLIFY_COOLD_BUILDER_ENABLED=$BUILDER_ENABLED @@ -468,19 +460,7 @@ UNIT lima_shell sudo systemctl restart corrosion.service coold.service } -generate_yaml() { - mkdir -p "$(dirname "$GENERATED")" - sed \ - -e "s#{{COOLIFY_REPO}}#$ROOT#g" \ - -e "s#{{COOLIFY_COOLD_VERSION}}#$VERSION#g" \ - -e "s#{{COOLIFY_CORROSION_VERSION}}#$CORROSION_VERSION#g" \ - -e "s#{{COOLIFY_COOLD_VM_SSH_PORT}}#$SSH_PORT#g" \ - "$TEMPLATE" > "$GENERATED" -} - start_vm() { - generate_yaml - if instance_running; then return fi @@ -491,7 +471,7 @@ start_vm() { if instance_exists; then limactl start --tty=false "$INSTANCE" else - limactl start --tty=false --name="$INSTANCE" "$GENERATED" + limactl start --tty=false --name="$INSTANCE" "$TEMPLATE" fi } @@ -604,14 +584,27 @@ wait_for_guest_provisioning() { | awk '{ print "[guest] " $0; fflush(); }' || true } +ensure_mdns_hostname() { + local current_hostname + current_hostname="$(lima_shell hostname 2>/dev/null || true)" + + if [ "$current_hostname" != "$INSTANCE" ]; then + echo "==> Setting guest hostname to ${INSTANCE} for ${INSTANCE}.local mDNS..." + lima_shell sudo hostnamectl set-hostname "$INSTANCE" + fi + + lima_shell sudo systemctl restart avahi-daemon.service +} + up_with_logs() { echo "==> Coolify coold VM: $INSTANCE" echo "==> coold package tag: $VERSION" echo "==> corrosion package tag: $CORROSION_VERSION" - echo "==> Lima config: $GENERATED" + echo "==> Lima config: $TEMPLATE" wait_for_lima_start wait_for_guest_provisioning + ensure_mdns_hostname echo "==> VM is ready. Run coolify bootstrap via: scripts/dev.sh up" } diff --git a/scripts/dev.sh b/scripts/dev.sh index 3cf03c1e4..309145b9c 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -60,9 +60,71 @@ coold_vm_wg_port() { read_coolify_env "COOLIFY_COOLD_VM_WG_PORT_${index}" "$((51820 + index))" } -coold_vm_ssh_port() { +coold_vm_dns_name() { local index="$1" - read_coolify_env "COOLIFY_COOLD_VM_SSH_PORT_${index}" "6000${index}" + printf '%s.local\n' "$(coold_vm_instance "$index")" +} + +resolve_lima_dns_name() { + local name="$1" + local ip="" + + if command -v dscacheutil >/dev/null 2>&1; then + ip="$(dscacheutil -q host -a name "$name" 2>/dev/null | awk '/ip_address:/ && $2 ~ /^[0-9.]+$/ { print $2; exit }')" + fi + + if [ -z "$ip" ] && command -v getent >/dev/null 2>&1; then + ip="$(getent ahostsv4 "$name" 2>/dev/null | awk '{ print $1; exit }')" + fi + + if [ -z "$ip" ]; then + ip="$(ping -c 1 -W 1 "$name" 2>/dev/null | sed -n 's/^PING .* (\([0-9.]*\)).*$/\1/p' | head -n 1)" + fi + + if [ -z "$ip" ]; then + echo "ERROR: Could not resolve ${name} from the host." >&2 + echo "Check that the Lima VM is running, bridged networking is enabled, and Avahi/mDNS finished provisioning." >&2 + return 1 + fi + + printf '%s\n' "$ip" +} + +sync_lima_hosts_into_coolify_container() { + local count + local hosts_file="$ROOT/.dev/lima/hosts" + local name + local ip + + count="$(coold_vm_count)" + mkdir -p "$(dirname "$hosts_file")" + : > "$hosts_file" + + for index in $(seq 1 "$count"); do + name="$(coold_vm_dns_name "$index")" + ip="$(resolve_lima_dns_name "$name")" || return 1 + printf '%s %s\n' "$ip" "$name" >> "$hosts_file" + done + + echo "==> Syncing Lima .local host records into the Coolify container..." + spin exec -T -u root coolify sh -lc ' +set -e +records=/tmp/coolify-lima-hosts +next=/tmp/coolify-hosts-next +cat > "$records" +cp /etc/hosts "$next" +while read -r ip name; do + if [ -z "$ip" ] || [ -z "$name" ]; then + continue + fi + + awk -v name="$name" '\''$0 !~ ("(^|[[:space:]])" name "([[:space:]]|$)") { print }'\'' "$next" > "${next}.filtered" + printf "%s %s\n" "$ip" "$name" >> "${next}.filtered" + mv "${next}.filtered" "$next" +done < "$records" +cat "$next" > /etc/hosts +rm -f "$records" "$next" "${next}.filtered" +' < "$hosts_file" } coold_vm_container_subnet() { @@ -75,124 +137,13 @@ coold_vm_container_gateway() { read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_GATEWAY_${index}" "10.210.$((index - 1)).1" } -host_arch() { - case "$(uname -m)" in - x86_64|amd64) - printf '%s\n' amd64 - ;; - arm64|aarch64) - printf '%s\n' arm64 - ;; - *) - echo "ERROR: unsupported host architecture: $(uname -m)" >&2 - exit 1 - ;; - esac -} - -host_os() { - case "$(uname -s)" in - Darwin) - printf '%s\n' darwin - ;; - Linux) - printf '%s\n' linux - ;; - *) - echo "ERROR: unsupported host OS: $(uname -s)" >&2 - exit 1 - ;; - esac -} - coolify_cli_bin() { - printf '%s\n' "$ROOT/.dev/bin/coolify" -} - -ensure_coolify() { - local bin - local version - local arch - local os - local url - local tmpdir - - bin="$(coolify_cli_bin)" - version="$(read_coolify_env COOLIFY_CLI_VERSION "$(read_coolify_env COOLIFY_COOLD_VERSION nightly)")" - arch="$(host_arch)" - os="$(host_os)" - - url="https://github.com/coollabsio/coold/releases/download/${version}/coolify-${os}-${arch}.tar.gz" - echo "==> Installing coolify from ${url}" - - mkdir -p "$(dirname "$bin")" - tmpdir="$(mktemp -d)" - if ! curl -fsSL --retry 3 --max-time 120 -o "$tmpdir/coolify.tar.gz" "$url"; then - rm -rf "$tmpdir" - return 1 - fi - tar -xzf "$tmpdir/coolify.tar.gz" -C "$tmpdir" - install -m 0755 "$tmpdir/coolify" "$bin" - rm -rf "$tmpdir" + printf '%s\n' '/usr/local/bin/coolify' } lima_ssh_target() { local index="$1" - local instance - - instance="$(coold_vm_instance "$index")" - - if [ ! -f "$HOME/.lima/${instance}/ssh.config" ]; then - echo "ERROR: Lima SSH config for ${instance} was not found. Start it first with scripts/dev.sh up." >&2 - exit 1 - fi - - printf 'lima-%s\n' "$instance" -} - -lima_ssh_config() { - local count - local config="$ROOT/.dev/lima/ssh.config" - local instance - count="$(coold_vm_count)" - - mkdir -p "$(dirname "$config")" - : > "$config" - - for index in $(seq 1 "$count"); do - instance="$(coold_vm_instance "$index")" - if [ ! -f "$HOME/.lima/${instance}/ssh.config" ]; then - echo "ERROR: Lima SSH config for ${instance} was not found. Start it first with scripts/dev.sh up." >&2 - exit 1 - fi - - cat "$HOME/.lima/${instance}/ssh.config" >> "$config" - printf '\n' >> "$config" - done - - printf '%s\n' "$config" -} - -lima_ssh_port() { - local index="$1" - local instance - local port - - instance="$(coold_vm_instance "$index")" - - if [ ! -f "$HOME/.lima/${instance}/ssh.config" ]; then - echo "ERROR: Lima SSH config for ${instance} was not found. Start it first with scripts/dev.sh up." >&2 - exit 1 - fi - - port="$(awk 'tolower($1) == "port" { print $2; exit }' "$HOME/.lima/${instance}/ssh.config")" - - if [ -z "$port" ]; then - echo "ERROR: Lima SSH port for ${instance} was not found in $HOME/.lima/${instance}/ssh.config." >&2 - exit 1 - fi - - printf '%s\n' "$port" + coold_vm_dns_name "$index" } coolify_nodes_arg() { @@ -240,18 +191,37 @@ coolify_wg_endpoint_overrides_arg() { if [ -n "$overrides" ]; then overrides="${overrides}," fi - overrides="${overrides}${node}=host.lima.internal:$(coold_vm_wg_port "$index")" + overrides="${overrides}${node}=$(coold_vm_dns_name "$index"):$(coold_vm_wg_port "$index")" done printf '%s\n' "$overrides" } coolify_ssh_key() { - read_coolify_env COOLIFY_CLI_SSH_KEY "$HOME/.lima/_config/user" + read_coolify_env COOLIFY_CLI_SSH_KEY "/var/www/html/.dev/lima/ssh_key" +} + +coolify_host_ssh_key_source() { + read_coolify_env COOLIFY_CLI_HOST_SSH_KEY "$HOME/.lima/_config/user" +} + +ensure_coolify_container_ssh_key() { + local source_key + local target_key="$ROOT/.dev/lima/ssh_key" + + source_key="$(coolify_host_ssh_key_source)" + if [ ! -f "$source_key" ]; then + echo "ERROR: SSH key for dev Lima VMs was not found at ${source_key}." >&2 + echo "Set COOLIFY_CLI_HOST_SSH_KEY to the host-side private key that Lima authorized." >&2 + exit 1 + fi + + mkdir -p "$(dirname "$target_key")" + install -m 0600 "$source_key" "$target_key" } coolify_ssh_user() { - read_coolify_env COOLIFY_CLI_SSH_USER "$USER" + read_coolify_env COOLIFY_CLI_SSH_USER coolify } coolify_bootstrap_concurrency() { @@ -264,20 +234,17 @@ coolify_bootstrap_ssh_timeout() { coolify_bootstrap_command() { local nodes - local ssh_config local listen_overrides local endpoint_overrides - ensure_coolify nodes="$(coolify_nodes_arg)" || return 1 - ssh_config="$(lima_ssh_config)" || return 1 listen_overrides="$(coolify_wg_listen_overrides_arg)" || return 1 endpoint_overrides="$(coolify_wg_endpoint_overrides_arg)" || return 1 cat < coolify CLI is provided by the Coolify dev container." + spin exec -T coolify "$(coolify_cli_bin)" --version ;; path) - ensure_coolify coolify_cli_bin ;; bootstrap-command) coolify_bootstrap_command ;; run) - ensure_coolify - exec "$(coolify_cli_bin)" "$@" + exec spin exec -T coolify "$(coolify_cli_bin)" "$@" ;; -h|--help|help) cat <<'USAGE' Usage: scripts/dev.sh coolify Commands: - install Download/install the nightly coolify dev binary - path Print the local coolify path + install Print the coolify CLI version from the dev container + path Print the coolify CLI path inside the dev container bootstrap-command Print the dev Lima bootstrap command without running it run Run coolify with arbitrary args @@ -414,7 +377,6 @@ coold_vm() { shift COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance "$index")" \ COOLIFY_COOLD_VM_WG_IP="$(coold_vm_wg_ip "$index")" \ - COOLIFY_COOLD_VM_SSH_PORT="$(coold_vm_ssh_port "$index")" \ COOLIFY_COOLD_VM_CONTAINER_SUBNET="$(coold_vm_container_subnet "$index")" \ COOLIFY_COOLD_VM_CONTAINER_GATEWAY="$(coold_vm_container_gateway "$index")" \ scripts/coold-vm.sh "$@" @@ -524,8 +486,7 @@ sync_v5_dev_lima_servers() { for index in $(seq 1 "$count"); do instance="$(coold_vm_instance "$index")" - ssh_port="$(lima_ssh_port "$index")" - server_args+=(--server="${instance}|host.docker.internal|${ssh_user}|${ssh_port}|$(coold_vm_wg_ip "$index")") + server_args+=(--server="${instance}|$(coold_vm_dns_name "$index")|${ssh_user}|22|$(coold_vm_wg_ip "$index")") done echo "==> Running pending migrations before syncing v5 dev Lima state..." @@ -605,6 +566,10 @@ up() { COOLIFY_CLI_SSH_USER="$(coolify_ssh_user)" spin up -d fi + if [ "$coold_vm_enabled" != "false" ]; then + sync_lima_hosts_into_coolify_container + fi + if [ "$naked" = "true" ]; then echo "==> --naked enabled. Skipping coolify bootstrap and Flux VM wiring. Use /v5 to bootstrap hosts from the UI." return @@ -940,7 +905,7 @@ SH example_nginx_require_pair() { if [ "$(coold_vm_count)" = "1" ]; then - echo "ERROR: example-nginx ping/firewall commands require COOLIFY_COOLD_VM_COUNT=2." >&2 + echo "ERROR: example-nginx ping command requires COOLIFY_COOLD_VM_COUNT=2." >&2 exit 1 fi } @@ -966,28 +931,6 @@ echo 'ok: coolify-example-nginx can reach coolify-example-nginx-2 on tcp/80' SH } -example_nginx_firewall_up() { - local src - local dst - example_nginx_require_pair - - src="$(example_nginx_container_ip 1)" - dst="$(example_nginx_container_ip 2)" - - scripts/dev.sh firewall allow "$src" "$dst" tcp 80 -} - -example_nginx_firewall_down() { - local src - local dst - example_nginx_require_pair - - src="$(example_nginx_container_ip 1)" - dst="$(example_nginx_container_ip 2)" - - scripts/dev.sh firewall revoke "$src" "$dst" tcp 80 -} - example_nginx_help() { cat <<'USAGE' Usage: scripts/dev.sh example-nginx @@ -997,8 +940,6 @@ Commands: down Remove the example nginx containers check-dns Verify host 1 nginx can resolve host 2 nginx through coold DNS ping Verify host 1 nginx can reach host 2 nginx on tcp/80 - firewall up Allow host 1 nginx to reach host 2 nginx through the coolify CLI - firewall down Revoke the example nginx tcp/80 allow rule through the coolify CLI USAGE } @@ -1021,21 +962,6 @@ example_nginx() { ping) example_nginx_ping ;; - firewall) - case "${1:-help}" in - up) - example_nginx_firewall_up - ;; - down) - example_nginx_firewall_down - ;; - *) - echo "unknown example-nginx firewall command: ${1:-help}" >&2 - echo "Run: scripts/dev.sh example-nginx help" >&2 - exit 1 - ;; - esac - ;; -h|--help|help) example_nginx_help ;; @@ -1047,139 +973,6 @@ example_nginx() { esac } -firewall_help() { - cat <<'USAGE' -Usage: scripts/dev.sh firewall - -Commands: - allow [proto] [port] Allow traffic through the coolify CLI (proto/port optional) - revoke [id|src] [dst] [proto] [port] - Remove an allow rule through the coolify CLI - list List allow rules through the coolify CLI - containers List registered containers through the coolify CLI - -Examples: - scripts/dev.sh firewall allow 10.210.0.2 10.210.1.2 tcp 80 - scripts/dev.sh firewall revoke - scripts/dev.sh firewall revoke 10.210.0.2 10.210.1.2 tcp 80 - scripts/dev.sh firewall revoke 3ba6e0c235a6 - scripts/dev.sh firewall list -USAGE -} - -coolify_firewall() { - local command="$1" - shift - local nodes - local ssh_config - - ensure_coolify - nodes="$(coolify_nodes_arg)" || return 1 - ssh_config="$(lima_ssh_config)" || return 1 - - "$(coolify_cli_bin)" firewall "$command" \ - --nodes "$nodes" \ - --ssh-config "$ssh_config" \ - --ssh-user "$(coolify_ssh_user)" \ - "$@" -} - -firewall_allow() { - local src="${1:-}" - local dst="${2:-}" - local proto="${3:-}" - local port="${4:-}" - local args=() - - if [ -z "$src" ] || [ -z "$dst" ]; then - firewall_help >&2 - exit 1 - fi - - if [ -n "$port" ] && [ -z "$proto" ]; then - echo "ERROR: port requires proto (tcp or udp)." >&2 - exit 1 - fi - - args+=(--from "$src" --to "$dst") - if [ -n "$proto" ]; then - args+=(--proto "$proto") - fi - if [ -n "$port" ]; then - args+=(--port "$port") - fi - - coolify_firewall allow "${args[@]}" -} - -firewall_revoke() { - local id_or_src="${1:-}" - local dst="${2:-}" - local proto="${3:-}" - local port="${4:-}" - local args=() - - if [ -z "$id_or_src" ]; then - echo "Current firewall allow rule IDs:" - firewall_list - echo - echo "Revoke one with: scripts/dev.sh firewall revoke " - return - fi - - if [ -z "$dst" ]; then - args+=(--id "$id_or_src") - else - args+=(--from "$id_or_src" --to "$dst") - if [ -n "$proto" ]; then - args+=(--proto "$proto") - fi - if [ -n "$port" ]; then - args+=(--port "$port") - fi - fi - - coolify_firewall revoke "${args[@]}" -} - -firewall_list() { - coolify_firewall list "$@" -} - -firewall_containers() { - coolify_firewall containers "$@" -} - -firewall() { - local command="${1:-help}" - if [ $# -gt 0 ]; then - shift - fi - - case "$command" in - allow) - firewall_allow "$@" - ;; - revoke|remove|delete|deny) - firewall_revoke "$@" - ;; - list) - firewall_list "$@" - ;; - containers) - firewall_containers "$@" - ;; - -h|--help|help) - firewall_help - ;; - *) - echo "unknown firewall command: $command" >&2 - echo "Run: scripts/dev.sh firewall help" >&2 - exit 1 - ;; - esac -} - refresh_test_host_key() { echo "==> Refreshing /tmp/testhostkey inside coolify..." @@ -1258,7 +1051,6 @@ Commands: clean-vms Delete the coold Lima VMs and all VM-local runtime state (alias for down --cleanup) naked-vm Recreate the naked Lima VM used for bootstrap testing corrosion Inspect Corrosion state, config, logs, and registered containers - firewall Manage dev coold firewall allow rules example-nginx Start/check example nginx containers with coold DNS coolify Install/run the released coolify dev helper USAGE @@ -1294,9 +1086,6 @@ case "$cmd" in corrosion) corrosion "$@" ;; - firewall) - firewall "$@" - ;; example-nginx) example_nginx "$@" ;; diff --git a/tests/Feature/DevEnvironmentExampleTest.php b/tests/Feature/DevEnvironmentExampleTest.php index 2462a113e..8b93adc90 100644 --- a/tests/Feature/DevEnvironmentExampleTest.php +++ b/tests/Feature/DevEnvironmentExampleTest.php @@ -21,11 +21,10 @@ it('does not include coold dev tooling defaults in the development env example', ]); it('defaults coold dev VM settings in Laravel config', function () { - expect(config('coold.dev_host_count'))->toBe(2) - ->and(config('coold.dev_host_id'))->toBe('coolify-coold-dev') - ->and(config('coold.dev_host_id_2'))->toBe('coolify-coold-dev-2') - ->and(config('coold.dev_wireguard_ip_1'))->toBe('100.64.0.10') - ->and(config('coold.dev_wireguard_ip_2'))->toBe('100.64.0.11') + expect(config('coold.coolify_cli_bin'))->toBe('/usr/local/bin/coolify') + ->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(); }); diff --git a/tests/Feature/DevScriptExampleNginxTest.php b/tests/Feature/DevScriptExampleNginxTest.php index ef7762f5d..1bd7ce3e1 100644 --- a/tests/Feature/DevScriptExampleNginxTest.php +++ b/tests/Feature/DevScriptExampleNginxTest.php @@ -2,22 +2,22 @@ use Symfony\Component\Process\Process; -it('documents example nginx connectivity and firewall commands', function () { +it('documents example nginx connectivity commands', function () { $process = new Process(['bash', base_path('scripts/dev.sh'), 'example-nginx', 'help'], base_path()); $process->run(); expect($process->isSuccessful())->toBeTrue() ->and($process->getOutput())->toContain('ping') - ->and($process->getOutput())->toContain('firewall up') - ->and($process->getOutput())->toContain('firewall down') + ->and($process->getOutput())->not->toContain('firewall up') + ->and($process->getOutput())->not->toContain('firewall down') ->and($process->getOutput())->not->toContain('firewall-up') ->and($process->getOutput())->not->toContain('firewall-down'); }); -it('uses the coolify firewall wrapper for example nginx firewall changes', function () { +it('does not wire example nginx to removed firewall CLI commands', function () { $script = file_get_contents(base_path('scripts/dev.sh')); - expect($script)->toContain('scripts/dev.sh firewall allow "$src" "$dst" tcp 80') - ->and($script)->toContain('scripts/dev.sh firewall revoke "$src" "$dst" tcp 80'); + expect($script)->not->toContain('scripts/dev.sh firewall allow "$src" "$dst" tcp 80') + ->and($script)->not->toContain('scripts/dev.sh firewall revoke "$src" "$dst" tcp 80'); }); diff --git a/tests/Feature/DevScriptFirewallDelegationTest.php b/tests/Feature/DevScriptFirewallDelegationTest.php index d001d39f5..3f57d6ff2 100644 --- a/tests/Feature/DevScriptFirewallDelegationTest.php +++ b/tests/Feature/DevScriptFirewallDelegationTest.php @@ -1,12 +1,11 @@ toContain('coolify_firewall()') - ->and($script)->toContain('"$(coolify_cli_bin)" firewall "$command"') - ->and($script)->toContain('--nodes "$nodes"') - ->and($script)->toContain('--ssh-config "$ssh_config"') + expect($script)->not->toContain('coolify_firewall()') + ->and($script)->not->toContain('scripts/dev.sh firewall') + ->and($script)->not->toContain('firewall ') ->and($script)->not->toContain('/api/v1/firewall/allow') ->and($script)->not->toContain('firewall_api_for_each_vm'); }); @@ -22,13 +21,28 @@ it('installs the coolify CLI in both application container images', function (st 'production image' => 'docker/production/Dockerfile', ]); -it('refreshes the host coolify CLI on every dev script run', function () { +it('runs the coolify CLI from the development application container', function () { $script = file_get_contents(base_path('scripts/dev.sh')); - expect($script)->toContain('url="https://github.com/coollabsio/coold/releases/download/${version}/coolify-${os}-${arch}.tar.gz"') - ->and($script)->toContain('==> Installing coolify from ${url}') - ->and($script)->not->toContain('COOLIFY_CLI_FORCE_DOWNLOAD') - ->and($script)->not->toContain('if [ -x "$bin" ] && "$bin" --version >/dev/null 2>&1'); + expect($script)->toContain("printf '%s\\n' '/usr/local/bin/coolify'") + ->and($script)->toContain('spin exec -T coolify "$(coolify_cli_bin)" init bootstrap') + ->and($script)->toContain('spin exec -T coolify "$(coolify_cli_bin)" "$@"') + ->and($script)->toContain('ensure_coolify_container_ssh_key') + ->and($script)->not->toContain('.dev/bin/coolify') + ->and($script)->not->toContain('coolify-${os}-${arch}.tar.gz'); +}); + +it('syncs host-resolved Lima local names into the Coolify container hosts file', function () { + $script = file_get_contents(base_path('scripts/dev.sh')); + + expect($script)->toContain('resolve_lima_dns_name()') + ->and($script)->toContain('dscacheutil -q host -a name "$name"') + ->and($script)->toContain('getent ahostsv4 "$name"') + ->and($script)->toContain('sync_lima_hosts_into_coolify_container()') + ->and($script)->toContain('spin exec -T -u root coolify sh -lc') + ->and($script)->toContain('cat "$next" > /etc/hosts') + ->and($script)->toContain('sync_lima_hosts_into_coolify_container') + ->and($script)->toContain('if [ "$naked" = "true" ]; then'); }); it('does not require predefined UI node environment variables in the development app container', function () { @@ -81,28 +95,38 @@ it('seeds bootstrapped Lima VMs into v5 development server state', function () { expect($script)->toContain('sync_v5_dev_lima_servers()') ->and($script)->toContain('COOLIFY_CLI_SSH_USER="$ssh_user"') ->and($script)->toContain('COOLIFY_CLI_SSH_USER="$(coolify_ssh_user)" spin up -d') - ->and($script)->toContain('lima_ssh_port "$index"') - ->and($script)->toContain('host.docker.internal') + ->and($script)->toContain('coold_vm_dns_name()') + ->and($script)->toContain('$(coold_vm_dns_name "$index")') ->and($script)->toContain('v5:sync-dev-lima-servers') - ->and($script)->toContain('--server="${instance}|host.docker.internal|${ssh_user}|${ssh_port}|$(coold_vm_wg_ip "$index")"') + ->and($script)->toContain('--server="${instance}|$(coold_vm_dns_name "$index")|${ssh_user}|22|$(coold_vm_wg_ip "$index")"') ->and($compose)->toContain('COOLIFY_CLI_SSH_USER: "${COOLIFY_CLI_SSH_USER:-}"') ->and($script)->not->toContain('db:seed --class=V5DevLimaSeeder --force') - ->and($script)->not->toContain('--server "${instance}|${node}|$(coolify_ssh_user)|22"'); + ->and($script)->not->toContain('host.docker.internal|${ssh_user}'); }); -it('configures predictable Lima SSH local ports for dev VMs', function () { +it('uses the shared Lima template directly instead of generated per-VM YAML', function () { $script = file_get_contents(base_path('scripts/dev.sh')); $vmScript = file_get_contents(base_path('scripts/coold-vm.sh')); $template = file_get_contents(base_path('dev/lima/coold.yaml')); - expect($script)->toContain('coold_vm_ssh_port()') - ->and($script)->toContain('COOLIFY_COOLD_VM_SSH_PORT_') - ->and($script)->toContain('6000${index}') - ->and($script)->toContain('COOLIFY_COOLD_VM_SSH_PORT="$(coold_vm_ssh_port "$index")"') - ->and($vmScript)->toContain('SSH_PORT="$(read_coolify_env COOLIFY_COOLD_VM_SSH_PORT 60002)"') - ->and($vmScript)->toContain('{{COOLIFY_COOLD_VM_SSH_PORT}}') - ->and($template)->toContain('ssh:') - ->and($template)->toContain('localPort: {{COOLIFY_COOLD_VM_SSH_PORT}}'); + expect($script)->toContain('coold_vm_dns_name()') + ->and($script)->not->toContain('COOLIFY_COOLD_VM_SSH_PORT="$(coold_vm_ssh_port "$index")"') + ->and($vmScript)->toContain('limactl start --tty=false --name="$INSTANCE" "$TEMPLATE"') + ->and($vmScript)->not->toContain('GENERATED=') + ->and($vmScript)->not->toContain('generate_yaml()') + ->and($template)->not->toContain('{{COOLIFY_COOLD_VM_SSH_PORT}}'); +}); + +it('sets the Lima guest hostname to the instance name for predictable mDNS', function () { + $vmScript = file_get_contents(base_path('scripts/coold-vm.sh')); + $template = file_get_contents(base_path('dev/lima/coold.yaml')); + + expect($vmScript)->toContain('ensure_mdns_hostname()') + ->and($vmScript)->toContain('hostnamectl set-hostname "$INSTANCE"') + ->and($vmScript)->toContain('systemctl restart avahi-daemon.service') + ->and($vmScript)->toContain('ensure_mdns_hostname') + ->and($template)->toContain('hostnamectl set-hostname "{{.Name}}"') + ->and($template)->not->toContain('hostnamectl set-hostname myvm'); }); it('authorizes the seeded testing host key in dev Lima VMs', function () { @@ -112,10 +136,11 @@ it('authorizes the seeded testing host key in dev Lima VMs', function () { ->and($template)->toContain('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68') ->and($template)->toContain('authorized_keys') ->and($template)->toContain('grep -qxF "$coolify_test_public_key"') - ->and($template)->toContain('install -d -m 700 /root/.ssh') - ->and($template)->toContain('/root/.ssh/authorized_keys') - ->and($template)->toContain('target_home="$(getent passwd 501 | cut -d: -f6 || true)"') - ->and($template)->toContain('owner="$(stat -c \'%u:%g\' "$target_home")"') + ->and($template)->toContain('install_public_key root') + ->and($template)->toContain('install_public_key coolify') + ->and($template)->toContain('owner="$(stat -c \'%u:%g\' "$home")"') + ->and($template)->toContain('hostnamectl set-hostname "{{.Name}}"') + ->and($template)->not->toContain('hostnamectl set-hostname myvm') ->and($template)->not->toContain('mode: user') ->and($template)->not->toContain('user="$(basename "$home")"'); }); diff --git a/tests/Feature/V5/DashboardTest.php b/tests/Feature/V5/DashboardTest.php index 9c8726d53..7603eb9fc 100644 --- a/tests/Feature/V5/DashboardTest.php +++ b/tests/Feature/V5/DashboardTest.php @@ -4710,8 +4710,8 @@ it('seeds dev Lima VMs into v5 clusters and servers idempotently', function () { ->and($cluster->created_by_user_id)->toBe($user->id) ->and($cluster->description)->toBe('Local Lima development cluster managed by scripts/dev.sh.') ->and(V5Server::query()->count())->toBe(2) - ->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'host.docker.internal')->where('ssh_user', get_current_user())->where('ssh_port', 60001)->exists())->toBeTrue() - ->and(V5Server::query()->where('name', 'coold-dev-2')->where('host', 'host.docker.internal')->where('ssh_user', get_current_user())->where('ssh_port', 60002)->exists())->toBeTrue() + ->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'coold-dev.local')->where('ssh_user', 'coolify')->where('ssh_port', 22)->exists())->toBeTrue() + ->and(V5Server::query()->where('name', 'coold-dev-2')->where('host', 'coold-dev-2.local')->where('ssh_user', 'coolify')->where('ssh_port', 22)->exists())->toBeTrue() ->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) @@ -4747,7 +4747,7 @@ it('seeds dev Lima VMs by updating existing named servers', function () { (new V5DevLimaSeeder)->run(); expect(V5Server::query()->where('name', 'coold-dev')->count())->toBe(1) - ->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'host.docker.internal')->where('ssh_port', 60001)->exists())->toBeTrue() + ->and(V5Server::query()->where('name', 'coold-dev')->where('host', 'coold-dev.local')->where('ssh_port', 22)->exists())->toBeTrue() ->and(V5Server::query()->count())->toBe(2); });