@wpelevator/tug 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  The name: tugboats pull ships into place, like the pull-based agents here; `tug` is the CLI.
4
4
 
5
- Deploy Docker Compose projects to one or more ordinary Linux VPSes through a small self-hosted control plane and a pull-based agent on each node, without Kubernetes or a hosted PaaS. The original specification is in [plan.md](plan.md).
5
+ Deploy Docker Compose projects to one or more ordinary Linux VPSes through a small self-hosted control plane and a pull-based agent on each node, without Kubernetes or a hosted PaaS. What it is for, the deployment shapes it targets and the planned milestones are in the [Roadmap](docs/roadmap.md).
6
6
 
7
7
  ## Features
8
8
 
9
9
  - Control API with bearer-token auth, scoped `read`/`deploy`/`admin` API tokens for CI and teammates, an audit trail naming who acted, and SQLite metadata.
10
10
  - Nodes join over SSH (`node-install`) or enroll themselves with a single `docker run` (`node-enroll`), with per-node agent and registry tokens. Agents only dial out, so nodes behind NAT work.
11
- - Agents pull desired state from control, apply a generated per-node Compose file, run TCP/HTTP/Docker health checks and report status.
11
+ - Agents pull desired state from control, apply a generated per-node Compose file, run TCP/HTTP/Docker health checks and report status. An outbound WebSocket wakes them as soon as control queues work and carries live `exec`, followed logs, `ps` and `inspect` sessions, with long-poll and periodic-poll fallbacks for ordinary work.
12
12
  - `x-tug` placement of services and volumes on nodes, with immutable desired-state records per deployment.
13
13
  - Build application images on control from uploaded source, or locally with `deploy --build-local`. Agents download saved images directly from control by default; a bring-your-own registry is optional.
14
14
  - Secrets encrypted at rest, released only to the nodes whose services reference them, with targeted service recreation when only a secret changes.
