forje-cloud 0.1.0__tar.gz

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.
Files changed (29) hide show
  1. forje_cloud-0.1.0/.gitignore +35 -0
  2. forje_cloud-0.1.0/LICENSE +21 -0
  3. forje_cloud-0.1.0/PKG-INFO +384 -0
  4. forje_cloud-0.1.0/README.md +354 -0
  5. forje_cloud-0.1.0/pyproject.toml +64 -0
  6. forje_cloud-0.1.0/src/forje_cloud/__init__.py +8 -0
  7. forje_cloud-0.1.0/src/forje_cloud/cli.py +508 -0
  8. forje_cloud-0.1.0/src/forje_cloud/core/__init__.py +22 -0
  9. forje_cloud-0.1.0/src/forje_cloud/core/context.py +229 -0
  10. forje_cloud-0.1.0/src/forje_cloud/core/hashlog.py +165 -0
  11. forje_cloud-0.1.0/src/forje_cloud/core/runner.py +252 -0
  12. forje_cloud-0.1.0/src/forje_cloud/core/step.py +201 -0
  13. forje_cloud-0.1.0/src/forje_cloud/docgen.py +294 -0
  14. forje_cloud-0.1.0/src/forje_cloud/restore.py +225 -0
  15. forje_cloud-0.1.0/src/forje_cloud/services/__init__.py +1 -0
  16. forje_cloud-0.1.0/src/forje_cloud/services/analyzer.py +183 -0
  17. forje_cloud-0.1.0/src/forje_cloud/services/cloudflare.py +244 -0
  18. forje_cloud-0.1.0/src/forje_cloud/services/llm.py +163 -0
  19. forje_cloud-0.1.0/src/forje_cloud/services/telegram.py +112 -0
  20. forje_cloud-0.1.0/src/forje_cloud/steps/__init__.py +56 -0
  21. forje_cloud-0.1.0/src/forje_cloud/steps/backup.py +582 -0
  22. forje_cloud-0.1.0/src/forje_cloud/steps/docker.py +195 -0
  23. forje_cloud-0.1.0/src/forje_cloud/steps/edge.py +370 -0
  24. forje_cloud-0.1.0/src/forje_cloud/steps/hardening.py +172 -0
  25. forje_cloud-0.1.0/src/forje_cloud/steps/monitoring.py +247 -0
  26. forje_cloud-0.1.0/src/forje_cloud/steps/system.py +119 -0
  27. forje_cloud-0.1.0/src/forje_cloud/ui.py +261 -0
  28. forje_cloud-0.1.0/src/forje_cloud/wizard.py +265 -0
  29. forje_cloud-0.1.0/tests/test_forje_cloud.py +731 -0
