first commit to git
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Shared
|
||||
APP_ENV=development
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# HTTP services
|
||||
BACKEND_ADDR=:8080
|
||||
WORKER_CONCURRENCY=5
|
||||
CONSOLE_PROXY_ADDR=:8081
|
||||
|
||||
# Supabase/Postgres
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable
|
||||
SUPABASE_JWKS_URL=http://localhost:54321/auth/v1/.well-known/jwks.json
|
||||
SUPABASE_ISSUER=http://localhost:54321/auth/v1
|
||||
|
||||
# Redis/asynq
|
||||
REDIS_ADDR=localhost:6379
|
||||
|
||||
# Secrets
|
||||
# 32-byte key, base64 encoded. Replace locally.
|
||||
MASTER_KEY_BASE64=
|
||||
|
||||
# Frontend
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
VITE_SUPABASE_URL=http://localhost:54321
|
||||
VITE_SUPABASE_ANON_KEY=
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS/editor
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Go
|
||||
bin/
|
||||
coverage.out
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Local data
|
||||
.tmp/
|
||||
data/
|
||||
@@ -0,0 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- E0-T01 begonnen: Monorepo-Grundstruktur, Root-Dokumentation, Env-Beispiel und minimale Service-Startpunkte angelegt.
|
||||
- Annahme dokumentiert: vorlaeufiger Modulpfad ist `proxui`.
|
||||
@@ -0,0 +1,33 @@
|
||||
# ProxUI
|
||||
|
||||
Multi-Tenant-Konsole fuer Proxmox auf Basis von Go, Supabase Postgres/Auth, Redis/asynq und React.
|
||||
|
||||
## Architektur
|
||||
|
||||
- `backend/`: HTTP-API, Auth/JWT-Validierung, RBAC, DB-Zugriff und Proxmox-Orchestrierung
|
||||
- `worker/`: asynchrone Jobs fuer Proxmox-Tasks, Reconciliation und Cleanup
|
||||
- `console-proxy/`: Websocket-Proxy fuer noVNC/xterm.js ohne direkte Proxmox-Verbindung im Browser
|
||||
- `frontend/`: React/TypeScript/Vite-App fuer Kunden- und Admin-Workflows
|
||||
- `migrations/`: versionierte SQL-Migrationen
|
||||
- `deploy/`: lokale und spaetere Deployment-Artefakte
|
||||
|
||||
## Lokales Setup
|
||||
|
||||
1. Go >= 1.22 installieren.
|
||||
2. Node.js >= 20 und npm installieren.
|
||||
3. Docker Desktop oder kompatible Docker Engine installieren.
|
||||
4. `.env.example` nach `.env` kopieren und lokale Werte setzen.
|
||||
5. Abhaengigkeiten installieren: `npm install --prefix frontend`.
|
||||
6. Go-Abhaengigkeiten installieren: `go work sync`.
|
||||
7. Backend starten: `go run ./backend/cmd/api`.
|
||||
8. Console-Proxy starten: `go run ./console-proxy/cmd/console-proxy`.
|
||||
9. Frontend starten: `npm run dev --prefix frontend`.
|
||||
|
||||
`make dev`, `make migrate`, `make test` und `make lint` werden mit E0-T02 angelegt, sobald die lokale Docker-Umgebung steht.
|
||||
|
||||
## Sicherheitsregeln
|
||||
|
||||
- Nur Backend, Worker und Console-Proxy sprechen mit Proxmox.
|
||||
- Proxmox-Tokens werden nie im Frontend sichtbar und nie unverschluesselt gespeichert.
|
||||
- Tenant-bezogene Queries muessen immer tenant-gefiltert sein.
|
||||
- Secrets gehoeren in `.env` oder einen Secret-Manager, nie ins Repo.
|
||||
@@ -0,0 +1,46 @@
|
||||
# TODO
|
||||
|
||||
Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsdatei bleibt unveraendert; Fortschritt und Annahmen werden hier gepflegt.
|
||||
|
||||
## In Arbeit
|
||||
|
||||
- [x] E0-T01: Monorepo-Grundgeruest
|
||||
- [x] Ordner fuer `backend/`, `worker/`, `console-proxy/`, `frontend/`, `migrations/`, `deploy/` angelegt
|
||||
- [x] Root-Dateien `README.md`, `CHANGELOG.md`, `.gitignore`, `.env.example` angelegt
|
||||
- [x] Minimale Startpunkte fuer Backend, Worker, Console-Proxy und Frontend angelegt
|
||||
- [x] Lokale Toolchain geprueft: Go 1.26.4 ist im PATH
|
||||
- [x] Erste Build-/Test-Kommandos ausgefuehrt
|
||||
|
||||
## Naechste Aufgaben
|
||||
|
||||
- [ ] E0-T02: Lokale Dev-Umgebung mit Docker Compose, Supabase-nahem Postgres/Auth-Setup und Redis
|
||||
- [ ] E0-T03: CI-Pipeline mit Build, Lint, Tests und Secret-Scan
|
||||
- [ ] E0-T04: Konfigurations- und Logging-Layer fuer Go-Dienste
|
||||
- [ ] E2-T01: Migrations-Setup mit `golang-migrate`
|
||||
|
||||
## MVP-Backlog
|
||||
|
||||
- [ ] E1: Supabase Auth, JWT-Middleware und Profil-Sync
|
||||
- [ ] E2: Datenmodell, Migrationen, RLS und VMID-Allokator
|
||||
- [ ] E3: Envelope-Encryption, Cluster-Repository und Proxmox-Client
|
||||
- [ ] E4: RBAC, Policy-Funktion und Autorisierungs-Middleware
|
||||
- [ ] E6: Worker-Grundgeruest und UPID-Polling
|
||||
- [ ] E5: VM-Liste, Detail, Power-Aktionen und Reconciliation
|
||||
- [ ] E7: SSH-Keys, Templates und Provisioning
|
||||
- [ ] E8: Konsolen-Tickets und Websocket-Proxy
|
||||
- [ ] E9: Audit-Writer und Audit-Anzeige
|
||||
- [ ] E10: Frontend-MVP fuer Auth, Projekte, VMs, Wizard und Konsole
|
||||
|
||||
## Annahmen
|
||||
|
||||
- Modulpfad vorlaeufig: `proxui`. Falls spaeter ein Git-Remote/Org-Name feststeht, wird der Go-Modulpfad angepasst.
|
||||
- Bis die Supabase-CLI-Konfiguration festgelegt ist, startet die lokale Datenbank zuerst als normaler Postgres-Service plus Redis.
|
||||
- UI-Texte werden Deutsch, Code-Identifier Englisch.
|
||||
|
||||
## Verifikation
|
||||
|
||||
- 2026-06-09: `go work sync` erfolgreich mit lokalem `GOCACHE=.tmp/go-build`.
|
||||
- 2026-06-09: `go test ./backend/... ./worker/... ./console-proxy/...` erfolgreich; aktuell keine Testdateien vorhanden.
|
||||
- 2026-06-09: `go build ./backend/cmd/api ./worker/cmd/worker ./console-proxy/cmd/console-proxy` erfolgreich.
|
||||
- 2026-06-09: `npm install --prefix frontend` erfolgreich; 0 Vulnerabilities.
|
||||
- 2026-06-09: `npm run build --prefix frontend` erfolgreich.
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := getenv("BACKEND_ADDR", ":8080")
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":"ok","service":"backend"}`))
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
go func() {
|
||||
logger.Info("backend listening", "addr", addr)
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.Error("backend failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Error("backend shutdown failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module proxui/backend
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := getenv("CONSOLE_PROXY_ADDR", ":8081")
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":"ok","service":"console-proxy"}`))
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
go func() {
|
||||
logger.Info("console proxy listening", "addr", addr)
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.Error("console proxy failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Error("console proxy shutdown failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module proxui/console-proxy
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxUI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3172
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "proxui-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@supabase/supabase-js": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./styles.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<section className="panel">
|
||||
<p className="eyebrow">ProxUI</p>
|
||||
<h1>Proxmox Multi-Tenant Console</h1>
|
||||
<p>
|
||||
Grundgeruest fuer Login, Projekte, VM-Verwaltung und Web-Konsole.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
:root {
|
||||
color: #172026;
|
||||
background: #f6f8fb;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: min(720px, 100%);
|
||||
border: 1px solid #d7dee8;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
color: #1f6f8b;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/main.tsx"],"version":"5.9.3"}
|
||||
@@ -0,0 +1,9 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
# Proxmox Multi-Tenant Console — Entwicklungsplan
|
||||
|
||||
Plan zur Übergabe an einen ausführenden AI-Agenten. Jede Aufgabe ist so geschnitten, dass sie eigenständig umsetzbar, testbar und reviewbar ist. Arbeite die Epics in Reihenfolge ab; innerhalb eines Epics geben die `Abhängigkeiten` die Reihenfolge vor.
|
||||
|
||||
---
|
||||
|
||||
## 0. Stack-Entscheidungen (verbindlich)
|
||||
|
||||
Diese Entscheidungen sind getroffen. Nicht abweichen, ohne Rückfrage.
|
||||
|
||||
| Bereich | Entscheidung |
|
||||
|---|---|
|
||||
| Auth | Supabase Auth (E-Mail/Passwort, OAuth, TOTP/2FA) |
|
||||
| Datenbank | Supabase Postgres (direkter Postgres-Zugriff, **nicht** über PostgREST) |
|
||||
| Backend | **Go** (>= 1.22), HTTP-Framework: `chi`; DB-Zugriff: `pgx` + `sqlc` für typsichere Queries |
|
||||
| Migrations | `golang-migrate` (SQL-Files, versioniert) |
|
||||
| Worker / Queue | Eigener Go-Worker + `asynq` (Redis-basiert) |
|
||||
| Konsolen-Proxy | Eigener Go-Dienst (Websocket), `nhooyr.io/websocket` |
|
||||
| Proxmox-Client | Eigener dünner HTTP-Client (keine fertige Lib zwingend; falls Lib, dann nur als Referenz) |
|
||||
| Frontend | React + TypeScript, Vite, TanStack Query, Tailwind, shadcn/ui |
|
||||
| Konsole-Rendering | noVNC (VNC) bzw. xterm.js (Serial) |
|
||||
| Secrets | Master-Key aus ENV/Secret-Manager; Cluster-Tokens **verschlüsselt** in DB (AES-GCM, Envelope) |
|
||||
|
||||
> Falls Python statt Go gewünscht ist: ersetze die Go-spezifischen Schritte sinngemäß (FastAPI + SQLAlchemy/asyncpg + Celery), die Architektur bleibt identisch.
|
||||
|
||||
### Architektur-Grundregeln (gelten für ALLE Aufgaben)
|
||||
|
||||
1. **Nur das Backend spricht mit Proxmox.** Kein Proxmox-Token, kein Proxmox-Hostname erreicht jemals das Frontend.
|
||||
2. **Supabase liefert Identität, das Backend macht Autorisierung.** JWT wird gegen Supabase-JWKS validiert; Rollen/Rechte kommen aus eigenen Tabellen.
|
||||
3. **Source of Truth ist die eigene DB.** Proxmox ist Ausführungsebene. Ein Reconciliation-Job gleicht Drift ab.
|
||||
4. **Multi-Cluster von Anfang an.** Alle Proxmox-Zugriffe gehen über eine `cluster_id`.
|
||||
5. **Jede tenant-bezogene Query trägt `WHERE tenant_id = $1`.** RLS bleibt zusätzlich aktiv (Defense in depth).
|
||||
6. **Sensible Felder (Token, Secrets) niemals in DTOs/API-Responses mappen.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Konventionen für den Agenten
|
||||
|
||||
- **Sprache:** Code-Identifier und Commits auf Englisch; UI-Texte auf Deutsch.
|
||||
- **Branches:** ein Branch pro Aufgabe, Name = Task-ID (z.B. `E2-T03-schema-tenants`).
|
||||
- **Commits:** Conventional Commits (`feat:`, `fix:`, `chore:`, `test:`).
|
||||
- **Definition of Done pro Task:** Code + Tests grün + Lint sauber + kurze Notiz in `CHANGELOG.md`, was umgesetzt wurde und welche Annahmen getroffen wurden.
|
||||
- **Keine echten Credentials im Repo.** `.env.example` pflegen, echte `.env` in `.gitignore`, Secret-Scanner in CI.
|
||||
- **Tests:** Unit-Tests für Krypto/Auth/Autorisierung sind Pflicht. Für Proxmox-Calls einen Mock-Server bauen (kein Live-Cluster in CI).
|
||||
- **Bei Unklarheit:** Annahme dokumentieren und weiterarbeiten, nicht blockieren — außer es betrifft Sicherheit (dann stoppen und nachfragen).
|
||||
|
||||
---
|
||||
|
||||
## EPIC E0 — Projekt-Setup & Infrastruktur
|
||||
|
||||
### E0-T01 — Monorepo-Grundgerüst
|
||||
- **Ziel:** Repo-Struktur stehen.
|
||||
- **Schritte:** Ordner `backend/`, `worker/`, `console-proxy/`, `frontend/`, `migrations/`, `deploy/`. Root-`README.md`, `CHANGELOG.md`, `.gitignore`, `.env.example`.
|
||||
- **Akzeptanz:** Repo klont sich, `README` erklärt Setup in <10 Schritten.
|
||||
- **Abhängigkeiten:** keine.
|
||||
|
||||
### E0-T02 — Lokale Dev-Umgebung
|
||||
- **Ziel:** Alles lokal startbar.
|
||||
- **Schritte:** `docker-compose.yml` mit Supabase CLI Stack (Postgres + Auth + Studio) und Redis. Makefile-Targets: `make dev`, `make migrate`, `make test`, `make lint`.
|
||||
- **Akzeptanz:** `make dev` bringt Postgres, Auth, Redis hoch; Studio erreichbar.
|
||||
- **Abhängigkeiten:** E0-T01.
|
||||
|
||||
### E0-T03 — CI-Pipeline
|
||||
- **Ziel:** Automatische Qualitätssicherung.
|
||||
- **Schritte:** GitHub Actions (oder gewähltes CI): Build, Lint (`golangci-lint`, `eslint`), Tests, Secret-Scan (`gitleaks`).
|
||||
- **Akzeptanz:** PR ohne grüne CI ist nicht mergebar.
|
||||
- **Abhängigkeiten:** E0-T01.
|
||||
|
||||
### E0-T04 — Konfigurations- & Logging-Layer
|
||||
- **Ziel:** Saubere Config und strukturierte Logs.
|
||||
- **Schritte:** Config aus ENV (z.B. `envconfig`), validiert beim Start. Strukturiertes Logging (`slog`), Request-IDs, keine Secrets in Logs.
|
||||
- **Akzeptanz:** Fehlt eine Pflicht-ENV, bricht der Start mit klarer Meldung ab.
|
||||
- **Abhängigkeiten:** E0-T01.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E1 — Auth & JWT-Middleware
|
||||
|
||||
### E1-T01 — Supabase-Projekt & Auth-Konfiguration
|
||||
- **Ziel:** Auth nutzbar.
|
||||
- **Schritte:** Supabase-Projekt anlegen (oder CLI lokal). E-Mail/Passwort aktivieren, mind. einen OAuth-Provider, TOTP/2FA aktivieren. JWKS-URL und Issuer notieren.
|
||||
- **Akzeptanz:** Test-User kann sich registrieren und JWT erhalten.
|
||||
- **Abhängigkeiten:** E0-T02.
|
||||
|
||||
### E1-T02 — JWT-Validierungs-Middleware (Backend)
|
||||
- **Ziel:** Eingehende Requests authentifizieren.
|
||||
- **Schritte:** Middleware, die `Authorization: Bearer <jwt>` liest, Signatur gegen Supabase-JWKS prüft (JWKS cachen + periodisch refreshen), Issuer/Expiry validiert. `sub`-Claim (= Supabase-User-ID) in den Request-Context legen. Bei ungültig: 401.
|
||||
- **Akzeptanz:** Unit-Test mit gültigem, abgelaufenem und manipuliertem Token (jeweils korrektes Verhalten). Geschützter Endpunkt ohne Token → 401.
|
||||
- **Abhängigkeiten:** E1-T01.
|
||||
|
||||
### E1-T03 — Profil-Sync (auth.users → public.profiles)
|
||||
- **Ziel:** Eigene Nutzer-Entität verknüpft mit Supabase.
|
||||
- **Schritte:** Beim ersten authentifizierten Request: falls kein `profiles`-Eintrag zur `sub`-ID existiert, anlegen (lazy provisioning). Alternativ Supabase DB-Trigger auf `auth.users`. Eine Variante wählen und dokumentieren.
|
||||
- **Akzeptanz:** Nach Erstlogin existiert genau ein `profiles`-Datensatz.
|
||||
- **Abhängigkeiten:** E1-T02, E2-T02.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E2 — Datenmodell & Migrations
|
||||
|
||||
> Alle Tabellen ins `public`-Schema. `profiles.id` referenziert `auth.users.id`. RLS auf allen tenant-bezogenen Tabellen aktivieren.
|
||||
|
||||
### E2-T01 — Migrations-Setup
|
||||
- **Ziel:** Versionierte Schema-Änderungen.
|
||||
- **Schritte:** `golang-migrate` einbinden, `make migrate` / `make migrate-down`. Namens-Schema `NNNN_beschreibung.up.sql` / `.down.sql`.
|
||||
- **Akzeptanz:** Up und Down laufen idempotent durch.
|
||||
- **Abhängigkeiten:** E0-T02.
|
||||
|
||||
### E2-T02 — Kern-Tabellen: profiles, tenants, memberships
|
||||
- **Ziel:** Mandanten- und Nutzergrundlage.
|
||||
- **Schritte:**
|
||||
- `profiles` (id = auth.users.id, email, display_name, created_at)
|
||||
- `tenants` (id, name, slug, status, created_at)
|
||||
- `memberships` (profile_id, tenant_id, role enum: owner|admin|member|viewer, UNIQUE(profile_id, tenant_id))
|
||||
- **Akzeptanz:** Migration läuft; FKs und Indizes gesetzt; ein User kann mehreren Tenants angehören.
|
||||
- **Abhängigkeiten:** E2-T01.
|
||||
|
||||
### E2-T03 — Projekte & Quotas
|
||||
- **Ziel:** Projektebene wie bei Hetzner.
|
||||
- **Schritte:**
|
||||
- `projects` (id, tenant_id FK, name, created_at)
|
||||
- `project_quotas` (project_id FK, max_vcpu, max_ram_mb, max_disk_gb, max_vms)
|
||||
- **Akzeptanz:** Projekte gehören eindeutig zu einem Tenant; Quota-Defaults gesetzt.
|
||||
- **Abhängigkeiten:** E2-T02.
|
||||
|
||||
### E2-T04 — Clusters-Tabelle (verschlüsselte Credentials)
|
||||
- **Ziel:** Multi-Cluster-Zugriff sicher.
|
||||
- **Schritte:**
|
||||
- `clusters` (id, name, api_endpoint, tls_fingerprint, `encrypted_token` bytea, token_id text, status, created_at)
|
||||
- Klartext-Token **nie** speichern. Verschlüsselung kommt aus E3.
|
||||
- **Akzeptanz:** Spalte `encrypted_token` ist `bytea`; kein Klartext-Feld vorhanden.
|
||||
- **Abhängigkeiten:** E2-T01.
|
||||
|
||||
### E2-T05 — VMs & Ressourcen-Mapping
|
||||
- **Ziel:** Eigene Sicht auf VMs (Source of Truth).
|
||||
- **Schritte:**
|
||||
- `vms` (id, project_id FK, cluster_id FK, proxmox_vmid int, node text, name, status, vcpu, ram_mb, disk_gb, created_at)
|
||||
- `ssh_keys` (id, tenant_id FK, name, public_key, created_at)
|
||||
- Index auf (cluster_id, proxmox_vmid) UNIQUE.
|
||||
- **Akzeptanz:** VM ist eindeutig einem Projekt und einem Cluster zugeordnet.
|
||||
- **Abhängigkeiten:** E2-T03, E2-T04.
|
||||
|
||||
### E2-T06 — VMID-Allokator
|
||||
- **Ziel:** Race-freie, clusterweit eindeutige VMID-Vergabe.
|
||||
- **Schritte:** Tabelle oder Sequence pro Cluster; Vergabe in einer Transaktion mit `SELECT ... FOR UPDATE` oder Postgres-Sequence. Vergebene IDs reservieren, bevor Proxmox-Call startet.
|
||||
- **Akzeptanz:** Concurrency-Test mit 50 parallelen Anfragen erzeugt keine Doppel-VMID.
|
||||
- **Abhängigkeiten:** E2-T05.
|
||||
|
||||
### E2-T07 — Audit-Log-Tabelle
|
||||
- **Ziel:** Nachvollziehbarkeit.
|
||||
- **Schritte:** `audit_log` (id, tenant_id, profile_id, action, target_type, target_id, metadata jsonb, created_at). Append-only.
|
||||
- **Akzeptanz:** Tabelle vorhanden, indiziert auf (tenant_id, created_at).
|
||||
- **Abhängigkeiten:** E2-T02.
|
||||
|
||||
### E2-T08 — RLS-Policies
|
||||
- **Ziel:** DB-seitige Mandanten-Isolation als zweite Verteidigungslinie.
|
||||
- **Schritte:** RLS auf tenants/projects/vms/ssh_keys/audit_log aktivieren; Policies auf Basis der Mitgliedschaft. Service-Role des Backends ist dokumentiert ausgenommen (Backend macht eigene Filterung).
|
||||
- **Akzeptanz:** Direkter Query als Nicht-Mitglied liefert keine fremden Zeilen.
|
||||
- **Abhängigkeiten:** E2-T02 bis E2-T07.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E3 — Proxmox-Zugriffsschicht
|
||||
|
||||
### E3-T01 — Krypto-Layer (Envelope-Encryption)
|
||||
- **Ziel:** Cluster-Tokens sicher ver-/entschlüsseln.
|
||||
- **Schritte:** AES-GCM mit 32-Byte-Master-Key aus ENV/Secret-Manager. Nonce wird dem Ciphertext vorangestellt. Funktionen `Encrypt(plaintext)` / `Decrypt(ciphertext)`. Key-Versionierung vorsehen (Prefix-Byte), um spätere Rotation zu ermöglichen.
|
||||
- **Akzeptanz:** Roundtrip-Unit-Test; falscher Key → Fehler statt Müll; Master-Key fehlt → Start bricht ab.
|
||||
- **Abhängigkeiten:** E0-T04.
|
||||
|
||||
### E3-T02 — Cluster-Repository
|
||||
- **Ziel:** Cluster-Credentials laden/speichern.
|
||||
- **Schritte:** `GetCluster(id)` lädt und entschlüsselt das Token im Speicher; `UpsertCluster` verschlüsselt vor dem Speichern. Token nie loggen, nie in DTO.
|
||||
- **Akzeptanz:** Test: gespeichertes Token kommt entschlüsselt korrekt zurück; DB enthält nur Ciphertext.
|
||||
- **Abhängigkeiten:** E3-T01, E2-T04.
|
||||
|
||||
### E3-T03 — Proxmox-HTTP-Client mit TLS-Pinning
|
||||
- **Ziel:** Sichere Verbindung zum Cluster.
|
||||
- **Schritte:** HTTP-Client, der pro Cluster den `tls_fingerprint` prüft (kein `InsecureSkipVerify`). Auth-Header `PVEAPIToken=<token_id>=<secret>`. Methoden: `Get/Post/Put/Delete` mit Retry + Timeout + Kontext.
|
||||
- **Akzeptanz:** Verbindung gegen Mock-Server mit korrektem/falschem Fingerprint (akzeptiert/abgelehnt). Kein Klartext-Secret in Logs.
|
||||
- **Abhängigkeiten:** E3-T02.
|
||||
|
||||
### E3-T04 — Admin-Endpunkt: Cluster verwalten
|
||||
- **Ziel:** Cluster anlegen/Token rotieren ohne Deployment.
|
||||
- **Schritte:** Interner Endpunkt (nur Operator-Rolle, **nicht** für Kunden): Cluster anlegen, Token aktualisieren, Status setzen. Response enthält nie das Token.
|
||||
- **Akzeptanz:** Token-Rotation per Request ändert nur `encrypted_token`; alter Wert nicht mehr lesbar.
|
||||
- **Abhängigkeiten:** E3-T03, E4-T02.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E4 — Autorisierung (RBAC)
|
||||
|
||||
### E4-T01 — Mitgliedschafts-Resolver
|
||||
- **Ziel:** Vom JWT zur Berechtigung.
|
||||
- **Schritte:** Aus `sub` → `profiles` → `memberships` laden; aktuellen Tenant/Projekt aus Request ableiten; Rolle ermitteln. Im Context bereitstellen.
|
||||
- **Akzeptanz:** Nicht-Mitglied eines Tenants erhält bei tenant-Zugriff 403.
|
||||
- **Abhängigkeiten:** E1-T02, E2-T02.
|
||||
|
||||
### E4-T02 — Policy-/Permission-Funktion
|
||||
- **Ziel:** Zentrale `Can(role, action, resource)`-Prüfung.
|
||||
- **Schritte:** Rollen-Matrix (owner/admin/member/viewer × Aktionen wie vm.read, vm.power, vm.create, vm.delete, project.manage, cluster.manage[nur Operator]). Eine Funktion, überall aufgerufen.
|
||||
- **Akzeptanz:** Tabellengetriebener Unit-Test deckt alle Rollen×Aktionen ab.
|
||||
- **Abhängigkeiten:** E4-T01.
|
||||
|
||||
### E4-T03 — Autorisierungs-Middleware
|
||||
- **Ziel:** Deklarativer Schutz pro Route.
|
||||
- **Schritte:** Middleware/Helper, der vor Handler die nötige Permission prüft. Quota-Check als separater Helper für create-Aktionen.
|
||||
- **Akzeptanz:** Geschützte Route ohne Recht → 403; mit Recht → durch.
|
||||
- **Abhängigkeiten:** E4-T02.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E5 — VM-Lifecycle (synchron lesend + Aktionen)
|
||||
|
||||
### E5-T01 — VM-Liste & Detail (read)
|
||||
- **Ziel:** Kunden sehen ihre VMs.
|
||||
- **Schritte:** Endpunkte `GET /projects/:id/vms` und `GET /vms/:id`. Daten aus eigener DB; Live-Status optional via Proxmox ergänzen. Immer tenant-/projektgefiltert.
|
||||
- **Akzeptanz:** Nutzer sieht nur VMs seiner Projekte.
|
||||
- **Abhängigkeiten:** E3-T03, E4-T03, E2-T05.
|
||||
|
||||
### E5-T02 — Power-Aktionen (start/stop/reboot)
|
||||
- **Ziel:** VM steuern.
|
||||
- **Schritte:** Endpunkte, die Permission `vm.power` prüfen, Proxmox-Call absetzen, die zurückgegebene UPID an den Worker übergeben (siehe E6), Audit-Log schreiben.
|
||||
- **Akzeptanz:** Start einer VM erzeugt Task; Status wird nach Abschluss in DB aktualisiert.
|
||||
- **Abhängigkeiten:** E5-T01, E6-T02, E2-T07.
|
||||
|
||||
### E5-T03 — Reconciliation-Job
|
||||
- **Ziel:** Drift zwischen DB und Proxmox erkennen.
|
||||
- **Schritte:** Periodischer Job, der pro Cluster den Ist-Zustand abruft und mit der DB abgleicht; Abweichungen markieren/loggen. Vorerst nur Reporting, kein Auto-Fix.
|
||||
- **Akzeptanz:** Manuell in Proxmox gestoppte VM wird im nächsten Lauf als abweichend erkannt.
|
||||
- **Abhängigkeiten:** E5-T01.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E6 — Task-Worker (asynchron)
|
||||
|
||||
### E6-T01 — Worker-Grundgerüst
|
||||
- **Ziel:** Eigener Dienst für lange Jobs.
|
||||
- **Schritte:** Go-Worker mit `asynq`, Redis-Anbindung, Verbindung zur selben Supabase-DB über den **Connection Pooler (Supavisor)**. Graceful Shutdown.
|
||||
- **Akzeptanz:** Worker startet, verarbeitet einen Dummy-Job.
|
||||
- **Abhängigkeiten:** E0-T02, E2-T01.
|
||||
|
||||
### E6-T02 — UPID-Polling-Job
|
||||
- **Ziel:** Proxmox-Tasks zu Ende verfolgen.
|
||||
- **Schritte:** Job-Typ `proxmox.task.poll` (Input: cluster_id, node, UPID, ziel-Entität). Pollt Status bis Erfolg/Fehler, aktualisiert die betroffene Entität in der DB, schreibt Audit-Log. Timeout + max. Retries; bei Fehler Status `failed` + Fehlertext.
|
||||
- **Akzeptanz:** Mock-Proxmox liefert „running" dann „stopped/OK" → DB-Status wird final korrekt; bei „error" → `failed`.
|
||||
- **Abhängigkeiten:** E6-T01, E3-T03.
|
||||
|
||||
### E6-T03 — Cleanup bei Fehlschlag
|
||||
- **Ziel:** Keine Zombie-Ressourcen.
|
||||
- **Schritte:** Wenn ein Provisioning-Task scheitert, halbfertige VM in Proxmox entfernen oder als `failed` markieren und VMID freigeben.
|
||||
- **Akzeptanz:** Simulierter Fehlschlag hinterlässt keine reservierte VMID und keine verwaiste VM.
|
||||
- **Abhängigkeiten:** E6-T02, E2-T06.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E7 — Provisioning (Clone + cloud-init)
|
||||
|
||||
### E7-T01 — SSH-Key-Verwaltung
|
||||
- **Ziel:** Kunden hinterlegen Keys.
|
||||
- **Schritte:** CRUD-Endpunkte für `ssh_keys` (tenant-gescoped), Validierung des Public-Key-Formats.
|
||||
- **Akzeptanz:** Ungültiger Key wird abgelehnt; Keys sind tenant-isoliert.
|
||||
- **Abhängigkeiten:** E4-T03, E2-T05.
|
||||
|
||||
### E7-T02 — Template-Vorbereitung (Doku + Skript)
|
||||
- **Ziel:** cloud-init-fähige Templates pro Node.
|
||||
- **Schritte:** Dokumentiertes Vorgehen + optionales Skript, um Cloud-Images (Ubuntu/Debian) als Proxmox-Templates anzulegen. In der DB eine `templates`-Referenz (id, cluster_id, name, proxmox_template_vmid).
|
||||
- **Akzeptanz:** Mind. ein Template pro Test-Cluster auswählbar.
|
||||
- **Abhängigkeiten:** E3-T03.
|
||||
|
||||
### E7-T03 — VM-Erstellung (Clone + cloud-init)
|
||||
- **Ziel:** Self-Service-VM wie bei Hetzner.
|
||||
- **Schritte:** `POST /projects/:id/vms`: Permission + Quota prüfen → VMID allokieren → Proxmox-Clone vom Template → cloud-init setzen (SSH-Key, Hostname, Netzwerk) → Start → UPID an Worker. DB-Eintrag mit Status `provisioning`.
|
||||
- **Akzeptanz:** End-to-End gegen Mock: VM-Datensatz entsteht, Worker schließt ab, Status `running`. Quota-Überschreitung → 409.
|
||||
- **Abhängigkeiten:** E7-T01, E7-T02, E6-T02, E2-T06, E4-T03.
|
||||
|
||||
### E7-T04 — VM löschen
|
||||
- **Ziel:** Aufräumen sauber.
|
||||
- **Schritte:** `DELETE /vms/:id` mit Permission `vm.delete`; Proxmox-Stop+Delete via Worker; DB-Eintrag und VMID freigeben; Audit-Log.
|
||||
- **Akzeptanz:** Nach Löschung keine VM in DB/Proxmox, VMID wieder vergebbar.
|
||||
- **Abhängigkeiten:** E7-T03.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E8 — Konsolen-Proxy (noVNC)
|
||||
|
||||
### E8-T01 — Konsolen-Ticket-Endpunkt
|
||||
- **Ziel:** Kurzlebigen Zugang ausstellen.
|
||||
- **Schritte:** `POST /vms/:id/console`: Permission `vm.console` prüfen, von Proxmox VNC-Ticket holen, ein eigenes kurzlebiges, signiertes Proxy-Ticket an den Client geben (enthält keine Proxmox-Daten).
|
||||
- **Akzeptanz:** Nicht-berechtigter Nutzer → 403; Ticket hat kurze Gültigkeit.
|
||||
- **Abhängigkeiten:** E4-T03, E3-T03.
|
||||
|
||||
### E8-T02 — Websocket-Proxy-Dienst
|
||||
- **Ziel:** Browser ↔ Proxmox-Konsole ohne direkte Verbindung.
|
||||
- **Schritte:** Eigener Dienst, der das Proxy-Ticket validiert, **bei jedem Verbindungsaufbau** die Berechtigung erneut gegen die DB prüft, dann zur Proxmox-VNC-Websocket durchreicht. Kein Proxmox-Hostname zum Client.
|
||||
- **Akzeptanz:** Verbindung mit gültigem Ticket funktioniert; abgelaufenes/fremdes Ticket wird abgewiesen.
|
||||
- **Abhängigkeiten:** E8-T01.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E9 — Audit-Log (Querschnitt)
|
||||
|
||||
### E9-T01 — Audit-Writer
|
||||
- **Ziel:** Einheitliches Logging sicherheitsrelevanter Aktionen.
|
||||
- **Schritte:** Helper `Audit(ctx, action, target, metadata)`. In allen state-ändernden Endpunkten aufrufen (power, create, delete, key-changes, cluster-admin).
|
||||
- **Akzeptanz:** Jede schreibende Aktion erzeugt genau einen Audit-Eintrag.
|
||||
- **Abhängigkeiten:** E2-T07.
|
||||
|
||||
### E9-T02 — Audit-Anzeige (read)
|
||||
- **Ziel:** Transparenz für Tenant-Admins.
|
||||
- **Schritte:** `GET /tenants/:id/audit` mit Paginierung, nur für admin/owner.
|
||||
- **Akzeptanz:** Viewer/Member sehen kein Audit-Log.
|
||||
- **Abhängigkeiten:** E9-T01, E4-T03.
|
||||
|
||||
---
|
||||
|
||||
## EPIC E10 — Frontend MVP
|
||||
|
||||
### E10-T01 — App-Grundgerüst & Auth
|
||||
- **Ziel:** Login funktioniert.
|
||||
- **Schritte:** Vite + React + TS, Supabase JS-Client nur für Auth (Login/Logout/2FA), JWT im Speicher halten, an Backend-Calls per `Authorization`-Header anhängen. API-Layer mit TanStack Query.
|
||||
- **Akzeptanz:** Nutzer kann sich einloggen, 2FA durchlaufen, Token wird an Backend gesendet.
|
||||
- **Abhängigkeiten:** E1-T01.
|
||||
|
||||
### E10-T02 — Tenant-/Projekt-Umschalter & Mitgliederverwaltung
|
||||
- **Ziel:** Multi-Tenant-Navigation.
|
||||
- **Schritte:** Tenant-Switcher, Projektliste, Einladen/Rollen-Zuweisung (admin/owner). Layout im Hetzner-Stil (clean, shadcn/ui).
|
||||
- **Akzeptanz:** Wechsel zwischen Tenants lädt jeweils nur eigene Projekte.
|
||||
- **Abhängigkeiten:** E10-T01, E4-T03.
|
||||
|
||||
### E10-T03 — VM-Übersicht & Detail
|
||||
- **Ziel:** Kern-UI.
|
||||
- **Schritte:** VM-Liste mit Status, Detailseite mit Basis-Metriken, Power-Buttons (start/stop/reboot), Live-Status via Polling/Refetch.
|
||||
- **Akzeptanz:** Power-Aktion spiegelt sich nach Task-Abschluss in der UI.
|
||||
- **Abhängigkeiten:** E10-T02, E5-T02.
|
||||
|
||||
### E10-T04 — VM-Erstellungs-Wizard
|
||||
- **Ziel:** Self-Service-Provisioning.
|
||||
- **Schritte:** Wizard (Template, Größe, SSH-Key, Name), Quota-Anzeige, Fortschritt während `provisioning`.
|
||||
- **Akzeptanz:** Erstellte VM erscheint und wechselt nach Abschluss auf `running`.
|
||||
- **Abhängigkeiten:** E10-T03, E7-T03.
|
||||
|
||||
### E10-T05 — Web-Konsole
|
||||
- **Ziel:** noVNC im Browser.
|
||||
- **Schritte:** noVNC-Komponente, holt Ticket vom Backend, verbindet sich mit dem Konsolen-Proxy.
|
||||
- **Akzeptanz:** Funktionierende Konsole für berechtigte Nutzer.
|
||||
- **Abhängigkeiten:** E10-T03, E8-T02.
|
||||
|
||||
### E10-T06 — SSH-Keys & Audit-Ansicht
|
||||
- **Ziel:** Restliche MVP-Verwaltung.
|
||||
- **Schritte:** Key-Verwaltung-UI; Audit-Log-Ansicht für Admins.
|
||||
- **Akzeptanz:** Keys verwaltbar; Audit nur für Admins sichtbar.
|
||||
- **Abhängigkeiten:** E7-T01, E9-T02.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 & 3 (Backlog, nach MVP)
|
||||
|
||||
Grob umrissen — bei Bedarf nach demselben Schema (Ziel/Schritte/Akzeptanz/Abhängigkeiten) herunterbrechen.
|
||||
|
||||
- **Phase 2:** Snapshots & Backups (Proxmox Backup Server), VM-Resize/Rebuild, Firewall-Regeln pro VM (Proxmox-Firewall-API), private Netzwerke via SDN/VNet pro Tenant, Floating IPs (eigener IP-Pool), feinere Quota-Durchsetzung.
|
||||
- **Phase 3:** Metriken-Dashboards (Proxmox-Metriken nach Prometheus/InfluxDB exportieren), Usage-Tracking als Abrechnungsgrundlage, öffentliche API mit Projekt-Tokens (Terraform-Support), E-Mail-Benachrichtigungen, ISO-Mounts, LXC-Container, Load Balancer.
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Bearbeitungsreihenfolge (kritischer Pfad)
|
||||
|
||||
```
|
||||
E0 → E1 → E2 → E3 → E4 → E6 (Grundgerüst) → E5 → E7 → E8 → E9 → E10
|
||||
```
|
||||
|
||||
E9 (Audit) und E10 (Frontend) laufen teils parallel, sobald die jeweiligen Backend-Endpunkte stehen. E6-T01 sollte früh kommen, weil E5/E7 darauf aufbauen.
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
logger.Info("worker started")
|
||||
<-ctx.Done()
|
||||
logger.Info("worker stopped")
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module proxui/worker
|
||||
|
||||
go 1.22
|
||||
Reference in New Issue
Block a user