Repository navigation
feat(cli): run a managed deployment on Incus - #5427
Merged
Merged
Conversation
@tale/incus gains what the CLI's Incus runtime drives a whole stack with, each call a module of its own over the client's bounded transport, so the sandbox spawner loads none of them: - exec: mode 1 (exit code) and mode 2 (record-output read under a byte cap, then deleted); stdin pushed as a 0600 file of at most 1 MiB in /dev/shm and redirected in by the command's shell, never argv; the image's timeout wraps the command when the caller names it, since Incus cannot cancel a record-output exec. - exec-session: mode 3, a websocket session over the same pinned mutual TLS (fds 0/1/2 and control), stdin of any size, ended by closing the control socket. - files: the instance and custom-volume file APIs with owner and mode, every write as application/octet-stream, and no rename. - volumes, snapshots, state: rename (detached only, waiting out the mount the file API leaves behind), attach and detach of a disk under an ETag, pool resources, volume snapshots and a copy from one, start, stop with a grace or forced, freeze, unfreeze, instance and project state. - client: operation cancel, the console and log files, image alias create/list/delete and image delete, and a refusal of a plain-HTTP endpoint without a transport of its own. - unix-transport: a client over the local socket, for bootstrap only. The fake answers every one of them, a websocket exec through a loopback Bun.serve. tests/real runs the client against a real Incus in a restricted project (TALE_INCUS_REAL=1, TALE_INCUS_DIR; `test:real`).
tale.json's runtime and the managed specification's runtime.engine and
runtime.incus now share one schema (lib/project/runtime-declaration.ts),
which select-runtime reads through readProject: engine docker (the
default) or incus with its Incus block (endpoint, project, pool, network
with subnet, gateway and block, sessions subnet, ingress, registry
mirror, root size, volumes with snapshots.* and zfs.* settings, and
operatorReach). A workspace needs a /24 block; a managed deployment takes
a /24 or a /25. Unknown engines, a block without its engine and the
engine without its block are refused, never read as Docker. TALE_RUNTIME
only guards the declared engine.
In the managed specification every text value of the Incus block may be
a literal or an environment reference that preparation resolves. With engine incus,
runtime.containerPrefix is refused and sandbox is required as
{ backend: incus } without an address, keeping a declared egressProxy.
runtime.json gains runtime: incus and pins incus-stack.json exactly then,
so older CLIs refuse such a bundle, and verify-bundle requires
runtime/incus-stack.json exactly for an Incus specification.
Running a workspace or a managed deployment on Incus is refused as "not
available yet" at selection, prepare and apply, until the lanes that
build it lift the refusal.
The Incus credentials loader reads client.crt, client.key and server.crt
from .tale/incus (or TALE_INCUS_DIR) or <stateDirectory>/incus under the
custody check the spawner credentials already use (now shared), and
validates them with @tale/incus readIncusTlsMaterial.
tale init --runtime and the --incus-* flags write the block, checked
before anything is scaffolded; a re-init and tale update keep it, and a
re-init stops before it writes over a block this CLI cannot read. Docs
in English, German and French; INC-F1, INC-F2 and INC-B1 and an
automation row in the CLI's manual layer.
The Incus runtime reads no compose file, so a pure compiler turns the
compose documents Docker Compose runs into an Incus stack target
(lib/runtime/incus/compile): images by digest alias, data and
runtime-owned volumes, the files the runtime writes onto them, and every
instance with its slot address, a CIDR NIC with the gateway and no IPv6,
oci.entrypoint behind the patched /init rewriting the halt signals to the
stop signal (a leading tini dropped), environment, limits.memory and
limits.processes on every instance, start priorities and the three stop
groups (90, 50, 10), oci.dns.nameservers (the gateway for the resolvers,
names for the rest), user.tale.* labels, health check and stop contract.
It adds the resolvers and the switch, the resolver records in a stack
and a session view, the switch's upstream lists over every slot of both
colours (blue alone on a managed /25) and the ingress.
The generators gain build*Compose(), which return the document;
generate*Compose() stringifies it as before. The managed path compiles the
bundle's compose.yml.
Every ComposeService key has one rule (map, ignore with its reason, or
refuse), and rules-coverage.test.ts fails on a key without one. Each
refusal carries a sentence and has its own test; the managed database's
loopback publishes and the Docker sandbox shape's mounts and ports are
ignored with their reasons, and the spawner always gets the Incus shape.
A value in a shape the translator does not read (long-syntax ports, a
mapping of cap_add or extra_hosts) is refused, never dropped.
The placement table (address, start and stop priority, the images
accepted without the init) is held to every generated document and
compose.yml. The address plan takes a /24 for a workspace and a /25 for a
managed deployment and refuses more replicas than a role's 16 slots.
Interpolation and the .env grammar follow Docker Compose (${V}, :-, -,
:?, ?, nested; any other operator refused), from the run's process
snapshot over .env for a workspace and from .env alone for a managed
deployment; a refused .env line is named by its number, never its text.
interpolate-oracle.test.ts holds both to docker compose
config under env -i for every generated document and compose.yml; it
skips on Windows and without Docker Compose, except on a Linux CI runner
with Docker.
Goldens: stateful.json, colour-blue.json and managed-compose.json, each
with no refusal.
The compiled stack target now has a planner, an applier, a journal and a readback (lib/runtime/incus/plan and apply), the configuration flow's validate, plan, apply and read back over an Incus project. The plan (incusPlanSchema, strict like configurationPlanSchema) holds digests and revisions only: its target, a phase, the destination (endpoint, pinned server certificate, project, pool, architecture) and one resource per image, volume, file, instance and ingress, each with an action (create, replace, update, delete, unchanged, deferred) and its effects (downtime, drain-sandbox, drain-backend, data, names, switch). Digests are HMAC-SHA256 under a per-stack key in .tale/incus/plan.key, made 0600 once. The planner is pure and refuses an instance or a volume that another stack labelled, or that carries no label. A running stop-gated service (db, object-store, proxy) keeps running until a deploy with --stop, as on Docker; the switch and the resolvers defer an image change alone, and are replaced behind --stop when their running image does not read the file format the CLI writes. The switch's upstream lists are part of the switch and written only when it is made, so a flip, a roll or a scale change rewrites none. A missing data volume of a stack that ran before (a deploy record in backups, an instance of the stack, a manifest, a pre-restore holder) is the data effect; only a first deploy, or the first after a recorded reset, makes one. An instance is compared without the oci.cwd, oci.uid and oci.gid that Incus fills from its image, and a volume on the keys the target sets alone, not the pool's volume.* defaults Incus copies into it, so a converged stack on a real Incus plans nothing. The applier converges in order: images, volumes, files, the instances tier by tier (made stopped, started, health-gated before their dependants), the superseded resolver records, the switch, the ingress, the orphans last. Instance PATCHes carry the planned ETag; where it moved, or Incus gives none, the resource is read again and compared with the plan (adopted when already as wanted, refused when changed otherwise). downtime needs --stop, and data is never applied. It writes user.tale.want on every start and stop, withdraws a switched replica and drains the spawner and backends before they stop, writes resolver records and the deploy record (backups/stack.<gen>.json) under a new name before it deletes the old, and journals each step in .tale/incus/runtime.json: pending, ready or interrupted (on SIGINT), exec payload files in /dev/shm, withdrawal markers, and restore-pending, which stops converge, start, deploy, rollback, services and migrate with a sentence naming tale restore --resume and --abort. The readback gives InstanceInfo with the allow-listed environment and a keyed digest, never a secret. Pulling an image, the health check, draining and withdrawing are hooks the verbs lanes fill. Tests against the @tale/incus fake cover every action and effect, compare-and-swap conflicts, the gates, the ingress, the missing-data-volume rule and the restore-pending refusals, and a crash after every write of a first deploy and of an upgrade with --stop, after which the next run converges to the same holdings and plans nothing.
The Incus runtime answers its first verbs: the probe, the images, the volumes and the instances (lib/runtime/incus/incus-runtime.ts and verbs/), over the restricted stack project's REST API. The probe reads GET /1.0, the project, the tale-stack profile and the pool. It requires the API extensions by name (instance_oci and its entrypoint, oci_network_config, storage_shifted, storage_initial_owner, file_storage_volume, the restricted project's snapshots and image servers, instance_auto_restart; not instance_port_forward), a single server, an architecture Tale ships images for, idmapped mounts (refused when the kernel has none, or on a ZFS older than 2.2), a project with its own images, profiles and volumes, a tale-stack profile that sets limits.memory and limits.processes whenever the project limits them, and every registry host in restricted.images.servers exactly as the remote URL carries it. A pool without quotas warns. Images are pulled by digest into the project under tale-<repository>-<32 hex>; an alias the project holds is kept, a failed pull does not stop the others, and the pull order is kept in .tale/incus/images.json so pruneImages keeps the current image, the one before it, and any an instance runs. What only an image's filesystem says is read once per digest through a stopped probe instance and its file API, then cached in .tale/incus/image-facts.json: the owner and mode of each mount point, the uid and gid of the app user, where timeout sits on the image PATH, and dev.tale.infra-formats. Volumes are shifted, labelled and made with their mount point's owner as initial.uid/gid/mode. A mount point the image lacks makes a root-owned volume with a warning, as Docker makes one there (main's platform and sandbox images hold no /app/static-assets and /app/platform-config); a path the image holds as a file is refused, and so is a stack whose volumes sit on another pool than the declared one. Instances are made stopped from their spec, started with user.tale.want running and user.tale.started set from the state's started_at, so a later started_at reads back as a restart Incus made. A stop waits its grace and is then forced with a warning; 'forever' waits the declared grace and never forces. Label writes wait out a start or stop Incus is running. Remove, parallel freeze and thaw, the census and an InstanceInfo readback follow, and provePid1 compares /proc/1/cmdline with oci.entrypoint. The applier starts and stops through the same verbs. StackRuntime gains ensureImages (Docker: every pull, then the tags, the argv unchanged) and inspectImage (the registry reader on both runtimes). runtimeForEngine builds the Incus runtime from a connection; a workspace that declares incus is still refused. The verbs the Incus runtime does not answer yet refuse in words. The in-memory Incus reports architectures, clustering, storage drivers, started_at and what an image holds. Proven on Incus 7.6 (TALE_INCUS_REAL=1 bun test src/lib/runtime/incus/verbs): tale-proxy 0.5.105 pulled by digest and kept on the second ensure, its /data owner read and given to a new volume, an instance made, started, read back, stopped in 0.7 s and removed; PID 1 is lxc-init without /init and the full /init argv with an Incus-ready image; main's platform 0.5.105 answers /api/health 200 on empty volumes with /app/data and /app/static-assets owned 1001:1001, and main's sandbox 0.5.105 listens on :8003 on an empty platform-config.
The Incus runtime answers exec, its held session, the copies in and out, the console, one-shot helpers and health (lib/runtime/incus/verbs: exec, files, logs, oneshot, health, health-tracker). Exec runs in mode 1 or 2 under the image's own timeout, which the image facts name per digest, because Incus cannot cancel a record-output exec; an image without one, or whose facts were never read, runs over the websocket, which a closed control socket ends. A stdin of up to 1 MiB is pushed as a 0600 file in /dev/shm, owned by the exec user, journaled before the push and removed by the command before it reads it; a larger stdin, and any stdin for db (whose /dev/shm is Postgres's shared memory), goes over the websocket and writes nothing. The journal's leftover files are reaped before the next exec in their instance, sparing the files a running exec of the same process still reads, and the journal's updates in one process run one at a time. Every exec has a deadline (600 s by default) and a byte cap (16 MiB), and fails past either on every path: a command the image's timeout ends at the deadline fails as one past the websocket deadline does, whether timeout exits 124 (coreutils) or 143 (busybox). StackRuntime gains execSession: a websocket exec on Incus, docker exec -i on Docker; RuntimeAccess shares its RuntimeSession type. A copy into a stack volume (the config store at /app/data, mounted read-only in the platform) goes through the volume's file API, any other path through the instance's, each file and directory written with its owner (names read in the instance's own passwd and group) and its local mode. A copy out walks the file API to its byte cap and deadline into 0600 files, and names a symlink instead of following it. Spike S21 measured the console on Incus 7.6: a read of a running instance takes the lines it shows from every later read and from the console it keeps after a stop, and about 128 KiB is all it keeps of the lines no read took. So tale logs reads a running instance only with the new --consume, after a one-line warning; --follow polls, --tail cuts on the client and --since is refused. A stopped console is read freely. Health checks, waits and the crash report never read a running console; the report says "stopped after repeated crashes" for an instance boot.autorestart gave up on, with the tail labelled "the last console lines Incus kept". A one-shot job runs in job-<purpose>-<8 hex>, made from the release's tale-proxy with the job's volumes, no network device and sleep as its entrypoint, labelled user.tale.role=helper; its exit code comes from the exec, then it is deleted, and the next job reaps one a crash left. Health runs the compose healthcheck by exec (CMD-SHELL as sh -c) and judges it with Docker's interval, retries, start_period and start_interval in a pure HealthTracker; waitForHealthy takes the runtime and keeps its 2 s cadence, and controlCall hands its budget to the runtime as the exec's deadline. The runtime contract suite also runs the Incus runtime over the in-memory Incus, for the verbs it answers today. Proven on Incus 7.6 (TALE_INCUS_REAL=1): stdin of 1 KiB and 2.7 MiB byte-equal, nothing left in /dev/shm or the journal, sleep 30 ended at 2 s on both paths, the byte cap, a held session, copies with owner and mode through both file APIs, a helper with only lo, health by exec, and a running console taken by its read.
The Incus runtime renders the files of its two dnsmasq resolvers and of its traffic switch, and withdraws and admits colour replicas (lib/runtime/incus/verbs: names, switch). Each resolver view has its dnsmasq.conf (local-ttl=1, server=<gateway> for names or no-resolv for names-sessions, local=// and one local=/name/ per name) and one records file in its hosts directory, one line per address, so a colour name answers each of its replicas. Nothing but a DNS name and an IPv4 address reaches a file. Records are written in place, because on Incus 7.6 with dnsmasq 2.90 a record two hosts files share is kept once, under the file read first, so writing a new file and deleting the old one (or truncating it first) dropped names the new file held; 36% of queries for an unchanged name answered NXDOMAIN. One file rewritten in place lost none of 1,420,344 answers over 40 changes. local=// answers every single-label name from the records, so a name a records change adds to a running resolver (a colour's names, on every flip) answers AAAA empty instead of being forwarded. The switch's upstream files list every slot of each role in every colour the block holds, owned by the switch instance and written once. A proxy or egress image that does not name the format the CLI writes in dev.tale.infra-formats (switch-upstreams/1, names-hosts/1) is refused at compile, so release/0.6's proxy fails closed; the formats now live in the placement table, which the planner reads. StackRuntime gains `withdrawal` and takes replicas in admit and withdraw. Docker keeps network disconnect, after the drain, its argv unchanged and its order held by a guard. Incus sets the role's marker (/tmp/platform-withdrawn, /tmp/backend-withdrawn) by exec as the image's app user from the image facts, journals it, waits two switch health intervals and cuts nothing, before the drain; admit removes a marker only where one is set. retireColor withdraws where the runtime allows it. The planner no longer takes oci.cwd, oci.uid and oci.gid for drift on an OCI instance without image.oci.* beside them: Incus 7.6 keeps none, and every second plan replaced every instance. Real check on Incus 7.6 (project ti-13), through the applier, the compiler's resolver and switch specs and main's edge in front of the switch. Musl and glibc clients resolve, AAAA is answered empty, the session view refuses other names; one flip under 16 plain, 8 SSE and 8 WS loops through the edge and a 1 Hz /api/health poller: 1,520,494 OK, no 5xx, no edge 502/503/504 or maintenance page, the edge's upstreams never failed, and the 8 SSE and 8 WS drops came at the old colour's stop alone.
tale incus contract prints the node contract, a versioned additive JSON document (schemaVersion 1) computed from the Incus declaration and the compiled stack target: required API extensions, the restricted project with limits for the peak (stateful tier, two colours, helpers), the tale-stack profile with its root disk and default limits, the app bridge (DHCP outside the block, NAT, no IPv6, default drop), the complete ACL (sessions' real subnet, spawner to the Incus API and the sessions, public egress per role, the effective egress proxy, operatorReach), pool and volume sizes, the certificate to trust and Tale's reserved stop band. The contract compiles with placeholder image facts, so it needs no registry; a test holds it equal to the contract of real-shaped facts. tale incus check reads the node back with the restricted certificate and refuses mismatches in fixed sentences, warns on pool capacity, and judges forwarding by effect: a helper on the app bridge opens each probed rule's connection, and a timeout is refused with the DOCKER-USER remedy first and ip-forward-no-drop second. tale incus bootstrap applies the contract over the local admin socket: plan first, idempotent, the sandbox zone as the spawner's docs lay it out, two restricted certificate pairs, the server certificate pinned (replaced only when asked), tale.json runtime and .tale/incus, .tale/sandbox-incus. It reads the FORWARD policy and applies DOCKER-USER rules only after asking, warns when another project stops inside Tale's band, and --remove takes away only what it made.
A deployment specification that declares runtime.engine "incus" is now prepared and applied on Incus (design 7.1, D-39). - deploy prepare reads every image from its registry, with no Docker daemon; the sandbox runs in its Incus shape on the address plan and publishes no port; the declared sandbox.egressProxy is kept (R-13); the bundle records runtime "incus" and pins runtime/incus-stack.json, the compiled stack without any environment value. - deploy --bundle runs applyIncusRuntime, the sibling of applyRuntime: probe, the node check against the contract, the target compiled from the deployment's .env and held to the prepared stack, custody, plan, apply (images, volumes, files, instances by tier behind health gates, the spawner drained before it is replaced, switched replicas withdrawn first, the downtime gate of --stop), readback, and the shared .tale/runtime.json receipt. --plan-only prints the plan (never with --configuration-only); --plan applies a reviewed plan only while the node still needs it. A pending restore refuses the deploy (R3-22). A single replica of platform or backend-api replaced in place is printed as a notice (R3-36). - Custody over InstanceInfo: labels, stack, index 1, no one-off, the expected disks and nothing outside the volume allow-list, and the applied configuration digest. - RuntimeAccess on Incus (runtime-access-incus.ts) for accept, observe, provision, export-client, the runtime configuration and the automation protocol: 64-hex identities per instance incarnation, Compose-named labels from one normalized readback, the backend-local binary pushed 0550. - copyOut takes a byte cap on both runtimes and refuses past it with one sentence; the bundled-database check reads a derived bundledDatabase fact, so no DATABASE_URL leaves either runtime (R-15). - A changed existing deployment on Incus is refused until the CLI can take its recovery snapshot there. - Docs (EN, DE, FR), the INC-F7 to INC-F10, INC-B5 and INC-B6 boxes and the automation row.
tale dev and the sandbox device run on this machine's Docker, and Incus runs on Linux only, so a workspace that declares runtime.engine "incus" refuses them in one sentence that says why (design D-35). tale dev refuses before it writes anything (--stop still stops the Docker containers a dev run left); sandbox connect and update refuse before the one-time join token is spent. declaredEngine moves to lib/project/declared-engine.ts beside the new check, so the loops read the declaration without loading the runtimes. tale doctor reports the declared runtime first. On an Incus workspace it checks Incus instead of Docker: the credentials in .tale/incus (custody, a pinnable server certificate, the key, expiry), the probe with the stack's restricted certificate, row by row, and each instance Incus stopped after repeated crashes with the last console lines it kept (R-09). Only a stopped instance's console is read, never a running one (R3-07). --offline makes no network call. Manual boxes INC-F2a, INC-F2b, INC-F6a and INC-F6b; the CLI reference in English, German and French.
tools/cli/e2e/incus drives the compiled CLI through the managed path against a real Incus node, with the stack's eight images built from the checkout (images.sh) into the lab's registry. The suites, in order: bootstrap, managed, fresh-noop, logs, crash-loop, stop, sandbox, export-client and secrets. README.md says what each proves, and what waits for later work (accept, observe, the upgrade's window and the credential export). test:e2e:incus runs them; harness.test.ts holds the pure parts. cli-incus-e2e.yml builds the node with the sandbox lab's lab.sh, runs @tale/incus's real tests in the project bootstrap made, then the suites. CI ready (CLI Incus E2E) is the ninth readiness context: PR scope runs the lab for the cli-incus filter, held by a test to the import closure of lib/runtime/incus, and the verdict blocks only on cli-incus-required (the runtime, packages/incus, the workflow) or a full scope; elsewhere it is reported, not required (R3-31). CI.md, the Forgejo port and the manual automation register follow. What the lab found, fixed here with a unit test each: - A declared host that serves a registry other than Docker Hub (runtime.incus.registryMirrors), for prepare, Incus pulls, the contract's image servers, the sessions' image and check's probe (docs in en/de/fr). - tale incus bootstrap --spec --compose writes a managed deployment's credentials into its state directory. - An empty .env value is a key Incus does not keep: it no longer reads as drift, and the fake drops it as Incus does. - A first deploy stopped at a health gate resumes. - tale logs --follow ends cleanly when the service stops between a state read and a console read, and shows each line once although a stopped console hands out the whole run again.
A new page, Run Tale on Incus, in English, German and French, takes an operator from an Incus host to a managed deployment on the experimental runtime. It covers the prerequisites (API extensions, idmapped mounts, a zfs, btrfs or lvm pool, the pinnable API endpoint), the declaration, the node contract with bootstrap and check, the restricted model and the network ACL rules, the two ingress modes, sizing from the profile and service limits, the in-place window of a single replica, logs that a read consumes, the three stop groups and the host-shutdown bound on a Tale-only host, a host shared with Docker, the current limits and the verified scope of the end-to-end suites. The install index names the runtime choice. cli-install asks for Docker only on the Docker runtime, links the page, and its Incus sandbox section becomes "the sandbox under each runtime" under the anchor it had. The environment reference gains a container runtime section (TALE_RUNTIME, TALE_INCUS_DIR, TALE_PROXY_ROLE, TALE_SHM_SIZE) and SANDBOX_BACKEND names the runtime's only backend. container-architecture traces the Incus layout: the resolvers, the switch and the trust boundary. The sandbox's Incus contract says what the CLI sets when the whole stack runs on Incus; the CLI README shows the incus commands; the repo contract names the runtime interface and what Ops passes; .env.example and compose.yml say how the Incus runtime uses them. nav.json and published.json carry the new page.
Bun's mock.module is process-global and CI's Build (linux) runs every CLI test file in one process. On its file order rollback.test.ts runs before runtime-incus-apply.test.ts and left wait-for-healthy replaced by a mock that afterEach had reset to answer undefined, so the Incus apply's real health wait read every instance as unhealthy at once and 7 apply tests failed in ~200 ms. The rollback suite now captures each module it mocks and puts the real one back after the file, and the Incus apply suite asserts its health wait is not a leaked mock, so a future leak names itself.
The Windows build job failed 82 distinct tests (164 lines in the log), nearly all over the Incus runtime's private files: the plan key, the journal, copies and the image facts are held by owner and mode (0600 in 0700), which Windows files do not carry, so readKey and readJournal refused every fixture with "must be a regular file of this user with mode 0600". The CLI already keeps POSIX custody off Windows: managed bundles are refused there (assertManagedPlatform), and the Incus credentials' custody check needs a POSIX host. So the Incus runtime follows that concept rather than a weaker check: assertIncusPlatform refuses it in one sentence before anything is read or made, in tale incus check, tale incus bootstrap and createIncusRuntime. tale incus contract reads no file and still runs. readKey, readJournal and markInterruptedSync now refuse a host without account ids instead of skipping the owner check. The tests over those files run on POSIX hosts alone (describePosix, the repository's pattern), the runtime contract drops its Incus leg on Windows, and a Windows-only test proves the refusal. Two tests had Windows faults of their own: the credentials directory expected a rooted path without its drive letter, and the Docker copy-out argv matched a slash-only temporary path. The Incus guide names the requirement in English, German and French.
A websocket exec connected stdin, stdout and stderr together, so the command could start, and write, before this side had finished the stdout upgrade. On macOS (Build (macos)) a command that wrote at once then read back as silent and resolved: its output never reached the byte cap. stdout and stderr are now open, their readers in place, before stdin connects; the command cannot start until it does. The managed-runtime fixture made its state below tmpdir() unresolved; on macOS that is /var/folders/..., below the /var symlink, which the credential custody check refuses as an ancestor, rightly: a private file's ancestors must be real directories. The fixture now resolves its directory as the credentials test already does.
3eb220a made runtimeFixture resolve its temporary directory for every suite. client-export's 'default OS temp path without TMPDIR' case checks the unresolved /tmp path on purpose, against a canonical one, so on macOS (where /tmp resolves to /private/tmp) it failed. Resolving is now an option of the fixture, taken by the Incus apply suite alone, whose code checks private files' custody and refuses the /var symlink as an ancestor. Every other suite keeps tmpdir()'s path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What M3 does
A deployment whose specification declares
runtime.engine: "incus"can now be prepared, applied and run as Incus instances on a single Incus node, on the stable 0.5.x line, with Caddy as the edge. No Docker daemon is needed on that node. The CLI:tale incus contract|check|bootstrap)tale deploy prepareandtale deploy --bundleLocal developer loops (
tale dev, sandbox connect) stay on Docker.tale doctorchecks Incus on an Incus workspace. A CI workflow runs end-to-end tests on a real Incus node. The new docs page "Run Tale on Incus" is in en/de/fr.The runtime is experimental. Docker stays the default, and Docker deployments behave as before. The M2 images (#5424) run unchanged under Compose.
What M3 does not do
accept --origin-containercannot capture an Incus instance. There is no backend drain and no legacy protocol cutover on Incus.incusis still refused byselectRuntime.tale startandtale stop, the status and doctor crash wiring, and the colour-stack listing come with M4 (TI-17). Until then, the docs and the warnings advise a manualincus stoporincus start.acceptandobserveend-to-end suites aretodo, and so is the upgrade window.export-clientis off by default because of a native-client identity defect, which looks engine-independent.Lanes (one commit each)
eda7d452b5feat(cli): extend the Incus client for a whole-stack runtime2bf8ae02d7feat(cli): declare the container runtime of a deployment128321443bfeat(cli): compile the compose stack into an Incus targetd4060d9f83feat(cli): plan and converge an Incus stack with readback249b239fdbfeat(cli): create and stop stack instances on Incus0e3de1b679feat(cli): exec, copy, logs and health checks on Incus0ea74946cffeat(cli): resolve stack names and switch colours on Incus95f4e54a48feat(cli): describe, check and bootstrap an Incus node62625cc1dffeat(cli): apply managed deployments on Incusc2804b2c4afeat(cli): keep local developer loops on Dockeraa77ec10adci(e2e): prove the CLI's Incus runtime on a real Incus node670f67e6f8docs: run Tale on IncusThe chain was reviewed on top of the M2 branch. After #5424 was squash-merged (5adcf24), the chain was rebased onto main without conflicts. Compared with the reviewed tip, the tree differs only by #5424's CRLF tweak in
compose-parity.test.ts.Tests and proofs
Integration gate on a Linux CI VM at 670f67e, Node 22.21.1:
bun run check --continue: format, conflicts and links pass; turbo 55 of 57 tasks pass@tale/cli4598 pass, 69 skip, 1 fail. The failure iscached site plans preserve Compose overrides: the VM has no Docker Compose plugin. The same test fails on clean main 5adcf24 on that VM.@tale/session-manager: theharness.test.tstiming test failed under load. It passes alone three times out of three on this branch, and passes on clean main. M3 does not touchservices/session-manager.@tale/incuspasses with 0 fail. Platform, sandbox and the other workspaces pass.lint:manual,knip:checkandlint:sastpass (0 findings)From the lanes (Incus 7.6 lab node):
@tale/incustests: 12 passtale incuscontract, check and bootstrap: 47 pass (TI-14)Not yet observed:
deploy --bundle, accept and observe with the released images (TI-15)cli-incus-e2e.yml, which can only run after this pushWhat follows
M4, the workspace:
tale startandtale stop, Incus as a workspace engine (TI-17), the recovery snapshot and upgrade window (TI-16), and the remaining docs (TI-24).