@@ -0,0 +1,35 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+
7
+ # Build
8
+ build/
9
+ dist/
10
+ PKG-INFO
11
+
12
+ # Environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Secrets
18
+ .env
19
+ *.pem
20
+ *.key
21
+
22
+ # Testing
23
+ .pytest_cache/
24
+ .coverage
25
+ htmlcov/
26
+
27
+ # Editors / OS
28
+ .vscode/
29
+ .idea/
30
+ .DS_Store
31
+
32
+ # forje-cloud output
33
+ INFRASTRUCTURE.md
34
+ recovery-compose.yml
35
+ runs.jsonl
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matheus Rodrigues Trindade
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.
@@ -0,0 +1,384 @@
1
+ Metadata-Version: 2.4
2
+ Name: forje-cloud
3
+ Version: 0.1.0
4
+ Summary: Idempotent server provisioning: Docker Swarm, Traefik and Cloudflare Tunnel, with rollback, backups and AI-explained alerts.
5
+ Author-email: Matheus Rodrigues Trindade <contact@forjelo.com>
6
+ Maintainer-email: Matheus Rodrigues Trindade <contact@forjelo.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: backup,cloudflare,cloudflared,devops,docker,homelab,idempotent,infrastructure,monitoring,provisioning,self-hosted,swarm,traefik,tunnel,vps
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
23
+ Classifier: Topic :: System :: Installation/Setup
24
+ Classifier: Topic :: System :: Systems Administration
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # forje-cloud
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/forje-cloud.svg)](https://pypi.org/project/forje-cloud/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/forje-cloud.svg)](https://pypi.org/project/forje-cloud/)
35
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
36
+ [![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen.svg)](#no-dependencies)
37
+ [![Platform](https://img.shields.io/badge/platform-Debian%20%7C%20Ubuntu-orange.svg)](#requirements)
38
+ [![Status](https://img.shields.io/badge/status-preview-yellow.svg)](#status-of-this-release)
39
+
40
+ **A production server, from a bare Debian box, in one command.**
41
+
42
+ Containers, HTTPS, backups, monitoring and hardening — provisioned, verified,
43
+ and documented. Every step checks what already exists and leaves it alone.
44
+
45
+ ```bash
46
+ pip install forje-cloud
47
+ forje-cloud setup
48
+ ```
49
+
50
+ ---
51
+
52
+ ## What you get
53
+
54
+ A server that publishes applications over HTTPS **without opening a single
55
+ inbound port**.
56
+
57
+ | | |
58
+ |---|---|
59
+ | **Docker Swarm** | Encrypted secrets, rolling updates, room to add machines. |
60
+ | **Traefik** | Routes hostnames to containers, using labels. |
61
+ | **Cloudflare Tunnel** | A `cloudflared` container holds one outbound connection that carries all inbound traffic. The firewall stays closed. |
62
+ | **Wildcard DNS + TLS** | `*.example.com` resolves and is certificated once. Every application deployed afterwards is a routing label and nothing else. |
63
+ | **Local image registry** | Build on the machine, deploy from the machine. |
64
+ | **Backups to Cloudflare R2** | Volumes, databases and images — off the machine, supervised, with retention. |
65
+ | **AI-explained monitoring** | Logs are classified; anything serious is explained by Claude or GPT and delivered to Telegram. |
66
+ | **Hardening** | Firewall, intrusion blocking, key-only SSH. |
67
+ | **Tamper-evident audit log** | Every entry carries the hash of the one before it. |
68
+ | **Self-documenting** | Every run writes an `INFRASTRUCTURE.md` describing the machine as it is. |
69
+
70
+ ---
71
+
72
+ ## Why
73
+
74
+ Provisioning scripts assume a clean machine. Real machines are never clean.
75
+
76
+ Docker is installed, but not in Swarm mode. A reverse proxy is running, but as a
77
+ plain container. A tunnel was set up months ago and nobody wants to touch it.
78
+ The deploy user exists but is not in the `docker` group.
79
+
80
+ Running a bootstrap script on that machine is a gamble: it either fails on the
81
+ first step that was already done, or it redoes the step and takes down whatever
82
+ was working.
83
+
84
+ `forje-cloud` checks before it acts. Every step answers *"does this already
85
+ exist, and is it correct?"* before touching anything.
86
+
87
+ ---
88
+
89
+ ## How traffic reaches an application
90
+
91
+ Nothing listens on a public port. A container named `cloudflared` runs on the
92
+ host network and opens a single outbound connection to Cloudflare. Inbound
93
+ requests travel back down that connection.
94
+
95
+ ```
96
+ browser ──HTTPS──> Cloudflare ──tunnel──> cloudflared ──HTTP──> Traefik ──> app
97
+ │ │
98
+ TLS ends here outbound only,
99
+ no port is opened
100
+ ```
101
+
102
+ **TLS terminates at Cloudflare.** Inside the machine everything speaks plain
103
+ HTTP, so Traefik never holds a certificate, never contacts a certificate
104
+ authority, and never needs port 443.
105
+
106
+ Provisioning wires this together in three parts:
107
+
108
+ 1. **The tunnel** is created through the Cloudflare API, and its ingress rules
109
+ are configured to forward to `http://127.0.0.1:80` — the port Traefik
110
+ publishes on the loopback address. A tunnel with no ingress rules
111
+ authenticates, reports healthy, and routes nothing; configuring them is what
112
+ makes it carry traffic.
113
+
114
+ 2. **Two DNS records** are created, both pointing at the tunnel: the apex
115
+ (`example.com`) and the wildcard (`*.example.com`). The wildcard is the
116
+ reason a new application needs no DNS work at all — any subdomain already
117
+ resolves, and already has a certificate.
118
+
119
+ 3. **The `cloudflared` container** receives the connector token as a root-only
120
+ file rather than a command-line flag, so `docker inspect` and `ps` cannot
121
+ read it.
122
+
123
+ ### Deploying an application afterwards
124
+
125
+ Once the tunnel and the wildcard exist, publishing a new application is five
126
+ labels. No DNS record, no certificate, no port.
127
+
128
+ ```yaml
129
+ services:
130
+ myapp:
131
+ image: 127.0.0.1:5000/myapp:20260711-1420
132
+ networks: [edge]
133
+ deploy:
134
+ # Traefik reads these from the Swarm service, not the container.
135
+ labels:
136
+ - "traefik.enable=true"
137
+ - "traefik.swarm.network=edge"
138
+ - "traefik.http.routers.myapp.rule=Host(`myapp.example.com`)"
139
+ - "traefik.http.routers.myapp.entrypoints=web"
140
+ - "traefik.http.services.myapp.loadbalancer.server.port=8000"
141
+
142
+ networks:
143
+ edge:
144
+ external: true
145
+ ```
146
+
147
+ `docker stack deploy`, and `https://myapp.example.com` is live.
148
+
149
+ ---
150
+
151
+ ## Guided setup
152
+
153
+ ```bash
154
+ forje-cloud setup
155
+ ```
156
+
157
+ Every question explains what the value is for, in one line, without jargon.
158
+ Every section can be skipped — skipping backups means no backups, not a crash.
159
+ Nothing is executed until the configuration is shown and confirmed.
160
+
161
+ ```
162
+ ── Public access (Cloudflare) ────────────────────────────
163
+ Cloudflare puts your apps online without opening any port on
164
+ this machine. It also provides HTTPS certificates automatically.
165
+
166
+ ? Publish this server through Cloudflare? [Y/n]
167
+
168
+ ? Domain
169
+ A domain you own, already added to Cloudflare. Example: example.com
170
+ >
171
+ ```
172
+
173
+ ---
174
+
175
+ ## The other commands
176
+
177
+ ```bash
178
+ forje-cloud status # what this machine already has
179
+ forje-cloud plan # what a run would change, without changing it
180
+ forje-cloud apply # provision from flags and environment, no prompts
181
+ forje-cloud restore # bring back volumes, databases and images
182
+ forje-cloud compose # reconstruct a Compose file for every running workload
183
+ forje-cloud verify # has the audit log been tampered with?
184
+ forje-cloud docs # regenerate the infrastructure document
185
+ ```
186
+
187
+ A run on an already-provisioned machine:
188
+
189
+ ```
190
+ • hostname already 'example-host'
191
+ • deploy-user account 'deploy' exists
192
+ • docker Docker version 28.0.0
193
+ ✔ docker-group added 'deploy' to the docker group
194
+ ✔ swarm initialized (advertise address 192.0.2.10)
195
+ • network overlay 'edge' exists
196
+ ▲ traefik running traefik:v2.11, expected traefik:v3.3
197
+ ```
198
+
199
+ `•` already present · `✔` configured now · `▲` **exists but diverges**
200
+
201
+ ---
202
+
203
+ ## Drift is not absence
204
+
205
+ Most tools treat "wrong" and "missing" as the same thing, and overwrite both.
206
+
207
+ They are not the same thing. Traefik v2 and v3 read different label syntax: v3
208
+ uses a dedicated `swarm` provider and reads `traefik.swarm.network`, where v2
209
+ used `docker` with `swarmMode: true` and read `traefik.docker.network`. The two
210
+ are not interchangeable — v3 labels on a v2 proxy are ignored, which leaves an
211
+ application running but unrouted, with no error anywhere.
212
+
213
+ So a proxy on the wrong major version is not a gap to fill. It is a running
214
+ system whose every routing label would have to be rewritten at once. That is a
215
+ migration, and a migration is a decision.
216
+
217
+ `forje-cloud` reports drift and stops.
218
+
219
+ ---
220
+
221
+ ## Backups
222
+
223
+ A backup stored on the machine it protects is not a backup. Archives are
224
+ uploaded to Cloudflare R2, off the machine.
225
+
226
+ Three things are captured, because restoring any two of them is not enough:
227
+
228
+ - **Volumes** — archived with `zstd`, which is faster and smaller than gzip.
229
+ - **Databases** — dumped with each engine's own tool. Postgres, MySQL, MariaDB,
230
+ MongoDB and Redis are recognised by image family, so `postgres:16-alpine` and
231
+ `pgvector/pgvector` are both handled. Copying live database files instead can
232
+ catch them mid-write, producing an archive that will not restore.
233
+ - **Images** — the local registry. Without them, a rebuilt machine has stack
234
+ files referencing images that exist nowhere, and every application must be
235
+ rebuilt from source before it can start.
236
+
237
+ The service is supervised: a failed cycle is retried, and if the process dies
238
+ Swarm restarts it. The schedule is configurable — daily by default, with
239
+ retention in days.
240
+
241
+ Every backup carries a manifest naming what it holds, so restore reads it rather
242
+ than inferring intent from filenames.
243
+
244
+ ### Restoring
245
+
246
+ ```bash
247
+ forje-cloud restore
248
+ ```
249
+
250
+ Pick a backup, then pick what to bring back — by number, by range, or `all`.
251
+ Recovering a machine that ran eight workloads means dozens of volumes, and
252
+ restoring them one command at a time is where mistakes happen.
253
+
254
+ ### The Compose file nobody kept
255
+
256
+ ```bash
257
+ forje-cloud compose
258
+ ```
259
+
260
+ Servers accumulate stacks deployed from files that were never committed
261
+ anywhere. This reads the running services back out of Swarm and reconstructs a
262
+ single Compose file covering every workload — images, volumes, networks,
263
+ secrets, routing labels.
264
+
265
+ A copy is written into every backup. After a total loss: restore the archives,
266
+ deploy the file, and the machine is back.
267
+
268
+ ---
269
+
270
+ ## Monitoring
271
+
272
+ Container logs are read on a schedule. Lines are matched against patterns and
273
+ scored for severity — out-of-memory kills, disk exhaustion, crash loops,
274
+ certificate failures, authentication attempts.
275
+
276
+ Only findings above the threshold are escalated. Those go to Claude or GPT,
277
+ which answers three questions: **what happened, what likely caused it, and what
278
+ to do about it.** The answer arrives on Telegram.
279
+
280
+ Raw logs never leave the machine. Only the classified findings are sent, which
281
+ are smaller, cheaper, and do not carry whatever a log line happened to contain.
282
+
283
+ Identical alerts are suppressed for an hour, so one incident produces one alert
284
+ rather than fifty.
285
+
286
+ ---
287
+
288
+ ## An audit log you can trust
289
+
290
+ Every entry carries the hash of the entry before it.
291
+
292
+ ```bash
293
+ $ forje-cloud verify
294
+ ✖ audit log entry #4 (step=firewall) was modified: hash does not match
295
+ ```
296
+
297
+ Editing or deleting any past record breaks the chain. This does not prevent
298
+ tampering; it makes tampering evident.
299
+
300
+ ---
301
+
302
+ ## It documents itself
303
+
304
+ Every run writes `INFRASTRUCTURE.md`: a diagram of how traffic reaches an
305
+ application, what is running, what an application must declare to be deployed
306
+ here, and what must never appear in its Compose file.
307
+
308
+ Written from the results of the run, so it describes the machine as it is.
309
+
310
+ ---
311
+
312
+ ## Safety
313
+
314
+ **Closing SSH requires a working tunnel.** The step refuses otherwise. Closing
315
+ the only way in, with no working alternative, cannot be undone — the channel
316
+ needed to revert is the one being closed.
317
+
318
+ **Destructive steps require confirmation.** Always explicit, never implied.
319
+
320
+ **Credentials are never written down.** The sudo password is passed on stdin,
321
+ never in `argv` (visible through `ps`) and never in the environment (visible
322
+ through `/proc`). Service credentials become Docker secrets, encrypted at rest.
323
+ Nothing is written to the audit log or the generated document.
324
+
325
+ **Sudo is scoped.** The deploy account gets `NOPASSWD` for an enumerated list of
326
+ commands, never `ALL`.
327
+
328
+ **Configuration is validated before it is loaded.** `sshd -t` and `visudo -cf`
329
+ run first. An invalid file is reverted, not applied.
330
+
331
+ ---
332
+
333
+ ## Requirements
334
+
335
+ - A Debian-based Linux machine — Debian or Ubuntu. VPS or bare metal.
336
+ - Python 3.9 or newer.
337
+ - Root, or an account with `sudo`.
338
+ - Optional, for public access: a domain on Cloudflare.
339
+ - Optional, for backups: a Cloudflare R2 bucket.
340
+ - Optional, for alerts: a Telegram bot and an Anthropic or OpenAI API key.
341
+
342
+ ## No dependencies
343
+
344
+ Standard library only. A tool that provisions a machine should not require the
345
+ machine to be provisioned first.
346
+
347
+ ---
348
+
349
+ ## Status of this release
350
+
351
+ **v0.1.0 is published for review, not for production use.**
352
+
353
+ It ships with a passing test suite and code that is complete and, as far as the
354
+ suite reaches, correct. What it has not yet had is a full run against a real VPS,
355
+ from a clean install through to a working tunnel. Until that has happened,
356
+ calling it ready would be a claim this release cannot support.
357
+
358
+ So it is published as what it is: a first cut, open for inspection, while
359
+ validation on real hardware is underway.
360
+
361
+ **v0.1.1 will be the first release intended for real use.** It follows shortly.
362
+
363
+ Read the code. Run the tests. Send corrections. Just don't point it at a machine
364
+ that matters yet.
365
+
366
+ ---
367
+
368
+ ## Contributing
369
+
370
+ Contributions are welcome and actively wanted — corrections, criticism of the
371
+ design, or an account of what happened when the tool met a real machine.
372
+
373
+ A public repository is not yet available, so the way in is email:
374
+
375
+ - **contact@forjelo.com**
376
+ - **matheusrodriguestrindade3@gmail.com**
377
+
378
+ Patches, findings and hard questions all land in the same place.
379
+
380
+ ---
381
+
382
+ ## License
383
+
384
+ MIT © Matheus Rodrigues Trindade