|
1 | | -# OpenCtrl |
| 1 | +<p align="center"> |
| 2 | + <img src="assets/openctrl.png" alt="OpenCtrl banner" width="50%"> |
| 3 | +</p> |
| 4 | + |
| 5 | +<p align="center"> |
| 6 | + <a href="docs/master.md"><img alt="API v2" src="https://img.shields.io/badge/API-v2-14b8a6?style=flat-square"></a> |
| 7 | + <a href="docs/master.md"><img alt="Events SSE" src="https://img.shields.io/badge/events-SSE-f59e0b?style=flat-square"></a> |
| 8 | + <img alt="Go 1.26.4" src="https://img.shields.io/badge/Go-1.26.4-00ADD8?style=flat-square&logo=go&logoColor=white"> |
| 9 | + <a href="LICENSE"><img alt="License BSD 3-Clause" src="https://img.shields.io/badge/license-BSD--3--Clause-0f172a?style=flat-square"></a> |
| 10 | +</p> |
| 11 | + |
| 12 | +OpenCtrl is a focused control-plane master for supervising URL-defined |
| 13 | +instances. It stores instance definitions, launches a compatible runtime binary |
| 14 | +for each managed instance, exposes a versioned REST API, and streams live |
| 15 | +lifecycle, log, and metric updates over Server-Sent Events. |
| 16 | + |
| 17 | +It is intentionally narrow: the master owns orchestration, persistence, |
| 18 | +authentication, and observability; runtime binaries own protocol-specific work. |
| 19 | + |
| 20 | +## Contents |
| 21 | + |
| 22 | +- [Highlights](#highlights) |
| 23 | +- [Architecture](#architecture) |
| 24 | +- [Quick Start](#quick-start) |
| 25 | +- [Master URL](#master-url) |
| 26 | +- [API Surface](#api-surface) |
| 27 | +- [Runtime Contract](#runtime-contract) |
| 28 | +- [State and Security](#state-and-security) |
| 29 | +- [Release](#release) |
| 30 | +- [Documentation](#documentation) |
| 31 | + |
| 32 | +## Highlights |
| 33 | + |
| 34 | +| Capability | What it provides | |
| 35 | +| --- | --- | |
| 36 | +| URL-defined instances | Every managed workload is represented by one URL and one generated instance ID. | |
| 37 | +| Process supervision | The master starts, stops, restarts, and deletes child processes through a stable runtime contract. | |
| 38 | +| Versioned control API | REST endpoints are served under `/api/v2` or a custom versioned prefix. | |
| 39 | +| Live event stream | SSE publishes initial state, lifecycle changes, child logs, metric checkpoints, deletions, and shutdown events. | |
| 40 | +| Durable local state | Instance definitions and the API key are persisted beside the executable with a backup file. | |
| 41 | +| Runtime neutrality | The master does not special-case child protocols; compatible binaries validate and execute their own URLs. | |
| 42 | +| TLS-ready operation | HTTP, self-signed TLS, and file-backed TLS certificate modes are selected from the master URL. | |
| 43 | + |
| 44 | +## Architecture |
| 45 | + |
| 46 | +```mermaid |
| 47 | +flowchart LR |
| 48 | + client["API clients"] |
| 49 | + events["SSE subscribers"] |
| 50 | + master["OpenCtrl master"] |
| 51 | + state["gob state"] |
| 52 | + runtime["runtime binary"] |
| 53 | + instA["instance URL"] |
| 54 | + instB["instance URL"] |
| 55 | +
|
| 56 | + client -->|"REST + X-API-Key"| master |
| 57 | + master -->|"event: instance"| events |
| 58 | + master <-->|"load / save"| state |
| 59 | + master -->|"spawn <runtime> <url>"| runtime |
| 60 | + runtime --> instA |
| 61 | + runtime --> instB |
| 62 | +``` |
| 63 | + |
| 64 | +The master process provides the control plane. Child processes stay in the |
| 65 | +foreground, emit logs and optional checkpoints, and exit when the master stops |
| 66 | +them. |
| 67 | + |
| 68 | +## Quick Start |
| 69 | + |
| 70 | +Build the master: |
| 71 | + |
| 72 | +```sh |
| 73 | +go build -trimpath -o ./bin/openctrl ./cmd/openctrl |
| 74 | +``` |
| 75 | + |
| 76 | +Start a local control plane: |
| 77 | + |
| 78 | +```sh |
| 79 | +./bin/openctrl 'master://127.0.0.1:8080' |
| 80 | +``` |
| 81 | + |
| 82 | +The master prints an API key on startup: |
| 83 | + |
| 84 | +```text |
| 85 | +Master.run: API key created: <api-key> |
| 86 | +``` |
| 87 | + |
| 88 | +Use the key with the versioned API: |
| 89 | + |
| 90 | +```sh |
| 91 | +BASE='http://127.0.0.1:8080/api/v2' |
| 92 | +API_KEY='<api-key>' |
| 93 | + |
| 94 | +curl -H "X-API-Key: ${API_KEY}" "${BASE}/info" |
| 95 | +curl -H "X-API-Key: ${API_KEY}" "${BASE}/instances" |
| 96 | +``` |
| 97 | + |
| 98 | +Run managed instances through a compatible runtime binary: |
| 99 | + |
| 100 | +```sh |
| 101 | +./bin/openctrl 'master://127.0.0.1:8080?bin=/opt/openctrl/runtime' |
| 102 | +``` |
| 103 | + |
| 104 | +Create an instance: |
| 105 | + |
| 106 | +```sh |
| 107 | +curl -X POST "${BASE}/instances" \ |
| 108 | + -H "X-API-Key: ${API_KEY}" \ |
| 109 | + -H "Content-Type: application/json" \ |
| 110 | + -d '{"alias":"edge-a","url":"managed://edge-a"}' |
| 111 | +``` |
| 112 | + |
| 113 | +## Master URL |
| 114 | + |
| 115 | +```text |
| 116 | +master://<host>:<port>[/prefix][?<query>] |
| 117 | +``` |
| 118 | + |
| 119 | +The host and port become the listen address. The optional path becomes the API |
| 120 | +prefix, and the API version is appended automatically. |
| 121 | + |
| 122 | +| Master URL | API base | |
| 123 | +| --- | --- | |
| 124 | +| `master://127.0.0.1:8080` | `http://127.0.0.1:8080/api/v2` | |
| 125 | +| `master://127.0.0.1:8080/control` | `http://127.0.0.1:8080/control/v2` | |
| 126 | +| `master://127.0.0.1:8443?tls=1` | `https://127.0.0.1:8443/api/v2` | |
| 127 | + |
| 128 | +| Parameter | Values | Default | Purpose | |
| 129 | +| --- | --- | --- | --- | |
| 130 | +| `tls` | `0`, `1`, `2` | `0` | Select HTTP, self-signed TLS, or certificate-backed TLS. | |
| 131 | +| `crt` | file path | empty | Certificate file used with `tls=2`. | |
| 132 | +| `key` | file path | empty | Private key file used with `tls=2`. | |
| 133 | +| `bin` | file path | current executable | Runtime binary used to launch managed instances. | |
| 134 | + |
| 135 | +## API Surface |
| 136 | + |
| 137 | +All protected requests must include: |
| 138 | + |
| 139 | +```text |
| 140 | +X-API-Key: <api-key> |
| 141 | +``` |
| 142 | + |
| 143 | +Core endpoints: |
| 144 | + |
| 145 | +| Method | Path | Description | |
| 146 | +| --- | --- | --- | |
| 147 | +| `GET` | `/info` | Return master identity, runtime configuration, uptime, and system metrics. | |
| 148 | +| `POST` | `/info` | Set the master alias. | |
| 149 | +| `GET` | `/instances` | Return all instances, including the internal API-key instance. | |
| 150 | +| `POST` | `/instances` | Create and asynchronously start an instance. | |
| 151 | +| `GET` | `/instances/{id}` | Return one instance. | |
| 152 | +| `PATCH` | `/instances/{id}` | Update metadata, restart policy, or lifecycle action. | |
| 153 | +| `PUT` | `/instances/{id}` | Replace the instance URL and restart the instance. | |
| 154 | +| `DELETE` | `/instances/{id}` | Stop and delete the instance. | |
| 155 | +| `GET` | `/events` | Open the Server-Sent Events stream. | |
| 156 | +| `GET` | `/tcping?target=host:port` | Test TCP reachability from the master process. | |
| 157 | + |
| 158 | +Lifecycle actions accepted by `PATCH /instances/{id}`: |
| 159 | + |
| 160 | +| Action | Effect | |
| 161 | +| --- | --- | |
| 162 | +| `start` | Start a stopped child process. | |
| 163 | +| `stop` | Stop an active child process. | |
| 164 | +| `restart` | Stop and start the child process. | |
| 165 | +| `reset` | Reset runtime byte counters. | |
| 166 | + |
| 167 | +SSE frames use the event name `instance`: |
| 168 | + |
| 169 | +```text |
| 170 | +event: instance |
| 171 | +data: {"type":"update","time":"2026-06-08T12:00:00Z","instance":{...},"logs":""} |
| 172 | +``` |
| 173 | + |
| 174 | +Event payload types are `initial`, `create`, `update`, `delete`, `log`, and |
| 175 | +`shutdown`. |
| 176 | + |
| 177 | +## Runtime Contract |
| 178 | + |
| 179 | +OpenCtrl supervises runtime binaries with a small process contract: |
| 180 | + |
| 181 | +```text |
| 182 | +<runtime-binary> <instance-url> |
| 183 | +``` |
| 184 | + |
| 185 | +For non-`master` instance schemes, point `bin` at a binary that understands |
| 186 | +those schemes. The master treats child URLs as opaque runtime configuration. |
| 187 | + |
| 188 | +Runtime binaries MUST: |
| 189 | + |
| 190 | +- accept the full instance URL as the first positional argument; |
| 191 | +- reject invalid configuration before starting long-running work; |
| 192 | +- stay in the foreground while the instance is active; |
| 193 | +- handle termination and exit within the master's 5-second grace period when |
| 194 | + possible; |
| 195 | +- return a non-zero exit code for failures that should place the instance in |
| 196 | + `error`; |
| 197 | +- avoid printing secrets from URLs, headers, keys, or tokens. |
| 198 | + |
| 199 | +Runtime binaries SHOULD: |
| 200 | + |
| 201 | +- emit newline-delimited logs to stdout or stderr; |
| 202 | +- emit checkpoints after the service is ready when metrics are available; |
| 203 | +- keep byte counters monotonic for the life of the process. |
| 204 | + |
| 205 | +Runtime binaries MAY: |
| 206 | + |
| 207 | +- define their own URL schemes and query parameters; |
| 208 | +- omit checkpoints when metrics are not meaningful; |
| 209 | +- print ordinary operational logs that should be visible to API clients. |
| 210 | + |
| 211 | +Checkpoint format: |
| 212 | + |
| 213 | +```text |
| 214 | +CHECK_POINT|MODE=<n>|PING=<n>ms|POOL=<n>|TCPS=<n>|UDPS=<n>|TCPRX=<bytes>|TCPTX=<bytes>|UDPRX=<bytes>|UDPTX=<bytes> |
| 215 | +``` |
| 216 | + |
| 217 | +Non-checkpoint child output is forwarded to stdout and published as `log` |
| 218 | +events. If a line contains the substring `ERROR`, the instance is marked |
| 219 | +`error`. |
| 220 | + |
| 221 | +## State and Security |
| 222 | + |
| 223 | +State is stored beside the executable: |
| 224 | + |
| 225 | +```text |
| 226 | +<executable-directory>/gob/openctrl.gob |
| 227 | +<executable-directory>/gob/openctrl.gob.backup |
| 228 | +``` |
| 229 | + |
| 230 | +Operational notes: |
| 231 | + |
| 232 | +- protect the API key printed by the master; |
| 233 | +- protect the state directory because `openctrl.gob` stores the API key; |
| 234 | +- use `tls=2` with a trusted certificate outside local-only deployments; |
| 235 | +- use a trusted `bin` path because managed instances execute that binary; |
| 236 | +- treat instance URLs as secrets when they contain credentials or tokens; |
| 237 | +- account for permissive CORS headers when exposing the API across origins. |
| 238 | + |
| 239 | +## Release |
| 240 | + |
| 241 | +Build a local binary: |
| 242 | + |
| 243 | +```sh |
| 244 | +go build -trimpath -o ./bin/openctrl ./cmd/openctrl |
| 245 | +``` |
| 246 | + |
| 247 | +Embed a version string: |
| 248 | + |
| 249 | +```sh |
| 250 | +go build -trimpath \ |
| 251 | + -ldflags="-s -w -X main.version=$VERSION" \ |
| 252 | + -o ./bin/openctrl ./cmd/openctrl |
| 253 | +``` |
| 254 | + |
| 255 | +Tagged releases are built by GoReleaser for Darwin, FreeBSD, Linux, and |
| 256 | +Windows on `amd64` and `arm64`. |
| 257 | + |
| 258 | +## Documentation |
| 259 | + |
| 260 | +- [Master API and runtime contract](docs/master.md) |
| 261 | + |
| 262 | +## License |
| 263 | + |
| 264 | +OpenCtrl is released under the BSD 3-Clause License. |
0 commit comments