@@ -16,8 +16,41 @@ Deploy Docker Compose projects to one or more ordinary Linux VPSes through a sma
16
16
  - Opt-in zero-downtime rolling rollouts for ingress-fronted services (`x-tug.rollout`) and automatic rollback of failed deployments (`x-tug.rollback: auto`). See [Compose extensions](docs/compose.md#rolling-rollouts).
17
17
  - Per-node registry pull tokens enforced by an htpasswd-backed registry you provide, with rotation and revocation.
18
18
  - Optional WireGuard mesh between nodes, health-aware cross-node service names and a managed Caddy ingress with automatic HTTPS, each opt-in. See [Networking](docs/networking.md).
19
+ - Private-by-default published ports: generated Compose binds them to the overlay IP, loopback or the node address, never every interface unless you write `0.0.0.0`. See [Networking](docs/networking.md#published-ports-private-by-default).
19
20
  - Control-plane backup and restore, and `tug doctor` diagnostics.
20
21
 
22
+ ## How it fits together
23
+
24
+ ```text
25
+ tug CLI / CI (API token)
26
+ |
27
+ | HTTPS: deploy, status, logs, exec
28
+ v
29
+ +-------------------------------------+
30
+ | Control plane |
31
+ | Control API (role-checked) |
32
+ | SQLite: desired states, secrets |
33
+ | image builds, saved images |
34
+ | registry (optional) |
35
+ +-------------------------------------+
36
+ ^ ^
37
+ | agents dial out: work, status,
38
+ | WebSocket wake and live sessions
39
+ | |
40
+ +------+------------+ +------+------------------+
41
+ | Node app1 | | Node app2 (ingress node)|
42
+ | Agent |<->| Agent |
43
+ | Docker Compose | | Docker Compose |
44
+ | services | | services |
45
+ | | | managed Caddy ingress |
46
+ +-------------------+ +-------------------------+
47
+ <-> optional WireGuard mesh between nodes
48
+
49
+ SSH is used only by the CLI to install the control plane and nodes.
50
+ ```
51
+
52
+ Operators and CI talk to the control API. The control plane stores desired state and builds images; each agent dials out to fetch the work for its node, applies it with Docker Compose and reports health back, so nodes behind NAT need no inbound ports. SSH is used only to install control and agents. The optional WireGuard mesh and Caddy ingress are described in [Networking](docs/networking.md), and every flow in detail in [Architecture](docs/architecture.md).
53
+
21
54
  ## Quick start
22
55
 
23
56
  Install the CLI (Node >= 22.18), or run it without installing through `npx @wpelevator/tug`:
@@ -33,13 +66,14 @@ The CLI installs the matching `ghcr.io/wpelevator/tug:<version>` image on your h
33
66
  3. Set the secrets your services need: `tug secret-set DB_PASSWORD`.
34
67
  4. Add nodes with `tug node-install root@app1.example.com --name app1 --install-docker`, or `tug node-enroll --name app1` and run the printed Docker command. For a bare cloud host, `tug node-enroll --name app1 --install-docker` prints cloud-init user-data.
35
68
  5. Deploy: `tug deploy -f docker-compose.yml`. It waits until every node reports the new version healthy.
36
- 6. Operate with `tug status`, `events`, `logs`, `restart`, `rollback` and `doctor`.
69
+ 6. Operate with `tug status`, `events`, `exec`, `logs`, `ps`, `inspect`, `restart`, `rollback` and `doctor`.
37
70
 
38
71
  ## Documentation
39
72
 
40
73
  - [Architecture](docs/architecture.md): components, reconciliation, workflow diagrams, secrets and runtime paths.
41
74
  - [Compose extensions](docs/compose.md): `x-tug` placement, health checks, volumes and limitations.
42
75
  - [Control plane](docs/control-plane.md): install, TLS, registry credentials, backup and restore.
76
+ - [Backups](docs/backups.md): interim WordPress backup sidecar with restic, per-node and NFS variants, and a restore drill until `x-tug.backup` (M11).
43
77
  - [Access control](docs/access-control.md): scoped API tokens, roles, audit trail and a GitHub Actions deploy example.
44
78
  - [Networking](docs/networking.md): WireGuard mesh, service DNS, ingress and TLS, firewall notes.
45
79
  - [Nodes](docs/nodes.md): SSH install, self-service enrollment, registration and status.
@@ -47,9 +81,10 @@ The CLI installs the matching `ghcr.io/wpelevator/tug:<version>` image on your h
47
81
  - [Configuration](docs/configuration.md): CLI, control and agent environment variables.
48
82
  - [Development](docs/development.md): local setup, CI, Compose stack, host mode and the integration test.
49
83
  - [Testing](docs/testing.md): test layers, the numbers rollouts are held to, and the backlog of mocked tests.
84
+ - [Observability](docs/observability.md): what can be observed today, and the proposed logs, metrics and alerting.
50
85
  - [Releases](docs/releases.md): changesets, npm and Docker image publishing.
51
86
  - [Standalone binary](docs/standalone-binary.md): proposed Bun-compiled CLI binaries and Homebrew distribution.
52
- - [Roadmap](docs/roadmap.md): planned milestones toward parity with and beyond uncloud.
87
+ - [Roadmap](docs/roadmap.md): goals, principles, the reference WordPress topology and planned milestones.
53
88
 
54
89
  ## Development
55
90
 
package/dist/agent.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { WebSocket } from 'ws';
3
+ import type { AgentLiveCommand, AgentLiveFrame } from './protocol.ts';
4
+ import type { LiveEngineOptions } from './live.ts';
2
5
  import { parseDuration } from './util.ts';
3
6
  import type { DockerRunner, HostRunner, HttpClient } from './runners.ts';
4
- import type { Command, CommandResult, DesiredState, HealthCheck, ProjectStatus, ImageArtifact, ServiceEndpoint } from './protocol.ts';
7
+ import type { Command, CommandResult, DesiredState, HealthCheck, ProjectStatus, ImageArtifact, PortBinding, ServiceEndpoint } from './protocol.ts';
5
8
  export interface AgentContext {
6
9
  docker: DockerRunner;
7
10
  http: HttpClient;
@@ -19,13 +22,71 @@ export interface AgentContext {
19
22
  rolloutPorts?: string;
20
23
  }
21
24
  export declare function defaultContext(): AgentContext;
25
+ export type AgentLiveControlFrame = AgentLiveCommand;
26
+ export type AgentLiveOutputFrame = AgentLiveFrame;
27
+ interface LiveChannelHandlers {
28
+ onLive?: (frame: AgentLiveControlFrame) => void | Promise<void>;
29
+ onDisconnect?: () => void;
30
+ }
31
+ type WebSocketFactory = (url: string, token: string) => WebSocket;
32
+ /** Keeps one outbound stream open and falls back to a bounded HTTP wait when an intermediary rejects WebSockets. */
33
+ export declare class AgentWakeChannel {
34
+ private readonly ctx;
35
+ private readonly createSocket;
36
+ private socket?;
37
+ private cursor;
38
+ private pending;
39
+ private waiter?;
40
+ private readonly handlers;
41
+ constructor(ctx: AgentContext, createSocket?: WebSocketFactory, handlers?: LiveChannelHandlers);
42
+ private notify;
43
+ private connect;
44
+ private waitOnSocket;
45
+ private longPoll;
46
+ wait(timeoutMs: number): Promise<void>;
47
+ send(frame: AgentLiveOutputFrame): boolean;
48
+ close(): void;
49
+ }
50
+ /** Resolves a service once before starting a process so rollouts cannot silently retarget an open session. */
51
+ export declare function resolveLiveContainer(ctx: AgentContext, request: {
52
+ project: string;
53
+ service?: string;
54
+ }): Promise<string>;
55
+ export declare class AgentLiveOperations {
56
+ private readonly ctx;
57
+ private readonly emit;
58
+ private readonly engine;
59
+ private readonly sessions;
60
+ constructor(ctx: AgentContext, emit: (frame: AgentLiveOutputFrame) => void, options?: Omit<LiveEngineOptions, 'emit'>);
61
+ handle(frame: AgentLiveControlFrame): Promise<void>;
62
+ dispose(): void;
63
+ private fail;
64
+ private open;
65
+ }
22
66
  export declare function containerIp(ctx: AgentContext, project: string, service: string, cwd: string, composeFile?: string): Promise<string>;
23
67
  export declare function waitForHealth(ctx: AgentContext, project: string, service: string, health: HealthCheck | null | undefined, cwd: string, composeFile?: string): Promise<void>;
24
68
  export { parseDuration };
69
+ export interface PublicListener {
70
+ project: string;
71
+ container: string;
72
+ listen: string;
73
+ }
74
+ /**
75
+ * Containers of Tug projects (`projects`, plus their rolling revision projects `<project>-…`) that
76
+ * listen on all interfaces or on a public IP, from `docker ps --format '{{.Label "com.docker.compose.project"}}\t{{.Names}}\t{{.Ports}}'`.
77
+ */
78
+ export declare function publicListeners(psOutput: string, projects: readonly string[]): PublicListener[];
25
79
  export declare function executeCommand(command: Command, ctx?: AgentContext): Promise<CommandResult>;
26
80
  export declare function ensureArtifact(ctx: AgentContext, artifact: ImageArtifact): Promise<void>;
27
81
  export declare function pruneManagedImages(ctx: AgentContext, retained: string[]): Promise<void>;
28
- export declare function reconcile(desiredState: DesiredState, ctx?: AgentContext, endpoints?: Record<string, ServiceEndpoint[]>): Promise<ProjectStatus>;
82
+ /**
83
+ * The IP that published ports without a host IP listen on: `127.0.0.1` when the node has no
84
+ * address, the address itself when it is an IP, otherwise what it resolves to on this node.
85
+ */
86
+ export declare function resolveBindAddress(address: string | null, lookup?: (host: string) => Promise<{
87
+ address: string;
88
+ }>): Promise<string>;
89
+ export declare function reconcile(desiredState: DesiredState, ctx?: AgentContext, endpoints?: Record<string, ServiceEndpoint[]>, ports?: PortBinding): Promise<ProjectStatus>;
29
90
  /** Reports this node's WireGuard public key when control has not seen the current one yet. */
30
91
  export declare function reportWgKey(ctx: AgentContext): Promise<void>;
31
92
  export declare function pollOnce(ctx?: AgentContext): Promise<ProjectStatus[]>;