@localctl/cli 0.2.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/LICENSE +21 -0
- package/README.md +431 -0
- package/assets/manifests/namespace-addons.yaml +4 -0
- package/assets/manifests/tls-store.yaml +8 -0
- package/bin/localctl.js +2 -0
- package/package.json +54 -0
- package/schema/app.schema.json +127 -0
- package/src/commands/addons.js +56 -0
- package/src/commands/doctor.js +43 -0
- package/src/commands/down.js +35 -0
- package/src/commands/exec.js +15 -0
- package/src/commands/generateManifests.js +12 -0
- package/src/commands/hosts.js +23 -0
- package/src/commands/logs.js +45 -0
- package/src/commands/new.js +55 -0
- package/src/commands/profiles.js +180 -0
- package/src/commands/prune.js +49 -0
- package/src/commands/registryInfo.js +18 -0
- package/src/commands/reload.js +44 -0
- package/src/commands/secrets.js +75 -0
- package/src/commands/setup.js +36 -0
- package/src/commands/status.js +136 -0
- package/src/commands/uninstall.js +105 -0
- package/src/commands/up.js +123 -0
- package/src/index.js +36 -0
- package/src/lib/addonDirs.js +43 -0
- package/src/lib/addonLoader.js +190 -0
- package/src/lib/addons/cluster/logging/addon.yaml +46 -0
- package/src/lib/addons/cluster/logging/config/all-logs.json +21 -0
- package/src/lib/addons/cluster/monitoring/addon.yaml +33 -0
- package/src/lib/addons/mongo/addon.yaml +18 -0
- package/src/lib/addons/postgres/addon.yaml +39 -0
- package/src/lib/addons/redis/addon.yaml +29 -0
- package/src/lib/appSecrets.js +60 -0
- package/src/lib/clusterAddons.js +59 -0
- package/src/lib/clusterProvision.js +170 -0
- package/src/lib/configSchema.js +103 -0
- package/src/lib/constants.js +16 -0
- package/src/lib/engine.js +20 -0
- package/src/lib/exec.js +77 -0
- package/src/lib/hostsFile.js +79 -0
- package/src/lib/loadConfig.js +51 -0
- package/src/lib/logger.js +9 -0
- package/src/lib/manifestGen.js +210 -0
- package/src/lib/paths.js +41 -0
- package/src/lib/podman.js +72 -0
- package/src/lib/profileStore.js +134 -0
- package/src/lib/projectContext.js +32 -0
- package/src/lib/prompt.js +97 -0
- package/src/lib/toolInstall.js +30 -0
- package/src/lib/ui.js +50 -0
- package/src/lib/workloadReadiness.js +35 -0
- package/templates/Tiltfile.tmpl +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedro Moreira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# localctl
|
|
2
|
+
|
|
3
|
+
📖 **[Full documentation site](https://psilvmoreira.github.io/localctl/)** — same content as
|
|
4
|
+
below, plus diagrams, search, and a nicer read. Update the `USERNAME`/`repo_url` placeholders in
|
|
5
|
+
`mkdocs.yml` and this line once this repo has a real GitHub remote (see [Docs
|
|
6
|
+
Site](#docs-site) below for how it's built/deployed).
|
|
7
|
+
|
|
8
|
+
A local Kubernetes dev platform for macOS and Windows: one shared cluster, one `*.local.test`
|
|
9
|
+
subdomain per app, config-driven deploys, live-reload via Tilt, HTTPS everywhere, and a single
|
|
10
|
+
CLI — `localctl` — that owns the whole lifecycle from first install to full teardown.
|
|
11
|
+
|
|
12
|
+
Point it at any app repo, describe the app in a small JSON file, and `localctl app up` builds it,
|
|
13
|
+
deploys it, wires up its subdomain and TLS certificate, and live-syncs your code on every save.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
$ localctl app up
|
|
17
|
+
ok orders-api -> https://orders.local.test
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Table of contents
|
|
21
|
+
|
|
22
|
+
- [Features](#features)
|
|
23
|
+
- [Requirements](#requirements)
|
|
24
|
+
- [Installing the CLI](#installing-the-cli)
|
|
25
|
+
- [Quickstart: deploy your first app](#quickstart-deploy-your-first-app)
|
|
26
|
+
- [CLI reference](#cli-reference)
|
|
27
|
+
- [App configuration](#app-configuration)
|
|
28
|
+
- [Multiple projects](#multiple-projects)
|
|
29
|
+
- [Architecture](#architecture)
|
|
30
|
+
- [Debugging](#debugging)
|
|
31
|
+
- [Adding tools (databases, logging, more)](#adding-tools-databases-logging-more)
|
|
32
|
+
- [Uninstalling](#uninstalling)
|
|
33
|
+
- [Troubleshooting](#troubleshooting)
|
|
34
|
+
- [Repo layout](#repo-layout)
|
|
35
|
+
- [Documentation index](#documentation-index)
|
|
36
|
+
- [Docs Site](#docs-site)
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **One cluster, one CLI.** A single k3d (k3s-in-Docker) cluster runs every app you're developing
|
|
41
|
+
locally. No per-project Kubernetes setup, no VM sprawl.
|
|
42
|
+
- **A real subdomain per app.** `myapp.local.test`, `orders.local.test`, whatever you name it —
|
|
43
|
+
routed by Traefik, no port-juggling, no `localhost:3000` vs `localhost:3001` confusion.
|
|
44
|
+
- **HTTPS by default.** A locally-trusted wildcard certificate (via `mkcert`) is wired into the
|
|
45
|
+
cluster's ingress automatically. Every app gets a green padlock, not a browser warning.
|
|
46
|
+
- **Config-driven, not YAML-driven.** Each app repo gets one `.local/config.json`. `localctl`
|
|
47
|
+
generates every Kubernetes manifest from it — you never hand-write or hand-edit Kubernetes YAML.
|
|
48
|
+
- **Fast inner loop.** Tilt builds your image, pushes it to a local registry, deploys it, and
|
|
49
|
+
live-syncs changed files straight into the running container — no full rebuild for every edit.
|
|
50
|
+
- **Debugger-friendly.** Declare a debug port in config and it's forwarded to `localhost`
|
|
51
|
+
automatically. Node, Python, Go, Java, and .NET patterns are documented and ready to copy.
|
|
52
|
+
- **Cross-platform, including Podman.** Works with Docker Desktop, Podman Desktop, or Rancher
|
|
53
|
+
Desktop. Podman-specific quirks (BuildKit protocol, insecure-registry trust) are detected and
|
|
54
|
+
handled automatically — you don't need to know they exist.
|
|
55
|
+
- **Real Helm charts, zero JS.** Per-app databases (Postgres, Redis, MongoDB via Bitnami's own
|
|
56
|
+
charts) and cluster-wide addons (a Grafana/Loki logging stack, a Prometheus metrics stack) are
|
|
57
|
+
each just one `addon.yaml` file pointing at a published chart — adding a new one is dropping in
|
|
58
|
+
a folder, no code required. Each dependency also gets a `wait-for-<type>` init container
|
|
59
|
+
automatically, so your app never races one that isn't ready yet.
|
|
60
|
+
- **Real readiness, not just "the process started."** Every app gets a TCP (or HTTP, if you set a
|
|
61
|
+
path) readiness/liveness probe by default — `kubectl rollout status` succeeding actually means
|
|
62
|
+
it's serving.
|
|
63
|
+
- **Your own secrets, never plaintext.** API keys and tokens you supply (not addon-generated
|
|
64
|
+
credentials) go through the same real-Secret/`secretKeyRef` pipeline — `localctl app secrets
|
|
65
|
+
set/show` to write and read them, `.local/config.json` only ever holds the key name.
|
|
66
|
+
- **Clean install, clean uninstall.** Nothing is installed with `sudo`-requiring global state where
|
|
67
|
+
avoidable, and `localctl uninstall` reverses everything `localctl setup` did.
|
|
68
|
+
- **More than one project when you need it.** `default` (one shared cluster) is all most people
|
|
69
|
+
ever need, but `localctl profiles new` can spin up an isolated project - either its own separate
|
|
70
|
+
cluster, or just a namespace-scoped slice of an existing one - and directory-based resolution
|
|
71
|
+
(walk up for the nearest `.local/project.json`, same idea as `.git`/`.claude`) means `app`
|
|
72
|
+
commands just know which project they're in.
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- **macOS** or **Windows** (PowerShell)
|
|
77
|
+
- One running container engine: **Docker Desktop**, **Podman Desktop**, or **Rancher Desktop**
|
|
78
|
+
- **Git**, to clone this repo and your app repos
|
|
79
|
+
- **Homebrew** (macOS) or **winget** (Windows) — used once to install missing tools
|
|
80
|
+
|
|
81
|
+
Everything else (`k3d`, `kubectl`, `tilt`, `mkcert`, `helm`, Node.js) is installed automatically if it's
|
|
82
|
+
missing.
|
|
83
|
+
|
|
84
|
+
## Installing the CLI
|
|
85
|
+
|
|
86
|
+
**From npm (recommended):**
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
npm install -g @localctl/cli
|
|
90
|
+
localctl setup
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Pre-releases (from the `beta` branch) are published under the `beta` dist-tag: `npm install -g @localctl/cli@beta`.
|
|
94
|
+
|
|
95
|
+
**From source** (to hack on `localctl` itself):
|
|
96
|
+
|
|
97
|
+
1. Clone this repo somewhere permanent (it acts as the control plane for every app you run
|
|
98
|
+
locally, so keep it around — don't nest it inside an app repo):
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
git clone <this-repo-url> ~/dev/localctl
|
|
102
|
+
cd ~/dev/localctl
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
2. Install `localctl` as a standard global npm package:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
cd cli
|
|
109
|
+
npm install
|
|
110
|
+
npm install -g .
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> **Hit `EACCES`?** That means npm's global install directory isn't writable by your user —
|
|
114
|
+
> common on macOS when Node wasn't installed through a version manager. Fix it once, for every
|
|
115
|
+
> future global npm install, not just this one ([npm's own documented fix](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally)):
|
|
116
|
+
> ```
|
|
117
|
+
> mkdir ~/.npm-global
|
|
118
|
+
> npm config set prefix ~/.npm-global
|
|
119
|
+
> echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.zshrc # or ~/.bash_profile
|
|
120
|
+
> source ~/.zshrc
|
|
121
|
+
> ```
|
|
122
|
+
> Then re-run `npm install -g .` from `cli/`.
|
|
123
|
+
|
|
124
|
+
3. Run the actual infra setup:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
localctl setup
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This detects your container engine, installs `k3d`/`kubectl`/`tilt`/`mkcert`/`helm` if missing,
|
|
131
|
+
creates the local cluster, starts the local image registry, generates and trusts the
|
|
132
|
+
`*.local.test` TLS certificate, and syncs your hosts file.
|
|
133
|
+
|
|
134
|
+
You'll see up to two password prompts during this step — both expected, both one-time:
|
|
135
|
+
- **mkcert** asking to trust its local certificate authority (macOS Touch ID/password prompt)
|
|
136
|
+
- **sudo/Administrator** asking to write the `*.local.test` entries to your hosts file
|
|
137
|
+
|
|
138
|
+
4. Confirm it worked:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
localctl doctor
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Every line should read `[ok]`.
|
|
145
|
+
|
|
146
|
+
From here on, `localctl` is a normal globally-installed command:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
localctl setup # re-check or repair the infra at any time — fully idempotent
|
|
150
|
+
localctl uninstall # tear the infra back down (see Uninstalling below)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary>Alternative: install without touching npm's global config</summary>
|
|
155
|
+
|
|
156
|
+
If you'd rather not change npm's global prefix, `bin/bootstrap.sh` (macOS) / `bootstrap.ps1`
|
|
157
|
+
(Windows) install `localctl` as a self-contained PATH shim in `~/.localctl/bin` instead of a real
|
|
158
|
+
npm global package — no `npm install -g`, no `EACCES` possible, same `localctl` command either
|
|
159
|
+
way:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
./bin/bootstrap.sh # macOS — installs the shim, then runs `localctl setup` for you
|
|
163
|
+
./bin/bootstrap.ps1 # Windows (PowerShell)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Uninstall the shim version with `./bin/uninstall.sh` / `uninstall.ps1` instead of
|
|
167
|
+
`npm uninstall -g` (see [Uninstalling](#uninstalling)).
|
|
168
|
+
</details>
|
|
169
|
+
|
|
170
|
+
## Quickstart: deploy your first app
|
|
171
|
+
|
|
172
|
+
Try it immediately with the bundled example, no app of your own required:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
cd examples/node-app
|
|
176
|
+
localctl app up
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`localctl app up` runs in the background by default — it prints the app's real URL once the
|
|
180
|
+
deployment is ready and hands control of the terminal straight back to you, so closing that
|
|
181
|
+
terminal doesn't stop the app:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
$ localctl app up
|
|
185
|
+
> Waiting for the deployment to become ready...
|
|
186
|
+
deployment "node-app" successfully rolled out
|
|
187
|
+
|
|
188
|
+
ok node-app -> https://node-app.local.test
|
|
189
|
+
debug: localhost:9229
|
|
190
|
+
logs: localctl app logs node-app -f
|
|
191
|
+
status: localctl app status
|
|
192
|
+
stop: localctl app down
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Open <https://node-app.local.test>. Live-sync keeps working in the background exactly as if it
|
|
196
|
+
were running in the foreground — edit a file under any path listed in `"sync"` and it updates
|
|
197
|
+
without a full rebuild, as long as something inside the container watches for the change and
|
|
198
|
+
restarts (`nodemon`, `uvicorn --reload`, etc. — see [Debugging](#debugging) and
|
|
199
|
+
`examples/node-app/Dockerfile` for a working pattern).
|
|
200
|
+
|
|
201
|
+
Want the interactive Tilt UI instead (build logs streaming live in your terminal)? Add `-f`:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
localctl app up -f
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
To use it with your own app:
|
|
208
|
+
|
|
209
|
+
1. From inside your app's repo:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
localctl app new
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
This scaffolds `.local/config.json` and a generic `Tiltfile`. `localctl app new` asks for an app
|
|
216
|
+
name and port; edit `.local/config.json` afterward for anything else (env vars, database
|
|
217
|
+
dependencies, debug settings — see [App configuration](#app-configuration)).
|
|
218
|
+
|
|
219
|
+
2. Deploy it: `localctl app up` (see above).
|
|
220
|
+
|
|
221
|
+
3. Check on it any time. Run from inside the app's own repo (or `-a <name>` from anywhere) for
|
|
222
|
+
that app's detail:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
$ localctl app status
|
|
226
|
+
node-app
|
|
227
|
+
namespace: node-app
|
|
228
|
+
ready: 1/1
|
|
229
|
+
url: https://node-app.local.test
|
|
230
|
+
debug: localhost:9229
|
|
231
|
+
dev loop: running (pid 4815)
|
|
232
|
+
dependencies:
|
|
233
|
+
node-app-postgres 1/1
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
From anywhere else (or with `-A`/`--all`), it's a table of every app:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
$ localctl app status
|
|
240
|
+
APP NAMESPACE READY URL DEV LOOP
|
|
241
|
+
node-app node-app 1/1 https://node-app.local.test running (pid 4815)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
4. When you're done:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
localctl app down
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Stops the background dev loop (if any) and tears down just this app; the shared cluster keeps
|
|
251
|
+
running for everything else.
|
|
252
|
+
|
|
253
|
+
## CLI reference
|
|
254
|
+
|
|
255
|
+
| Command | Purpose |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `localctl setup` | Install/repair the `default` project's infra (container engine, cluster, TLS, hosts). Idempotent. |
|
|
258
|
+
| `localctl uninstall [-y]` | Remove everything `setup`/`profiles` set up: every project's cluster, registries, hosts entries, certs/state. |
|
|
259
|
+
| `localctl doctor` | Quick health check of the active project's environment. |
|
|
260
|
+
| `localctl app new` | Scaffold `.local/config.json` + `Tiltfile` in the current app repo. |
|
|
261
|
+
| `localctl app up [-f]` | Build, deploy, and live-reload the app in the current directory. Runs detached in the background by default (survives closing the terminal) and prints the app's URL once ready; `-f`/`--foreground` runs the interactive Tilt UI instead. |
|
|
262
|
+
| `localctl app down` | Stop the background dev loop (if any) and tear down the app deployed from the current directory. |
|
|
263
|
+
| `localctl app reload [-a <name>]` | Force an immediate rebuild/redeploy of a running background dev loop, without waiting on a file change. |
|
|
264
|
+
| `localctl app status [-a <name>] [-A]` | App detail (readiness, URL, debug port, dependencies, dev-loop state) when run inside an app repo or with `-a <name>`; a table of every app otherwise, or always with `-A`/`--all`. |
|
|
265
|
+
| `localctl app logs <name> [-f]` | Tail logs for a deployed app. |
|
|
266
|
+
| `localctl app exec [cmd...]` | Shell into the running app's pod (`kubectl exec`); defaults to `sh`. |
|
|
267
|
+
| `localctl app prune [-y]` | Permanently delete an already-torn-down app's leftover data (dependency PVCs, secrets) - `app down` deliberately leaves these alone. |
|
|
268
|
+
| `localctl app secrets set <KEY> [value]` | Set your own secret value (hidden prompt if not given inline) - stored as a real k8s Secret, never in `.local/config.json`. |
|
|
269
|
+
| `localctl app secrets list` / `show [KEY]` / `unset <KEY>` | List which keys are set, reveal actual value(s), or remove one. |
|
|
270
|
+
| `localctl hosts sync` / `list` | Manage the `*.local.test` entries in your hosts file. |
|
|
271
|
+
| `localctl addons list` / `enable <name>` / `disable <name>` | Manage cluster-wide addons (e.g. `logging`, `monitoring`). |
|
|
272
|
+
| `localctl addons status` | Every addon instance in the active project's cluster — per-app dependencies and cluster-wide addons — shown separately from your actual apps. |
|
|
273
|
+
| `localctl profiles new <name>` | Create a project - its own cluster (Main project), or namespace-scoped under an existing one. |
|
|
274
|
+
| `localctl profiles list` | List every project this machine knows about, nested under its Main (cluster) project. |
|
|
275
|
+
| `localctl profiles switch <name>` | Make a project active - brings its cluster up if needed, applies its exclusive-teardown setting. |
|
|
276
|
+
| `localctl profiles status` | Show the currently active project. |
|
|
277
|
+
|
|
278
|
+
Run `localctl <command> --help` for options on any of these.
|
|
279
|
+
|
|
280
|
+
## App configuration
|
|
281
|
+
|
|
282
|
+
Everything about how an app deploys locally lives in that app repo's `.local/config.json`:
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"name": "orders-api",
|
|
287
|
+
"subdomain": "orders",
|
|
288
|
+
"port": 3000,
|
|
289
|
+
"build": { "context": ".", "dockerfile": "Dockerfile" },
|
|
290
|
+
"env": { "NODE_ENV": "development" },
|
|
291
|
+
"debug": { "enabled": true, "port": 9229, "type": "node" },
|
|
292
|
+
"dependencies": [{ "type": "postgres", "database": "orders" }],
|
|
293
|
+
"sync": [{ "localPath": "./src", "remotePath": "/app/src" }],
|
|
294
|
+
"tls": true
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
This resolves to `https://orders.local.test`, with a dedicated Postgres instance deployed
|
|
299
|
+
alongside it. Point your editor's JSON schema support at `schema/app.schema.json` for
|
|
300
|
+
autocomplete and validation as you type — `localctl app new` sets this up automatically via
|
|
301
|
+
`"$schema"`.
|
|
302
|
+
|
|
303
|
+
Full field-by-field reference: **[docs/config-schema.md](./docs/config-schema.md)**.
|
|
304
|
+
|
|
305
|
+
## Multiple projects
|
|
306
|
+
|
|
307
|
+
`default` is one shared cluster for everything - fine until you need real isolation (a second
|
|
308
|
+
client's stack, a "staging-like" environment, or just apps you don't want sharing a cluster with
|
|
309
|
+
your main work). From the folder you want it to apply to:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
localctl profiles new staging
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Choose either your own cluster (a Main project) or a namespace-scoped slice of an existing one, and
|
|
316
|
+
it writes `.local/project.json` there - every app under that folder, and any subfolder below it,
|
|
317
|
+
now resolves to `staging` automatically, the same directory walk-up `.git`/`.claude` use.
|
|
318
|
+
`localctl profiles list` / `status` / `switch <name>` manage which project is current.
|
|
319
|
+
|
|
320
|
+
Full mechanism (exclusive vs. concurrent clusters, namespace scoping, port handling):
|
|
321
|
+
**[docs/architecture.md#multi-project-support](./docs/architecture.md#multi-project-support)**.
|
|
322
|
+
|
|
323
|
+
## Architecture
|
|
324
|
+
|
|
325
|
+
k3d cluster (Traefik ingress + local registry) → Tilt (build, push, live-sync) → `localctl`
|
|
326
|
+
(config validation, manifest generation, infra lifecycle) → your app repo's
|
|
327
|
+
`.local/config.json`. Nothing about an app's Kubernetes shape is hand-written; it's all generated
|
|
328
|
+
from that one file by `cli/src/lib/manifestGen.js`.
|
|
329
|
+
|
|
330
|
+
Full breakdown of every component and why it was chosen (including the Podman-specific handling):
|
|
331
|
+
**[docs/architecture.md](./docs/architecture.md)**.
|
|
332
|
+
|
|
333
|
+
## Debugging
|
|
334
|
+
|
|
335
|
+
Set `"debug": { "enabled": true, "port": <port>, "type": "<node|python|go|java|dotnet>" }` in
|
|
336
|
+
`.local/config.json` and the port is forwarded to `localhost` automatically while `localctl app up` is
|
|
337
|
+
running — attach your debugger like you would to any local process. Per-language Dockerfile and
|
|
338
|
+
`launch.json` patterns (including the live-reload watcher each language needs — `nodemon`,
|
|
339
|
+
`uvicorn --reload`, `air`, Spring Boot DevTools): **[docs/debugging.md](./docs/debugging.md)**.
|
|
340
|
+
|
|
341
|
+
## Adding tools (databases, logging, more)
|
|
342
|
+
|
|
343
|
+
Every addon is one folder with one `addon.yaml` pointing at a real, published Helm chart — no JS.
|
|
344
|
+
|
|
345
|
+
- **Per-app dependencies** (Postgres, Redis, MongoDB today, via Bitnami's own charts) are declared
|
|
346
|
+
in an app's `"dependencies"` array and deployed alongside it, isolated per app, with connection
|
|
347
|
+
details (host, port, credentials via a real k8s Secret) auto-injected into its container.
|
|
348
|
+
- **Cluster-wide addons**: `logging` (Grafana Labs' own loki-stack chart, at
|
|
349
|
+
`https://grafana.local.test` once enabled, dashboard auto-provisioned from a file in the addon's
|
|
350
|
+
`config/` folder) and `monitoring` (prometheus-community's own Prometheus chart, at
|
|
351
|
+
`https://monitoring.local.test`) - shared infrastructure turned on once via `localctl addons
|
|
352
|
+
enable <name>`.
|
|
353
|
+
|
|
354
|
+
Adding a new one — a message broker, a tracing backend, whatever's next — means finding its Helm
|
|
355
|
+
chart and writing one `addon.yaml`: **[docs/adding-tools.md](./docs/adding-tools.md)**.
|
|
356
|
+
|
|
357
|
+
## Uninstalling
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
localctl uninstall # every project's cluster/registry, Podman VM config, hosts entries, certs/state
|
|
361
|
+
npm uninstall -g @localctl/cli # + the CLI itself, if you installed it via npm
|
|
362
|
+
./bin/uninstall.sh # or uninstall.ps1 # + the CLI itself, if you installed it via bin/bootstrap.sh's shim
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Prompts for confirmation (skip with `-y`/`--yes`); every step is best-effort and safe to re-run.
|
|
366
|
+
Deliberately left untouched: `k3d`/`kubectl`/`tilt`/`mkcert`/`node` themselves and mkcert's root CA
|
|
367
|
+
trust (other projects on your machine may depend on either) — both are printed with the exact
|
|
368
|
+
command to remove them yourself if you want them gone too.
|
|
369
|
+
|
|
370
|
+
Full details: **[docs/uninstalling.md](./docs/uninstalling.md)**.
|
|
371
|
+
|
|
372
|
+
## Troubleshooting
|
|
373
|
+
|
|
374
|
+
Hit something unexpected — a Podman-specific build/push error, a port conflict, a certificate
|
|
375
|
+
warning, `ImagePullBackOff`, stale live-sync? Check
|
|
376
|
+
**[docs/troubleshooting.md](./docs/troubleshooting.md)** first; it covers every issue found while
|
|
377
|
+
building and testing this repo, with the actual cause and fix for each.
|
|
378
|
+
|
|
379
|
+
## Repo layout
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
localctl/
|
|
383
|
+
├── bin/ thin installer stubs (macOS + Windows) - install Node/the CLI, then
|
|
384
|
+
│ hand off to `localctl setup`/`localctl uninstall`
|
|
385
|
+
├── cluster/ k3d cluster definition + cluster-level manifests (TLS store, namespaces)
|
|
386
|
+
├── cli/ the localctl Node.js CLI
|
|
387
|
+
│ ├── src/commands/ one file per CLI subcommand (setup, uninstall, up, new, profiles, ...)
|
|
388
|
+
│ ├── src/lib/ config validation, manifest generation, project/cluster/Podman/hosts handling
|
|
389
|
+
│ │ └── addons/ per-app (postgres/redis/mongo) + cluster/ (logging, monitoring) addons
|
|
390
|
+
│ └── templates/ generic Tiltfile copied into app repos by `localctl app new`
|
|
391
|
+
├── schema/ JSON Schema for .local/config.json (editor autocomplete)
|
|
392
|
+
├── examples/ ready-to-run sample apps (Node.js + Postgres, Python + Redis, .NET)
|
|
393
|
+
└── docs/ full documentation - also published as the docs site, see below
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Documentation index
|
|
397
|
+
|
|
398
|
+
- [docs/index.md](./docs/index.md) — documentation site home
|
|
399
|
+
- [docs/getting-started.md](./docs/getting-started.md) — the install/quickstart flow, in more detail
|
|
400
|
+
- [docs/architecture.md](./docs/architecture.md) — every component and why it was chosen, with diagrams
|
|
401
|
+
- [docs/config-schema.md](./docs/config-schema.md) — full `.local/config.json` reference
|
|
402
|
+
- [docs/adding-tools.md](./docs/adding-tools.md) — add new per-app or cluster-wide addons
|
|
403
|
+
- [docs/debugging.md](./docs/debugging.md) — attach a debugger, per language
|
|
404
|
+
- [docs/uninstalling.md](./docs/uninstalling.md) — full teardown reference
|
|
405
|
+
- [docs/troubleshooting.md](./docs/troubleshooting.md) — known issues and fixes
|
|
406
|
+
|
|
407
|
+
## Docs Site
|
|
408
|
+
|
|
409
|
+
Everything under `docs/` is also published as a [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
|
|
410
|
+
site — search, dark/light mode, and the Mermaid diagrams in [Architecture](./docs/architecture.md)
|
|
411
|
+
rendered properly (GitHub renders Mermaid too, but the site's theme is nicer for reading).
|
|
412
|
+
|
|
413
|
+
**One-time setup**, once this repo has a real GitHub remote:
|
|
414
|
+
|
|
415
|
+
1. In `mkdocs.yml`, replace every `USERNAME` with your GitHub username (or org), and in this
|
|
416
|
+
README's link at the top.
|
|
417
|
+
2. Push to GitHub, then in the repo's **Settings → Pages**, set **Source** to **Deploy from a
|
|
418
|
+
branch** and **Branch** to **`gh-pages`** / **`/ (root)`**. (The first push of
|
|
419
|
+
`.github/workflows/docs.yml` below creates that branch for you — the option won't appear in the
|
|
420
|
+
dropdown until after that first run.)
|
|
421
|
+
|
|
422
|
+
From then on, **every push to `master`** rebuilds and redeploys the site automatically
|
|
423
|
+
(`.github/workflows/docs.yml`) — nothing to run by hand.
|
|
424
|
+
|
|
425
|
+
To preview locally before pushing:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
429
|
+
pip install mkdocs-material
|
|
430
|
+
mkdocs serve # http://127.0.0.1:8000, live-reloads on save
|
|
431
|
+
```
|
package/bin/localctl.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@localctl/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "localctl - local Kubernetes dev platform CLI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kubernetes",
|
|
7
|
+
"k3d",
|
|
8
|
+
"tilt",
|
|
9
|
+
"local-development",
|
|
10
|
+
"cli",
|
|
11
|
+
"devtools"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://psilvmoreira.github.io/localctl/",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/psilvmoreira/localctl/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/psilvmoreira/localctl.git",
|
|
20
|
+
"directory": "cli"
|
|
21
|
+
},
|
|
22
|
+
"author": "Pedro Moreira",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"bin": {
|
|
25
|
+
"localctl": "bin/localctl.js"
|
|
26
|
+
},
|
|
27
|
+
"main": "src/index.js",
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"src/",
|
|
31
|
+
"templates/",
|
|
32
|
+
"assets/",
|
|
33
|
+
"schema/"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"sync-assets": "node scripts/sync-assets.js",
|
|
37
|
+
"prepack": "node scripts/sync-assets.js",
|
|
38
|
+
"test": "node scripts/smoke-test.js",
|
|
39
|
+
"test:pack": "node scripts/pack-test.js"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^4.1.2",
|
|
43
|
+
"commander": "^11.1.0",
|
|
44
|
+
"js-yaml": "^4.1.0",
|
|
45
|
+
"zod": "^3.23.8"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "localctl app config",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["name", "port", "build"],
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": { "type": "string" },
|
|
9
|
+
"name": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^[a-z0-9-]+$",
|
|
12
|
+
"description": "Lowercase alphanumeric + dashes. Used as Deployment/Service name and Docker image name."
|
|
13
|
+
},
|
|
14
|
+
"subdomain": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[a-z0-9-]+$",
|
|
17
|
+
"description": "Defaults to name. App is reachable at <subdomain>.local.test"
|
|
18
|
+
},
|
|
19
|
+
"namespace": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Defaults to name."
|
|
22
|
+
},
|
|
23
|
+
"port": { "type": "integer", "description": "Port the app listens on inside the container." },
|
|
24
|
+
"build": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"context": { "type": "string", "default": "." },
|
|
28
|
+
"dockerfile": { "type": "string", "default": "Dockerfile" }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"env": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": { "type": "string" }
|
|
34
|
+
},
|
|
35
|
+
"secrets": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"description": "Names only - values are never stored here. Set with \"localctl app secrets set <KEY>\"; injected via secretKeyRef, same as an addon's credentials. A name can't also appear in \"env\".",
|
|
38
|
+
"items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" }
|
|
39
|
+
},
|
|
40
|
+
"replicas": { "type": "integer", "minimum": 1, "default": 1 },
|
|
41
|
+
"resources": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"requests": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": { "cpu": { "type": "string" }, "memory": { "type": "string" } }
|
|
47
|
+
},
|
|
48
|
+
"limits": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": { "cpu": { "type": "string" }, "memory": { "type": "string" } }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"debug": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"enabled": { "type": "boolean", "default": false },
|
|
58
|
+
"port": { "type": "integer" },
|
|
59
|
+
"type": { "type": "string", "enum": ["node", "python", "go", "java", "dotnet"] }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"probes": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"description": "Defaults to a plain TCP check on \"port\" for both if omitted entirely. Set \"path\" to use an HTTP GET check instead.",
|
|
65
|
+
"properties": {
|
|
66
|
+
"readiness": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"path": { "type": "string" },
|
|
70
|
+
"initialDelaySeconds": { "type": "integer" },
|
|
71
|
+
"periodSeconds": { "type": "integer" }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"liveness": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"properties": {
|
|
77
|
+
"path": { "type": "string" },
|
|
78
|
+
"initialDelaySeconds": { "type": "integer" },
|
|
79
|
+
"periodSeconds": { "type": "integer" }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"items": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"required": ["type"],
|
|
89
|
+
"properties": {
|
|
90
|
+
"type": { "type": "string", "enum": ["postgres", "redis", "mongo"] },
|
|
91
|
+
"version": { "type": "string" },
|
|
92
|
+
"database": { "type": "string" },
|
|
93
|
+
"storage": { "type": "string", "description": "e.g. 1Gi" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"sidecars": {
|
|
98
|
+
"type": "array",
|
|
99
|
+
"description": "Extra containers in the same pod as your app (the k8s sidecar pattern) - reachable from your app via localhost:<port>, no Service needed.",
|
|
100
|
+
"items": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"required": ["name", "image"],
|
|
103
|
+
"properties": {
|
|
104
|
+
"name": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
105
|
+
"image": { "type": "string" },
|
|
106
|
+
"port": { "type": "integer" },
|
|
107
|
+
"command": { "type": "array", "items": { "type": "string" } },
|
|
108
|
+
"args": { "type": "array", "items": { "type": "string" } },
|
|
109
|
+
"env": { "type": "object", "additionalProperties": { "type": "string" } }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"sync": {
|
|
114
|
+
"type": "array",
|
|
115
|
+
"description": "Paths live-synced into the running container by Tilt without a full rebuild.",
|
|
116
|
+
"items": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"required": ["localPath", "remotePath"],
|
|
119
|
+
"properties": {
|
|
120
|
+
"localPath": { "type": "string" },
|
|
121
|
+
"remotePath": { "type": "string" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"tls": { "type": "boolean", "default": true }
|
|
126
|
+
}
|
|
127
|
+
}
|