Basis aus 2nd-brain für LPIC-Prüfungsvorbereitung übernommen
Regeln, Standards, Kontextstruktur und Werkzeuge übernommen; 2nd-brain- Inhalte (Stacks, Ansible-Inventar, Architektur, Knowledge) entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
7f6b8ebe07
70 files changed
+2816
No files matched your search
@@ -0,0 +1,24 @@
|
||||
# ai/common/ – Standards & Verabredungen
|
||||
|
||||
> **Wie** wir Dinge tun. Verbindlicher Standard; Abweichungen nur begründet (im Ergebnis bzw. Stack-/Projekt-README).
|
||||
> Verbesserungen sind erwünscht – Ablauf: `agents.md` § 5 „AI-Basis verbessern“. Jede Datei hat unten ein Änderungsprotokoll.
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| [agents.md](agents.md) | Arbeitsweise der Agenten, Qualitätscheckliste, Basis verbessern, Rollen |
|
||||
| [bash.md](bash.md) | Bash-Standard: `.env` + `_functions.sh`, Optionen, Logging (`becho`…), Trap, Präambel = Hilfe |
|
||||
| [ansible.md](ansible.md) | Ansible: Layout, `.env.sh`, Präambel, `hostlist`, Rollen, Facts, Secrets, Tags |
|
||||
| [docker.md](docker.md) | Docker-Compose-Standard (GELF, Traefik, Bind-Mounts, `.env`, README-Pflicht) |
|
||||
| [coding.md](coding.md) | Allgemeine Code-Regeln, Namen, Konfiguration, Python, Lint/Test |
|
||||
| [markdown.md](markdown.md) | Markdown-/Doku-Konventionen, READMEs, Projekt-`context.md` |
|
||||
| [architecture.md](architecture.md) | Architektur-Skizze (arc42-light), ADRs, Lösungsgrundsätze |
|
||||
|
||||
## templates/
|
||||
| Pfad | Inhalt |
|
||||
|---|---|
|
||||
| `templates/bash/` | `script_template.sh`, `_functions.sh` (Referenz, Version in `B_FUNCTIONS_VERSION`), `.env.example` |
|
||||
| `templates/ansible/` | `.env.sh`, `ansible.cfg`, `inv/`, `playbooks/{example,mini_check}.yml`, `roles/example/`, `files/example/` |
|
||||
| `templates/docker/` | `docker-compose.yml`, `.env.example`, Stack-`README.md` |
|
||||
| `templates/markdown/` | `knowledge_entry.md`, `adr.md` |
|
||||
|
||||
Vorlagen werden **kopiert**, nicht verlinkt. `bin/_functions.sh` ist eine Kopie von `templates/bash/_functions.sh` – beide gleich halten.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Agenten – Arbeitsweise (projektübergreifend)
|
||||
|
||||
> Gilt für alle KI-Agenten, agentisch (Claude Code, Codex, Gemini CLI, VSCodium/Continue) wie im Chat.
|
||||
> `ai/rules/*.md` haben Vorrang.
|
||||
|
||||
## 1. Arbeitsweise
|
||||
- **Sitzungsstart** gemäß `AGENTS.md`.
|
||||
- **Erst verstehen, dann bauen:** vorhandenen Code, Knowledge-Einträge und Projektstand lesen, bevor etwas Neues entsteht. Bestehendes wiederverwenden.
|
||||
- **Empfehlung statt Optionsliste.** Alternativen nur, wenn die Entscheidung wirklich beim Bediener liegt – dann knapp mit Empfehlung.
|
||||
- **Nachfragen**, wenn: Ziel/Projekt unklar, Regel berührt (Produktion, Löschen, extern), Verantwortliche für den Header fehlen, Annahmen das Ergebnis wesentlich ändern.
|
||||
- **Selbstständig handeln** bei: Formalien, Konventionen aus `common/`, offensichtlichen Defaults – im Ergebnis kurz erwähnen.
|
||||
- **Keine Erfindungen:** Hostnamen, IPs, Versionen, Optionen nur aus Knowledge/Projekt/Quelle. Unsicheres als „unsicher“ markieren.
|
||||
- **Ergebnisse ablegen, nicht nur im Chat:** Projektstand → `context.md`; Dauerhaftes → Knowledge-DB.
|
||||
|
||||
## 2. Qualitätsmaßstab („nah am Optimum“)
|
||||
Vor Abgabe von Code/Doku/Architektur selbst prüfen:
|
||||
- [ ] Passender Standard aus `common/` eingehalten, Vorlage aus `common/templates/` genutzt
|
||||
- [ ] Header mit Verantwortlichen (`rules/headers.md`), Funktionen mit Kurz-Doku
|
||||
- [ ] Keine Geheimnisse, keine internen Daten in Beispielen für Externe
|
||||
- [ ] Lokal getestet (Syntax, Lint, Trockenlauf) – oder ausdrücklich gesagt, was **nicht** getestet wurde
|
||||
- [ ] Doku/Knowledge aktualisiert, `bin/kb_index.sh` gelaufen
|
||||
- [ ] Einfachste Lösung, die das Ziel erfüllt – keine Vorratsfunktionen
|
||||
|
||||
## 3. Formales
|
||||
- Sprache, Datum: `rules/language.md`.
|
||||
- Markdown: `common/markdown.md`.
|
||||
- Commit-Messages (falls Git): Amtssprache, Imperativ, erste Zeile ≤ 72 Zeichen, Bezug zum Bereich (`ai/rules: Regel Prüfungsfragen ergänzt`).
|
||||
|
||||
## 4. Standards (Verweise)
|
||||
| Thema | Datei |
|
||||
|---|---|
|
||||
| Bash-Scripts | [bash.md](bash.md) |
|
||||
| Ansible | [ansible.md](ansible.md) |
|
||||
| Docker-Compose | [docker.md](docker.md) |
|
||||
| Coding allgemein, Python | [coding.md](coding.md) |
|
||||
| Markdown, Knowledge-Einträge | [markdown.md](markdown.md) |
|
||||
| Architektur-Doku, Entscheidungen (ADR) | [architecture.md](architecture.md) |
|
||||
|
||||
## 5. AI-Basis verbessern
|
||||
Agenten **sollen** die Basis besser machen, wenn sie bei der Arbeit Lücken, Widersprüche oder wiederkehrende Muster bemerken.
|
||||
|
||||
| Was | Wie |
|
||||
|---|---|
|
||||
| Regel fehlt/ist unklar (`rules/`) | **Nicht selbst ändern.** Vorschlag im Chat + Eintrag „Offene Fragen“ in `context.md` |
|
||||
| Standard verbessern (`common/`) | Ändern, wenn klar besser und rückwärtskompatibel; Eintrag im Änderungsprotokoll der Datei (unten, `TT.MM.JJJJ – was – warum`). Größeres vorher vorschlagen |
|
||||
| Vorlage/Script (`common/templates/`, `bin/`) | Ändern + lokal testen + `@version` erhöhen; `_functions.sh` in `bin/` und `templates/bash/` synchron halten |
|
||||
| Kontext/Knowledge | Jederzeit ergänzen/korrigieren, mit `updated` und Quelle |
|
||||
| Wiederkehrende Frage des Bedieners | Antwort als Knowledge-Eintrag (`howto/`) ablegen |
|
||||
|
||||
Verbesserungen, die **allgemein** gelten (nicht nur für dieses Projekt), dem Bediener zur Übernahme ins Skeleton (`~/skel-ai`) vorschlagen.
|
||||
|
||||
## 6. Rollen der Agenten
|
||||
| Agent | Einsatz | Grenzen |
|
||||
|---|---|---|
|
||||
| Claude Code | agentisches Coding, Doku | Datenklasse laut `rules/privacy.md` |
|
||||
| Chat-Dialog | Konzepte, Reviews | kein Dateizugriff → relevante Dateien einfügen lassen |
|
||||
|
||||
**Chat-Dialog ohne Dateizugriff:** Der Bediener fügt `AGENTS.md`, die betroffenen `rules/`, den passenden Standard und `context.md` ein. Der Agent liefert vollständige Dateien bzw. Diffs und am Ende einen Block „Für context.md“ zum Einpflegen.
|
||||
|
||||
## Änderungsprotokoll
|
||||
- 07.10.2026 – angelegt (aus skel-ai).
|
||||
- 23.10.2026 – auf Einzelprojekt reduziert (`context.md` im Root, Rollen festgelegt).
|
||||
@@ -0,0 +1,91 @@
|
||||
# Ansible-Standard (verbindlich)
|
||||
|
||||
> Gilt für alle Plays und Rollen in diesem Projekt. Verallgemeinert aus einem erprobten Homelab-Setup.
|
||||
> Vorlagen: [`templates/ansible/`](templates/ansible/). Geheimnisse: `rules/secrets.md`.
|
||||
|
||||
## 1. Verzeichnislayout
|
||||
```
|
||||
<repo>/ansible/
|
||||
├── .env.sh # setzt alle Pfade + Secret-Store-Variablen → source (bash/zsh)
|
||||
├── ansible.cfg # Fact-Cache, become, Log – Pfade aus .env.sh
|
||||
├── inv/
|
||||
│ ├── <umgebung> # Inventar (INI oder YAML), z. B. prod, lab
|
||||
│ ├── group_vars/all/ # globale Variablen (ansible_user, ans_files, Secret-Store-Pfade)
|
||||
│ ├── group_vars/<gruppe>.yml
|
||||
│ └── host_vars/<host>.yml
|
||||
├── playbooks/ # ein Dienst = ein Play; Präambel = Doku
|
||||
├── roles/<dienst>/ # Logik: tasks/ handlers/ defaults/ meta/ (keine templates/!)
|
||||
└── files/<dienst>/ # alle Templates (*.j2) und Dateien – Rollen holen sie aus $ANS_FILES
|
||||
```
|
||||
`.env.sh` (interaktiv in der Shell gesourct, setzt `ANSIBLE_*`) ist bewusst etwas anderes als die `.env` der Bash-Scripts.
|
||||
|
||||
## 2. Aufruf & Umgebung
|
||||
- Vor jedem Aufruf: `source ansible/.env.sh`. Pfade **nie** hart codieren: `$ANSIBLE_BASE_PATH`, `$ANS_PLAYDIR`, `$ANS_FILES` (in YAML: `ans_files`).
|
||||
- Aufrufe immer: `ansible-playbook $ANS_PLAYDIR/<play>.yml [-e hostlist=…] [--check --diff]`.
|
||||
- `.env.sh` setzt fremde `ANSIBLE_*`-Variablen zurück (`unset`), damit kein Kontext eines anderen Repos durchschlägt.
|
||||
|
||||
| Variable | Zweck |
|
||||
|---|---|
|
||||
| `ANSIBLE_BASE_PATH` | Basis für alles |
|
||||
| `ANSIBLE_CONFIG`, `ANSIBLE_INVENTORY`, `ANSIBLE_ROLES_PATH` | Standard-Ansible-Variablen |
|
||||
| `ANS_PLAYDIR` | Playbooks |
|
||||
| `ANS_FILES` | Templates/Dateien (YAML: `ans_files`) |
|
||||
| `ANS_DEFAULT_HOSTS` | Dokumentation des Standardziels (s. § 3) |
|
||||
| `VAULT_ADDR`, `ANSIBLE_HASHI_VAULT_*`, `ANS_VAULT_KV_MOUNT/PATH` | Secret-Store |
|
||||
| `ANSIBLE_VAULT_PASSWORD_FILE` | nur wenn Datei existiert (Fallback Ansible Vault) |
|
||||
|
||||
## 3. Playbooks
|
||||
- **Präambel max. 12 Zeilen** im Header-Format (`rules/headers.md`): `@file`, `@brief`, `@author`, `@owner`, `@version`, `@usage` mit nötigen/optionalen `-e`-Variablen, Tags und Voraussetzungen.
|
||||
- Ziel immer `hosts: "{{ hostlist | default('<STANDARDHOST>') }}"` – ohne `-e` genau **ein** bekannter, ungefährlicher Host
|
||||
(Lab/Test). **Nie** `all` oder eine Produktivgruppe als Default.
|
||||
- Ein Dienst = ein Play (`radius.yml`, `dns.yml`). Logik liegt in `roles/<dienst>/`; das Play bindet nur die Rolle ein.
|
||||
- Dateinamen: Kleinbuchstaben, `_` statt `-` (`vault_check.yml`).
|
||||
- Standard-Hilfsplays je Repo: `mini_check.yml` (Erreichbarkeit, Facts), `vault_check.yml` (Secret-Store erreichbar, nur Schlüsselnamen).
|
||||
|
||||
## 4. Rollen
|
||||
- Rollenname = Dienst. Tasks thematisch teilen: `main.yml` (Ablauf) → `secrets.yml`, `install.yml`, `config.yml`, `verify.yml`.
|
||||
- `main.yml` beginnt mit einem **Plattform-Check** (`ansible.builtin.assert` auf `ansible_facts`), Tag `always`.
|
||||
- **Templates liegen nicht in der Rolle**, sondern in `files/<dienst>/`; Zugriff über `{{ ans_files }}/<dienst>/…`.
|
||||
- Nur Dateien templaten, die wir „besitzen“. Paket-Defaults nicht komplett überschreiben.
|
||||
- Konfiguration **vor** dem Restart prüfen: Handler-Kette Check → Restart (`listen`/`notify`), `meta: flush_handlers` vor `service: started`.
|
||||
Bei Fehler bleibt der laufende Dienst unberührt.
|
||||
- Jede Rolle hat einen **Smoke-Test** (`verify.yml`, Tag `verify`, `check_mode: false`, `changed_when: false`).
|
||||
- **Idempotent:** zweiter Lauf `changed=0`. `command`/`shell` nur mit `creates`/`changed_when`.
|
||||
- Module mit FQCN (`ansible.builtin.…`, `community.…`). **Task-Namen in der Amtssprache**, als Satz („Paket installieren“).
|
||||
- Variablen der Rolle mit Rollenpräfix (`radius_clients`), Defaults in `defaults/main.yml` mit Kommentar je Variable.
|
||||
|
||||
## 5. SSH-Performance
|
||||
- `pipelining=True` (Standard in `templates/ansible/ansible.cfg`): Module laufen per stdin statt Kopie nach `/tmp` – spart je Task
|
||||
mehrere SSH-Roundtrips, typisch der größte Einzelgewinn. Voraussetzung: kein `Defaults requiretty` in `sudoers` (aktuelle Distros: Standard).
|
||||
- Dazu `ControlMaster=auto` / `ControlPersist=60s`: eine SSH-Verbindung je Host wird wiederverwendet.
|
||||
- Bei Fehlern wie „sudo: sorry, you must have a tty“: `requiretty` auf dem Ziel entfernen, nicht `pipelining` abschalten.
|
||||
|
||||
## 6. Facts
|
||||
- `gathering=smart` + Fact-Cache (`jsonfile`, 86400 s) aus `ansible.cfg`. Facts nutzen statt Shell-Abfragen
|
||||
(`ansible_facts['distribution_major_version']`), Zugriff immer über `ansible_facts[...]`, nicht über `ansible_*`-Kurzformen.
|
||||
- Facts neu sammeln: `ansible-playbook $ANS_PLAYDIR/mini_check.yml -e check_mode=setup`.
|
||||
|
||||
## 7. Geheimnisse
|
||||
- Secret-Store per `community.hashi_vault` (Lookup in `secrets.yml`), Fallback Ansible Vault (`vault_*`-Variablen).
|
||||
- Tasks mit Geheimnissen `no_log: true`, Templates mit Geheimnissen `diff: false`. Nicht entfernen.
|
||||
- Testbetrieb ohne Secret-Store: `-e <rolle>_secrets_source=vars -e @/pfad/außerhalb/testsecrets.yml`.
|
||||
|
||||
## 8. Tags
|
||||
`install`, `config`, `verify`; Plattform-Check und Secrets `always`.
|
||||
|
||||
## 9. Kommentare & Stil
|
||||
- Deaktivierte, absichtlich behaltene Blöcke mit `#-#` markieren.
|
||||
- YAML: 2 Leerzeichen, `---` am Anfang, `true`/`false` statt `yes`/`no`, Strings mit Jinja in Anführungszeichen.
|
||||
- Prüfen vor Abgabe: `ansible-lint`, `yamllint`, `ansible-playbook --syntax-check`, `--check --diff`, Lauf gegen lokalen Testcontainer, Idempotenz (`rules/testing.md`).
|
||||
|
||||
## 10. Neue Rolle in 5 Schritten
|
||||
1. Original-Doku des Dienstes holen (`rules/web_search.md`), Ergebnisse in `knowledge/services/<dienst>.md`.
|
||||
2. `cp -r ai/common/templates/ansible/roles/example roles/<dienst>`, `files/example` → `files/<dienst>`, Play aus `playbooks/example.yml`.
|
||||
3. Präfix `example_` → `<dienst>_`, Header ausfüllen.
|
||||
4. Lokal testen (Container), Idempotenz prüfen.
|
||||
5. Knowledge/`context.md` aktualisieren.
|
||||
|
||||
## Änderungsprotokoll
|
||||
- 07.10.2026 – angelegt, verallgemeinert aus infra-devices/ansible.
|
||||
- 08.10.2026 – § 5 SSH-Performance (pipelining, ControlPersist) als Vorgabe – Wunsch des Bedieners.
|
||||
- 23.10.2026 – Mandantenbezug entfernt (Einzelprojekt).
|
||||
@@ -0,0 +1,34 @@
|
||||
# Architektur-Dokumentation & Entscheidungen
|
||||
|
||||
> Ziel: Jede nicht triviale Lösung ist nachvollziehbar – was, warum, mit welchen Alternativen, und was sie voraussetzt.
|
||||
|
||||
## 1. Wann welche Form
|
||||
| Umfang | Form | Ort |
|
||||
|---|---|---|
|
||||
| Einzelne Entscheidung mit Tragweite (Produktwahl, Protokoll, Netzsegmentierung) | **ADR** | `context/knowledge/decisions/adr_NNNN_<slug>.md` |
|
||||
| Lösung des Projekts | **Architektur-Skizze** (arc42-light, § 2) | `architecture.md` (Root) |
|
||||
| Dauerhafter Systembestand (Host, Dienst, Netz) | Knowledge-Eintrag | `context/knowledge/{systems,services,network}/` |
|
||||
|
||||
## 2. Architektur-Skizze (arc42-light)
|
||||
1. **Ziel & Rahmen** – Anforderungen, Randbedingungen (Budget, Hardware, Datenschutzklasse).
|
||||
2. **Kontext** – Diagramm: System, Nutzer, Nachbarsysteme, Schnittstellen (Ports/Protokolle).
|
||||
3. **Bausteine** – Komponenten, wo sie laufen, wer sie pflegt (Ansible-Rolle, Docker-Stack, Script).
|
||||
4. **Abläufe** – wichtigste 1–3 Szenarien (z. B. Anmeldung, Backup, Ausfall).
|
||||
5. **Entscheidungen** – Verweise auf ADRs.
|
||||
6. **Risiken & offene Punkte**.
|
||||
7. **Betrieb** – Monitoring, Logging (GELF), Backup/Restore, Update-Weg.
|
||||
|
||||
## 3. ADR-Format
|
||||
Vorlage: [`templates/markdown/adr.md`](templates/markdown/adr.md). Nummern fortlaufend, nie wiederverwenden.
|
||||
Überholte ADRs nicht löschen: `status: superseded`, `supersedes`/`superseded_by` pflegen.
|
||||
|
||||
## 4. Grundsätze für Lösungen im Netz/IT-Umfeld
|
||||
- **Infrastructure as Code:** Neuaufbau = Playbook/Stack + Secret-Store, nicht Handarbeit. Handgriffe, die bleiben, als `howto/` dokumentieren.
|
||||
- **Ein Weg pro Aufgabe:** Ansible für Hostkonfiguration, Docker-Compose für Dienste, Bash für Werkzeuge/Glue.
|
||||
- **Sicher per Default:** minimale Rechte, Default-Ziel ungefährlich, Trockenlauf vor Änderung, Rückfallweg bei Netzänderungen.
|
||||
- **Beobachtbar:** jeder Dienst mit Healthcheck und zentralem Logging.
|
||||
- **Wiederherstellbar:** Backup-Pfad und Restore-Test sind Teil der Lösung, nicht Nacharbeit.
|
||||
|
||||
## Änderungsprotokoll
|
||||
- 07.10.2026 – angelegt.
|
||||
- 23.10.2026 – Architektur-Skizze im Root (Einzelprojekt).
|
||||
@@ -0,0 +1,161 @@
|
||||
# Bash-Standard (verbindlich)
|
||||
|
||||
> Gilt für jedes Bash-Script dieser Basis und ihrer Projekte. Referenzimplementierung: [`templates/bash/`](templates/bash/).
|
||||
> Die Scripts in `bin/` sind nach diesem Standard gebaut und dienen als Beispiele.
|
||||
|
||||
## 1. Bausteine – drei Dateien, mehr nicht
|
||||
```
|
||||
<scriptdir>/
|
||||
├── mein_script.sh # Präambel (= Hilfe) + Bootstrap + Funktionen + main
|
||||
├── _functions.sh # gemeinsame Basis – Kopie aus templates/bash/, NICHT lokal ändern
|
||||
├── _functions.local.sh # optional: projektspezifische Funktionen (lädt _functions.sh automatisch)
|
||||
├── .env # optional: Einstellungen (nie im Git)
|
||||
└── .env.example # Pflicht, wenn .env genutzt wird: gleiche Schlüssel, Platzhalter
|
||||
```
|
||||
Mehrere Scripts in einem Verzeichnis teilen sich `_functions.sh` und `.env`.
|
||||
Das frühere Modell (`_global_functions.sh`, `_project_functions.sh`, `_project_settings.sh`) entfällt:
|
||||
Settings → `.env`, Projektfunktionen → `_functions.local.sh`.
|
||||
|
||||
## 2. Aufbau eines Scripts
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------
|
||||
## @file backup_db.sh
|
||||
## @brief Sichert die Datenbank nach $DIR_BACKUP
|
||||
## @author <Name aus ai/context/responsible.md>
|
||||
## @owner <Name/Rolle aus ai/context/responsible.md>
|
||||
## @assist KI-unterstützt (<Modell>), geprüft von <author>
|
||||
## @version 1.0.0 (07.10.2026)
|
||||
##
|
||||
## @usage backup_db.sh [Optionen] <datenbank>
|
||||
## @options
|
||||
## @OPTIONS@
|
||||
## @env .env neben dem Script (Vorlage: .env.example)
|
||||
## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler
|
||||
#-------------------------------------------------------------------------------
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")" # Bootstrap – unverändert übernehmen
|
||||
SCRIPT_DIR="${SCRIPT_PATH%/*}"
|
||||
SCRIPT_NAME="${SCRIPT_PATH##*/}"
|
||||
source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; }
|
||||
|
||||
B_OPTS+=( "k|keep|value:TAGE|14|Sicherungen so lange behalten" )
|
||||
b_init "$@"
|
||||
set -- "${ARGS[@]}"
|
||||
|
||||
## @brief Hauptablauf
|
||||
## @param $1 Datenbankname
|
||||
## @return 0 ok, 1 Fehler
|
||||
main() { … }
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
### Präambel = Hilfe = Doku
|
||||
- Der Header zwischen den ersten beiden `#-----`-Linien ist **gleichzeitig** die Hilfe (`-h`): `b_usage` gibt alle `##`-Zeilen ohne `## ` aus.
|
||||
- `## @OPTIONS@` wird durch die Optionsliste aus `B_OPTS` ersetzt → Hilfe und Optionen können nicht auseinanderlaufen.
|
||||
- `##` ist der Doku-Marker (wie `/**` bei javadoc). `bin/bashdoc.sh script.sh` erzeugt daraus Markdown. Normale Kommentare mit `#`.
|
||||
- Header-Pflichtfelder: `rules/headers.md`.
|
||||
|
||||
## 3. Pfadsicherheit
|
||||
| Variable | Inhalt |
|
||||
|---|---|
|
||||
| `SCRIPT_PATH` | absoluter, aufgelöster Pfad des Scripts (Symlinks aufgelöst) |
|
||||
| `SCRIPT_DIR` | Verzeichnis des Scripts – Basis für `.env`, `_functions.sh`, relative Pfade aus `.env` |
|
||||
| `SCRIPT_NAME` | Dateiname des Scripts |
|
||||
| `B_CALL_DIR` | `$PWD` beim Aufruf – Basis für relative Pfade **von der Kommandozeile** |
|
||||
| `B_LIB_DIR` | Verzeichnis von `_functions.sh` |
|
||||
| `B_TMP_DIR` | Temp-Verzeichnis nach `b_tmpdir`, wird beim Exit gelöscht |
|
||||
|
||||
- Nie `cd` als Voraussetzung, nie relative Pfade ohne Basis. Helfer: `b_abspath <pfad> [basis]` (Default-Basis `SCRIPT_DIR`).
|
||||
- Faustregel: Pfade aus `.env`/Defaults → relativ zu `SCRIPT_DIR`; Pfade, die der Benutzer tippt → relativ zu `B_CALL_DIR`.
|
||||
|
||||
## 4. Optionen (`B_OPTS`)
|
||||
Format je Eintrag: `"kurz|lang|typ[:META]|default|Beschreibung"` – `typ` = `flag` (0/1) oder `value`.
|
||||
|
||||
| Quelle | Wo | Hinweis |
|
||||
|---|---|---|
|
||||
| Standard (`-h -d -q -n -l`) | `_functions.sh` (`B_OPTS_STD`) | immer da |
|
||||
| Script-eigen | im Script vor `b_init`: `B_OPTS+=(…)` | Regelfall |
|
||||
| Verzeichnisweit | `.env`: `B_OPTS+=(…)` | gilt für alle Scripts des Verzeichnisses |
|
||||
|
||||
- Ergebnis: Variable `OPTION_<LANG>` (Großbuchstaben, `-` → `_`): `--dry-run` → `OPTION_DRY_RUN`, `--keep 7` → `OPTION_KEEP=7`.
|
||||
- Unterstützt: `-d`, `-dn` (gebündelt), `-l datei`, `-ldatei`, `--logfile datei`, `--logfile=datei`, `--debug=0`, `--` (Ende der Optionen). Positionsargumente landen in `ARGS` (auch zwischen Optionen).
|
||||
- Unbekannte Option / fehlender Wert → Meldung, Hinweis auf `--help`, Exit 2.
|
||||
- Immer `+=` verwenden, nie `B_OPTS=(…)` (würde Einträge aus `.env`/`_functions.local.sh` löschen). Kein `|` in Default/Text.
|
||||
|
||||
### Vorrang (und Gültigkeit von Umgebungsvariablen)
|
||||
**Default < `.env` < Umgebung < Kommandozeile**
|
||||
- Exportierte `B_*`/`OPTION_*`-Variablen aus der Umgebung werden vor dem Laden von `.env` gesichert und danach wiederhergestellt
|
||||
(`B_LOG_LEVEL=debug ./script.sh` gewinnt gegen `.env`). Nicht exportierte Shell-Variablen des Aufrufers sieht das Script ohnehin nicht.
|
||||
- Für eigene Projektwerte in `.env` dasselbe Verhalten per `VAR="${VAR:-default}"`.
|
||||
- `.env` wird **nicht** automatisch exportiert. Werte, die Kindprozesse brauchen, explizit `export`ieren (Geheimnisse möglichst nicht).
|
||||
- Wirkung der Standardoptionen: `-d` → `B_LOG_LEVEL=debug`, `-q` → `B_LOG_CONSOLE=0`, `-l` → `B_LOG_FILE`, `-n` → `brun` führt nichts aus.
|
||||
|
||||
## 5. Ausgabe & Logging – nie `echo`
|
||||
| Funktion | Level | Ziel Konsole | Zweck |
|
||||
|---|---|---|---|
|
||||
| `becho "…"` | info | stdout, ohne Präfix | Ersatz für `echo` |
|
||||
| `bdebug "…"` | debug | stderr, `[DEBUG]` | Details, nur bei `-d` |
|
||||
| `bwarn "…"` | warn | stderr, `[WARN]` | Auffälliges, Lauf geht weiter |
|
||||
| `berror "…"` | error | stderr, `[FEHLER]` | Fehler melden (Funktion entscheidet über `return`) |
|
||||
| `bdie "…" [rc]` | error | stderr | melden + beenden (rc 2 = Aufruffehler mit Hilfe-Hinweis) |
|
||||
| `blog <level> "…"` | – | – | Basis aller obigen |
|
||||
|
||||
Steuerung (`.env`, Umgebung oder Option):
|
||||
| Variable | Default | Bedeutung |
|
||||
|---|---|---|
|
||||
| `B_LOG_LEVEL` | `info` | Mindestlevel Konsole (`debug`/`info`/`warn`/`error`) |
|
||||
| `B_LOG_CONSOLE` | `1` | Konsole an/aus |
|
||||
| `B_LOG_FILE` | leer | Logdatei; leer = keine |
|
||||
| `B_LOG_FILE_LEVEL` | = `B_LOG_LEVEL` | eigenes Mindestlevel für die Datei |
|
||||
| `B_LOG_COLOR` | `auto` | Farben nur auf Terminal |
|
||||
|
||||
Logdatei-Format: `JJJJ-MM-TT hh:mm:ss LEVEL script.sh[PID] Text`.
|
||||
**Ausnahme vom echo-Verbot:** echte Daten-Ausgabe für Pipes (`printf '%s\n' "$ergebnis"`) – Meldungen gehen nie auf stdout außer über `becho`.
|
||||
|
||||
## 6. Fehler, Return-Codes, Aufräumen
|
||||
- `set -Eeuo pipefail` ist Pflicht. `b_init` setzt Traps: `EXIT` (Aufräumen), `ERR` (meldet Befehl, Funktion, Zeile), `INT`/`TERM` (Exit 130/143 → EXIT-Trap).
|
||||
- **Return-Code-Prinzip:** Jede Funktion endet mit `return 0` (ok), `1` (Fehler) oder `2` (falscher Aufruf); dokumentiert unter `@return`.
|
||||
Fehler meldet die Funktion selbst (`berror`) und gibt den Code zurück – der Aufrufer entscheidet (`if f; then`, `f || return 1`).
|
||||
- Ergebnisse über stdout (`x="$(f)"`) oder globale Variablen mit Präfix – nie über Exit-Codes > 2.
|
||||
- Aufräumen: Script definiert optional `cleanup() { … }` – wird vom EXIT-Trap mit dem Exit-Code aufgerufen, auch bei Fehler/Abbruch.
|
||||
Temporäres immer über `b_tmpdir` (`$B_TMP_DIR`), wird automatisch entfernt.
|
||||
|
||||
## 7. Funktionsdoku („briefly!“)
|
||||
```bash
|
||||
## @brief Was die Funktion tut – ein Satz
|
||||
## @param $1 Bedeutung
|
||||
## @param $@ restliche Argumente
|
||||
## @return 0 ok, 1 Fehler; Ausgabe auf stdout (falls zutreffend)
|
||||
name() {
|
||||
```
|
||||
- Direkt über der Funktion, keine Leerzeile dazwischen. Interne Helfer mit `_` beginnen (`bashdoc.sh` blendet sie ohne `-p` aus).
|
||||
- `local` für alle Variablen in Funktionen.
|
||||
|
||||
## 8. Weitere Helfer aus `_functions.sh`
|
||||
| Funktion | Zweck |
|
||||
|---|---|
|
||||
| `brun cmd args…` | ausführen bzw. bei `-n` nur anzeigen (kein `eval`, Argumente als Array) |
|
||||
| `b_require prog…` | prüft, ob Programme vorhanden sind |
|
||||
| `b_require_vars VAR…` | prüft, ob Variablen gesetzt sind |
|
||||
| `b_tmpdir` | legt `$B_TMP_DIR` an |
|
||||
| `b_abspath pfad [basis]` | absoluter Pfad |
|
||||
| `b_usage` | Hilfe ausgeben |
|
||||
|
||||
## 9. Stil
|
||||
- Bash ≥ 4.4. Shebang `#!/usr/bin/env bash`. 2 Leerzeichen Einrückung.
|
||||
- Immer quoten (`"$var"`, `"${arr[@]}"`), `[[ ]]` statt `[ ]`, `$(…)` statt Backticks, `printf` statt `echo -e`.
|
||||
- Dateinamen: Kleinbuchstaben, `_` als Trenner, Endung `.sh`; Hilfsdateien mit `_`-Präfix.
|
||||
- Variablen: Konstanten/Globale `GROSS`, lokale `klein`; Präfix `B_`/`_B_`/`OPTION_` ist für `_functions.sh` reserviert.
|
||||
- Prüfen vor Abgabe: `bash -n`, `shellcheck -x`, `-h`, `-n`, ein echter Lauf (lokal, `rules/testing.md`).
|
||||
|
||||
## 10. Neues Script in 4 Schritten
|
||||
1. `cp ai/common/templates/bash/{script_template.sh,_functions.sh,.env.example} <ziel>/` (`_functions.sh` nur, wenn noch nicht vorhanden).
|
||||
2. Umbenennen, Header ausfüllen (Verantwortliche aus `responsible.md`), `B_OPTS` anpassen.
|
||||
3. Funktionen mit Kurz-Doku + `main` schreiben.
|
||||
4. Testen (§ 9), ggf. `bin/bashdoc.sh` für die Projektdoku.
|
||||
|
||||
## Änderungsprotokoll
|
||||
- 07.10.2026 – angelegt; Stack auf `.env` + `_functions.sh` (+ optional `_functions.local.sh`) reduziert.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Coding – allgemeine Verabredungen (alle Sprachen) + Python
|
||||
|
||||
> Sprachspezifisch gehen vor: [bash.md](bash.md), [ansible.md](ansible.md), [docker.md](docker.md).
|
||||
|
||||
## 1. Für jede Code-Datei
|
||||
- **Header** mit Verantwortlichen (`rules/headers.md`) – ohne Ausnahme, auch für Templates und Configs.
|
||||
- **Kurz-Doku je Funktion/Klasse:** Zweck, Parameter, Rückgabe – knapp („briefly!“).
|
||||
- **Keine Geheimnisse und keine Umgebungsdaten im Code:** Hosts, IPs, Pfade, Domains, Ports → `.env`/Inventar/Konfiguration.
|
||||
- **Fehler nie verschlucken:** melden (Log/stderr) und definierten Code zurückgeben. Exit-Codes: 0 ok · 1 Fehler · 2 Aufruffehler.
|
||||
- **Logging statt print/echo** für Meldungen; stdout nur für echte Daten.
|
||||
- **Dry-Run** (`-n`/`--dry-run`) für alles, was Systeme verändert.
|
||||
- **Idempotent**, wo möglich: zweiter Lauf ändert nichts.
|
||||
- **Einfach:** kleinste Lösung, die das Ziel erfüllt; keine Vorratsfunktionen, keine unnötigen Abhängigkeiten.
|
||||
|
||||
## 2. Namen
|
||||
| Was | Konvention |
|
||||
|---|---|
|
||||
| Dateien, Verzeichnisse | Englisch, klein, `_` als Trenner (Ausnahme: Konventionen des Werkzeugs, z. B. `docker-compose.yml`, `README.md`) |
|
||||
| Variablen/Funktionen | Englisch, sprechend; Bash `snake_case`, Python PEP 8 |
|
||||
| Konstanten/Umgebung | `GROSS_MIT_UNTERSTRICH` |
|
||||
| Docker/Compose-Namen | `_` statt `-` (`docker.md`) |
|
||||
| Kommentare, Meldungen | Amtssprache |
|
||||
|
||||
## 3. Konfiguration
|
||||
- Reihenfolge überall gleich: **Default < Konfigurationsdatei/.env < Umgebung < Kommandozeile**.
|
||||
- `.env` nie im Git, `.env.example` immer mit allen Schlüsseln.
|
||||
- Relative Pfade immer zu einer definierten Basis (Script-/Projektverzeichnis), nie zum zufälligen `$PWD`.
|
||||
|
||||
## 4. Python
|
||||
- Python ≥ 3.11, nur Standardbibliothek, wenn vertretbar. Abhängigkeiten in `requirements.txt` (gepinnt) bzw. `pyproject.toml`.
|
||||
- Struktur: `main()` + `if __name__ == "__main__": sys.exit(main())`; Optionen mit `argparse` (Standardoptionen analog Bash: `-d/--debug`, `-q/--quiet`, `-n/--dry-run`, `-l/--logfile`).
|
||||
- Logging mit `logging` (Konsole + optional Datei, Level aus Umgebung/Option); kein `print` für Meldungen.
|
||||
- Header als Modul-Docstring mit denselben Tags:
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
@file sync_dns.py
|
||||
@brief Gleicht DNS-Einträge mit dem Inventar ab
|
||||
@author <Name aus responsible.md>
|
||||
@owner <Name/Rolle aus responsible.md>
|
||||
@version 0.1.0 (TT.MM.JJJJ)
|
||||
@usage sync_dns.py [-d] [-n] <zone>
|
||||
"""
|
||||
```
|
||||
- Funktionen mit Typannotationen und Kurz-Docstring (eine Zeile Zweck, `Args:`/`Returns:` nur wenn nicht offensichtlich).
|
||||
- Pfade mit `pathlib`, Basis `Path(__file__).resolve().parent`.
|
||||
- Prüfen: `ruff check`, `ruff format --check` (oder `python -m py_compile`), Tests mit `pytest`, lokal.
|
||||
|
||||
## 5. Lint & Test (Übersicht)
|
||||
| Sprache | Mindestens |
|
||||
|---|---|
|
||||
| Bash | `bash -n`, `shellcheck -x`, `-h`, `-n`, echter Lauf |
|
||||
| Ansible/YAML | `yamllint`, `ansible-lint`, `--syntax-check`, `--check --diff`, Idempotenz |
|
||||
| Docker | `docker compose config -q`, Healthcheck `healthy` |
|
||||
| Python | `ruff`, `pytest` |
|
||||
| Markdown | Links prüfen, `bin/kb_index.sh --check` |
|
||||
Was nicht getestet werden konnte, steht ausdrücklich im Ergebnis und in `context.md`.
|
||||
|
||||
## Änderungsprotokoll
|
||||
- 07.10.2026 – angelegt.
|
||||
- 23.10.2026 – `$PROJ/context.md` → `context.md` (Einzelprojekt).
|
||||
@@ -0,0 +1,189 @@
|
||||
# Docker-Compose-Standard (verbindlich)
|
||||
|
||||
> Gilt für **jeden** Docker-Stack, der hier entsteht. Nicht zur Diskussion stellen – vollständig „durchdeklinieren“.
|
||||
> Abweichungen nur, wenn das Image es technisch erzwingt; dann im Stack-README begründen.
|
||||
> Vorlagen: [`templates/docker/`](templates/docker/). Umgebungswerte (`<…>`) stehen in `ai/context/environment.md`.
|
||||
|
||||
## 1. Pflichtbestandteile je Service
|
||||
| # | Vorgabe | Warum |
|
||||
|---|---|---|
|
||||
| 1 | `restart: unless-stopped` | stabiler Betrieb, manuelles Stoppen bleibt respektiert |
|
||||
| 2 | `logging: *default-logging` (GELF → Graylog) | zentrales Logging |
|
||||
| 3 | `container_name: ${COMPOSE_PROJECT_NAME}` (mehrere Services: `${COMPOSE_PROJECT_NAME}_<service>`) | eindeutige Namen, GELF-Tag passt |
|
||||
| 4 | `env_file: .env` statt aufgeblähtem `environment:` | Stackfile bleibt transportabel |
|
||||
| 5 | Persistenz nur per **Bind-Mount** aus `.env` (`${DIR_*}`), keine Named Volumes | Stack-Verzeichnis = komplettes Backup, umziehbar |
|
||||
| 6 | `healthcheck` | „guter Ton“; Test passend zum Image (s. 4.) |
|
||||
| 7 | Traefik-Labels (s. 3.) für jeden Service mit Web-Oberfläche/API | Exposition über Reverse Proxy |
|
||||
| 8 | Image-Tag aus `.env` (`${<APP>_TAG}`), **nie** `latest` | reproduzierbar, Updates bewusst |
|
||||
| 9 | **Alle** host-/umgebungsspezifischen Werte in `.env` (Hosts, IPs, Ports, Pfade, Domains, `extra_hosts`) | Stackfile enthält keine Umgebungsdaten |
|
||||
| 10 | **Namen ohne Bindestrich – Unterstrich statt `-`:** `COMPOSE_PROJECT_NAME`, Stack-Verzeichnis, Service-, Container- und Router-Namen (z. B. `hashi_vault`, nicht `hashi-vault`) | GELF-Tag = Projektname; `_` läuft in der Elasticsearch-Suche sauber (`-` wird als Worttrenner zerlegt) |
|
||||
| 11 | **Header** mit Verantwortlichen im `docker-compose.yml` (`rules/headers.md`) | Zuständigkeit sofort sichtbar |
|
||||
|
||||
## 2. Verzeichnislayout je Stack
|
||||
```
|
||||
<STACK_BASE>/<stack>/ # <stack> = COMPOSE_PROJECT_NAME
|
||||
├── docker-compose.yml
|
||||
├── .env # echte Werte inkl. Secrets – nie in Git/extern
|
||||
├── .env.example # gleiche Schlüssel, Platzhalter statt Secrets
|
||||
├── README.md # Stack-Doku (s. 5.)
|
||||
└── data/ config/ … # Bind-Mount-Ziele (über DIR_* referenziert)
|
||||
```
|
||||
|
||||
## 3. Vorlagen
|
||||
|
||||
### 3.1 `docker-compose.yml`
|
||||
```yaml
|
||||
x-logging: &default-logging
|
||||
driver: gelf
|
||||
options:
|
||||
gelf-address: "udp://${GELF_HOST}:${GELF_PORT}"
|
||||
tag: "${COMPOSE_PROJECT_NAME}"
|
||||
labels: "com.docker.compose.project,com.docker.compose.service"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: <hersteller>/<image>:${APP_TAG}
|
||||
container_name: ${COMPOSE_PROJECT_NAME}
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
# ports: nur wenn Zugriff ohne Traefik nötig ist
|
||||
# - "${APP_PORT}:<intern>"
|
||||
extra_hosts: # nur wenn benötigt; ein Eintrag je Variable
|
||||
- "${EXTRA_HOST_1}"
|
||||
volumes:
|
||||
- ${DIR_DATA}:/<pfad-im-container>
|
||||
- ${DIR_CONFIG}:/<config-pfad>:ro
|
||||
networks:
|
||||
- proxy
|
||||
labels: # Traefik bleibt aus, bis TRAEFIK_ENABLE=true (s. 3.2)
|
||||
- "traefik.enable=${TRAEFIK_ENABLE:-false}"
|
||||
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik-net}"
|
||||
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${APP_FQDN:-${COMPOSE_PROJECT_NAME}.invalid}`)"
|
||||
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
|
||||
- "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=${APP_INTERNAL_PORT:-80}"
|
||||
# bei Bedarf manuell ergänzen: entrypoints, middlewares …
|
||||
logging: *default-logging
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:<intern>/<pfad>"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
name: ${TRAEFIK_NETWORK:-traefik-net}
|
||||
external: true
|
||||
```
|
||||
|
||||
### 3.2 Traefik-Labels: Dummy zuerst, Freischaltung per Schalter
|
||||
- Labels stehen **immer** direkt im Stackfile und werden je Stack manuell angepasst (kein `extends`, keine Auslagerung).
|
||||
- `traefik.enable=${TRAEFIK_ENABLE:-false}` → solange in `.env` nicht `TRAEFIK_ENABLE=true` steht, ignoriert Traefik den Container: **kein Router, keine Let's-Encrypt-Anfrage.**
|
||||
- Ablauf: Stack ohne Traefik hochfahren → Healthcheck `healthy`, Funktion geprüft, FQDN/DNS stimmt → erst dann `TRAEFIK_ENABLE=true` + `docker compose up -d`.
|
||||
- Dummy-FQDN `<stack>.invalid` (reservierte TLD) verhindert, dass ein vergessener Wert je eine echte Domain anfragt.
|
||||
- Mehrere Web-Services: Router-/Service-Namen eindeutig halten (`${COMPOSE_PROJECT_NAME}_<service>`).
|
||||
|
||||
### 3.3 `.env` / `.env.example`
|
||||
```dotenv
|
||||
# --- Stack ---
|
||||
COMPOSE_PROJECT_NAME=<stack> # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag)
|
||||
APP_TAG=<version>
|
||||
|
||||
# --- Logging (Graylog) ---
|
||||
GELF_HOST=<GELF_HOST>
|
||||
GELF_PORT=12201
|
||||
|
||||
# --- Traefik (Defaults: traefik-net / le – nur bei Abweichung setzen) ---
|
||||
TRAEFIK_ENABLE=false # erst auf true, wenn Stack sauber läuft (LE-Limits!)
|
||||
APP_FQDN=<app.domain.tld>
|
||||
APP_INTERNAL_PORT=<port>
|
||||
#TRAEFIK_NETWORK=traefik-net
|
||||
#TRAEFIK_CERTRESOLVER=le
|
||||
|
||||
# --- Hosts ---
|
||||
EXTRA_HOST_1=<fqdn>:<ip>
|
||||
|
||||
# --- Pfade (Bind-Mounts, relativ zum Stack) ---
|
||||
DIR_DATA=./data
|
||||
DIR_CONFIG=./config
|
||||
|
||||
# --- Image-spezifische Variablen (laut Orig-Doku, s. README) ---
|
||||
```
|
||||
|
||||
### 3.4 Geprüfte, aber verworfene Wege (Compose v5.0.2, 06.10.2026)
|
||||
- **`label_file:`** – keine Variablen im Schlüssel möglich (Router-Name!) und Werte werden nicht interpoliert. Unbrauchbar für Traefik.
|
||||
- **`extends` aus gemeinsamer `traefik.yml`** – technisch ok, vom Bediener abgelehnt: Labels gehören sichtbar und manuell anpassbar ins Stackfile.
|
||||
- **Separate `.traefik.env` per `COMPOSE_ENV_FILES` in `.env`** – wirkt nicht; nur als Shell-Variable oder per `--env-file` bei jedem Aufruf → fehleranfällig.
|
||||
|
||||
## 4. Hinweise zur Umsetzung
|
||||
- **`extra_hosts` in `.env`:** ja, möglich – Compose interpoliert Listenwerte. Ein Eintrag je Variable (`EXTRA_HOST_1=host01.example.org:192.0.2.10`). Schlüssel in Map-Syntax werden *nicht* interpoliert → Listen-Syntax verwenden. Leere Variable ergibt einen ungültigen Eintrag → Zeile nur aufnehmen, wenn genutzt.
|
||||
- **Healthcheck:** vorher prüfen, ob `wget`/`curl` im Image existieren (`docker run --rm --entrypoint sh <image> -c 'command -v wget curl'`). Endpunkt wählen, der im Normalbetrieb nicht fälschlich failt (z. B. Vault: `/ui/` statt `/v1/sys/health`, weil versiegelt = 503).
|
||||
- **`env_file: .env`** reicht auch Stack-Variablen (GELF_*, DIR_*, TRAEFIK_*) in den Container durch – ist so gewollt und in der Regel harmlos.
|
||||
- Kommentare im Stackfile nur für Nicht-Offensichtliches (wie im Vault-Healthcheck).
|
||||
|
||||
## 5. Stack-README (Pflicht)
|
||||
Jeder Stack bekommt ein `README.md` mit:
|
||||
1. **Zweck** und Zugriff (FQDN, ggf. Port).
|
||||
2. **Orig-Doku:** Links zu Image-Seite (Docker Hub / GHCR) und Hersteller-Doku, Abrufdatum.
|
||||
3. **ENV-Variablen laut Orig-Doku:** Tabelle `Variable | Bedeutung | Default | gesetzt in .env?` – vollständig aus der Herstellerdoku übernommen, nicht geraten.
|
||||
4. **Volumes/Pfade** laut Orig-Doku (welcher Container-Pfad persistiert was).
|
||||
5. **Ports** intern/extern, Healthcheck-Endpunkt und Begründung.
|
||||
6. **Besonderheiten** (Capabilities wie `IPC_LOCK`, Init-/Unseal-Schritte, Update-Hinweise, Abweichungen vom Standard).
|
||||
|
||||
Vor dem Schreiben eines Stacks die Orig-Doku per Web-Recherche holen (erlaubt laut `rules/web_search.md`).
|
||||
|
||||
## 6. Abnahme-Checkliste
|
||||
- [ ] Punkte 1–11 aus Abschnitt 1 erfüllt
|
||||
- [ ] Kein `-` und kein `__` in Projekt-, Service-, Container- und Router-Namen
|
||||
- [ ] sultan-Host: Port veröffentlicht, kein externes Traefik-Netz, `entrypoints`-Label (7.1)
|
||||
- [ ] Keine Hosts/IPs/Domains/Pfade/Secrets im Stackfile
|
||||
- [ ] `.env.example` vollständig, ohne Secrets
|
||||
- [ ] README mit Orig-Doku und ENV-Tabelle
|
||||
- [ ] `docker compose config` läuft fehlerfrei
|
||||
- [ ] Healthcheck wird nach Start `healthy` – **erst danach** `TRAEFIK_ENABLE=true`
|
||||
|
||||
## 7. Ergänzungen (25.10.2026)
|
||||
|
||||
### 7.1 sultan-Variante (Traefik auf anderer VM)
|
||||
Gilt für Hosts, deren Traefik woanders läuft und per [sultan](../context/knowledge/services/sultan.md) angebunden ist.
|
||||
- **Kein** externes Netz `proxy`/`traefik-net`, **kein** Label `traefik.docker.network`.
|
||||
- Web-Service **veröffentlicht** seinen Port: `ports: ["${APP_PORT}:<intern>"]` (Bindung `0.0.0.0`; `127.0.0.1` verwirft der sultan-Agent).
|
||||
- Label `traefik.http.routers.<name>.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}` gehört dazu; sultan prüft Entrypoint-/Resolver-Namen nicht.
|
||||
- `loadbalancer.server.port` = Port **im** Container (der Agent rechnet auf den Host-Port um).
|
||||
- Namen: kein doppelter Unterstrich `__` (Trenner bei sultan).
|
||||
- Wartung: `docker compose stop` → Wartungsseite; `down` → 404. Für Updates `up -d` (ersetzt Container) ist unkritisch.
|
||||
|
||||
### 7.2 Stacks mit mehreren Services / Datenbank
|
||||
- Datenbanken als **eigener Stack** (`postgres`), nie veröffentlicht. Der Stack legt ein benanntes Netz an (`postgres_net`);
|
||||
Anwendungen hängen es als `external: true` an. Je Anwendung eigene Datenbank + eigener User.
|
||||
- Innerhalb eines Stacks: `depends_on` mit `condition: service_healthy`.
|
||||
- Anwendung braucht Internet/Port → zusätzlich Netz `default`.
|
||||
|
||||
### 7.3 `.env`: Erzeugung und Geheimnisse
|
||||
- `.env.example` ist die **einzige Quelle** der Schlüssel. Platzhalter:
|
||||
`@@SECRET@@` → beim ersten Deployment zufällig erzeugt (je Schlüssel eigener Wert); `<…>` → vom Betreiber zu setzen.
|
||||
- Die echte `.env` erzeugt Ansible **nur, wenn sie fehlt**; danach gehört sie dem Betreiber (kein Überschreiben).
|
||||
Später ersetzen Vault-Einträge die Werte (`rules/secrets.md`).
|
||||
- Werte, die mehrere Stacks brauchen (DB-Passwort), stehen in der `.env` der **Anwendung**; Ansible liest sie dort und legt damit DB/User an (über stdin, nie als Argument).
|
||||
|
||||
### 7.4 Deployment per Ansible
|
||||
- Rolle `compose_stack` (`ansible/roles/compose_stack`): Stack-Verzeichnis `<STACK_BASE>/<stack>/`, Dateien aus `stacks/<stack>/`,
|
||||
Bind-Mount-Ziele mit Eigentümer/Rechten, `.env` (s. 7.3), `community.docker.docker_compose_v2` mit `wait`.
|
||||
- Datenbank je Anwendung: Rolle `pg_app_db`.
|
||||
|
||||
### 7.5 Rechte, Dateisystem
|
||||
- Bind-Mount-Ziele mit dem Eigentümer des Containers anlegen (z. B. `1000:1000`); im Stack-README dokumentieren.
|
||||
- Auf btrfs: Datenbank-Verzeichnisse vor dem ersten Start `chattr +C` (kein Copy-on-Write, weniger Fragmentierung; Prüfsummen entfallen dort).
|
||||
|
||||
### 7.6 Backup & Update
|
||||
- Datenbanken per Dump sichern (`pg_dumpall`/`pg_dump` vor dem Borgmatic-Lauf) – das reine Kopieren des Datenverzeichnisses im Betrieb ist kein konsistentes Backup.
|
||||
- Update: neuen Tag in `.env` → Backup → `docker compose pull && docker compose up -d`. Rückweg: alter Tag + Restore.
|
||||
**Kein automatisches `docker system prune -a`** – das alte Image bleibt für den schnellen Rückweg.
|
||||
|
||||
## Festgelegte Werte
|
||||
`<STACK_BASE>`, `TRAEFIK_NETWORK`, `TRAEFIK_CERTRESOLVER`, `GELF_HOST`/`GELF_PORT` stehen in [`ai/context/environment.md`](../context/environment.md).
|
||||
|
||||
## Änderungsprotokoll
|
||||
- 07.10.2026 – übernommen und verallgemeinert (Mandantenwerte → Platzhalter, Header-Pflicht ergänzt).
|
||||
- 23.10.2026 – Mandantenbezug entfernt, Werte nur noch in `environment.md`; Verweis auf `web_search.md` korrigiert.
|
||||
- 25.10.2026 – § 7: sultan-Variante, Mehrfach-Services/DB, `.env`-Erzeugung (`@@SECRET@@`), Deployment per Ansible, Rechte/btrfs, Backup/Update ohne Prune – Entscheidungen des Betreibers vom 23.–25.10.2026.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Markdown & Dokumentation
|
||||
|
||||
## 1. Allgemein
|
||||
- Eine `#`-Überschrift pro Datei (= Titel), darunter Gliederung mit `##`/`###`. Optional direkt unter dem Titel ein Zitatblock `> Stand: TT.MM.JJJJ · …`.
|
||||
- Kurz, konkret, prüfbar: Tabellen für Fakten, Checklisten (`- [ ]`) für Abläufe/Offenes, Code-Blöcke mit Sprache (` ```bash `).
|
||||
- Datumsangaben im Text absolut `TT.MM.JJJJ`; in Frontmatter/Dateinamen ISO `JJJJ-MM-TT`.
|
||||
- Relative Links innerhalb der Basis (`[bash.md](../common/bash.md)`), keine absoluten Pfade.
|
||||
- Quellen als Link + Abrufdatum. Unbelegtes als „Einschätzung“ kennzeichnen.
|
||||
- Platzhalter: `<NAME>` (auszufüllen), `<!-- TODO: … -->` (offener Punkt); Vorlagen-Variablen für Scripts: `@@NAME@@`.
|
||||
- Diagramme: Mermaid oder ASCII im Code-Block (lesbar ohne Renderer); keine Binärbilder ohne Not.
|
||||
|
||||
## 2. READMEs
|
||||
Jeder Ordner der Basis hat eine `README.md`: Zweck, Inhalt (Tabelle Datei → Inhalt), Pflege (wer, wann, wie).
|
||||
|
||||
## 3. Knowledge-Einträge
|
||||
Format und Ablage: [`../context/knowledge/README.md`](../context/knowledge/README.md); Vorlage: [`templates/markdown/knowledge_entry.md`](templates/markdown/knowledge_entry.md).
|
||||
|
||||
## 4. `context.md` (Arbeitsgedächtnis im Root)
|
||||
Abschnitte nicht umbenennen (Agenten suchen danach):
|
||||
Metadaten · Ziel · Stand · Entscheidungen · Offene Fragen · Nächster Schritt · Sitzungsprotokoll.
|
||||
|
||||
## Änderungsprotokoll
|
||||
- 07.10.2026 – angelegt.
|
||||
- 23.10.2026 – `context.md` liegt im Root (Einzelprojekt).
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------
|
||||
## @file .env.sh
|
||||
## @brief Ansible-Kontext für dieses Repo setzen (Pfade + Secret-Store)
|
||||
## @author <AUTOR – aus ai/context/responsible.md>
|
||||
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
|
||||
## @version 1.0.0 (07.10.2026)
|
||||
## @usage source ansible/.env.sh (bash und zsh, aus beliebigem Verzeichnis)
|
||||
#-------------------------------------------------------------------------------
|
||||
# Pfad dieser Datei – bash: BASH_SOURCE, zsh: %x
|
||||
_ANS_SRC="${BASH_SOURCE[0]:-${(%):-%x}}"
|
||||
_ANS_DIR="$(cd "$(dirname "$_ANS_SRC")" && pwd -P)"
|
||||
|
||||
# --- Ansible-Pfade (fremden Kontext zurücksetzen) ---
|
||||
unset ANSIBLE_CONFIG ANSIBLE_INVENTORY ANSIBLE_ROLES_PATH ANSIBLE_VAULT_PASSWORD_FILE
|
||||
export ANSIBLE_BASE_PATH="$_ANS_DIR"
|
||||
export ANSIBLE_CONFIG="$ANSIBLE_BASE_PATH/ansible.cfg"
|
||||
export ANSIBLE_INVENTORY="$ANSIBLE_BASE_PATH/inv"
|
||||
export ANSIBLE_ROLES_PATH="$ANSIBLE_BASE_PATH/roles"
|
||||
export ANS_PLAYDIR="$ANSIBLE_BASE_PATH/playbooks"
|
||||
export ANS_FILES="$ANSIBLE_BASE_PATH/files"
|
||||
export ANS_DEFAULT_HOSTS="<STANDARDHOST>" # nur Doku – Default steht im Play
|
||||
|
||||
# --- Secret-Store (HashiCorp Vault, bevorzugt) – Token aus ~/.vault-token ---
|
||||
export VAULT_ADDR="<VAULT_ADDR>"
|
||||
export ANSIBLE_HASHI_VAULT_ADDR="$VAULT_ADDR"
|
||||
export ANSIBLE_HASHI_VAULT_AUTH_METHOD="token"
|
||||
export ANS_VAULT_KV_MOUNT="secret"
|
||||
export ANS_VAULT_KV_PATH="<projekt>"
|
||||
|
||||
# --- Ansible Vault (nur Fallback, Passwortdatei außerhalb des Repos) ---
|
||||
[ -f "$HOME/.ans-secret-<projekt>" ] && export ANSIBLE_VAULT_PASSWORD_FILE="$HOME/.ans-secret-<projekt>"
|
||||
|
||||
unset _ANS_DIR _ANS_SRC
|
||||
echo "Ansible-Kontext aktiv: ANSIBLE_BASE_PATH=$ANSIBLE_BASE_PATH"
|
||||
@@ -0,0 +1,24 @@
|
||||
## @file ansible.cfg – Pfade kommen aus .env.sh (source ansible/.env.sh)
|
||||
[defaults]
|
||||
inventory=$ANSIBLE_BASE_PATH/inv
|
||||
roles_path=$ANSIBLE_BASE_PATH/roles
|
||||
log_path=$ANSIBLE_BASE_PATH/ansible.log
|
||||
fact_caching=jsonfile
|
||||
fact_caching_connection=$ANSIBLE_BASE_PATH/fact_caching
|
||||
fact_caching_prefix=facts_
|
||||
fact_caching_timeout=86400
|
||||
gathering=smart
|
||||
interpreter_python=auto_silent
|
||||
retry_files_enabled=False
|
||||
stdout_callback=default
|
||||
callback_result_format=yaml
|
||||
|
||||
[privilege_escalation]
|
||||
become=True
|
||||
become_method=sudo
|
||||
become_user=root
|
||||
become_ask_pass=False
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args=-o ControlMaster=auto -o ControlPersist=60s
|
||||
pipelining=True
|
||||
@@ -0,0 +1,10 @@
|
||||
{#-
|
||||
## @file example.conf.j2
|
||||
## @brief Konfiguration für <Dienst> – verwaltet von Ansible (Rolle example)
|
||||
## @author <AUTOR – aus ai/context/responsible.md>
|
||||
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
|
||||
## @version 0.1.0 (TT.MM.JJJJ)
|
||||
-#}
|
||||
# {{ ansible_managed }}
|
||||
# Nicht von Hand ändern – Änderungen über Ansible.
|
||||
password = {{ example_secrets.password | default('') }}
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
## @brief Globale Variablen für alle Hosts
|
||||
ansible_user: "<ANSIBLE_USER>"
|
||||
|
||||
# Ablage für Templates/Dateien (aus .env.sh, Fallback relativ zum Repo)
|
||||
ans_files: "{{ lookup('env', 'ANS_FILES') | default(playbook_dir ~ '/../files', true) }}"
|
||||
|
||||
# Secret-Store (KV v2) – Werte aus .env.sh
|
||||
ans_vault_kv_mount: "{{ lookup('env', 'ANS_VAULT_KV_MOUNT') | default('secret', true) }}"
|
||||
ans_vault_kv_path: "{{ lookup('env', 'ANS_VAULT_KV_PATH') | default('<projekt>', true) }}"
|
||||
@@ -0,0 +1,3 @@
|
||||
## @file inv/lab – Inventar Testumgebung (Hosts aus ai/context/knowledge/systems/)
|
||||
[example]
|
||||
<STANDARDHOST>
|
||||
@@ -0,0 +1,16 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
## @file example.yml
|
||||
## @brief <Dienst> installieren + konfigurieren (Rolle example, Templates aus $ANS_FILES/example)
|
||||
## @author <AUTOR – aus ai/context/responsible.md>
|
||||
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
|
||||
## @version 0.1.0 (TT.MM.JJJJ)
|
||||
## @usage ansible-playbook $ANS_PLAYDIR/example.yml [-e hostlist=host1,gruppe]
|
||||
## Tags: install | config | verify Trockenlauf: --check --diff
|
||||
## ohne Secret-Store: -e example_secrets_source=vars -e @<testsecrets.yml>
|
||||
## @requires source ansible/.env.sh und vault login
|
||||
#-------------------------------------------------------------------------------
|
||||
---
|
||||
- name: Example
|
||||
hosts: "{{ hostlist | default('<STANDARDHOST>') }}"
|
||||
roles:
|
||||
- example
|
||||
@@ -0,0 +1,28 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
## @file mini_check.yml
|
||||
## @brief Erreichbarkeit prüfen und Kernfakten anzeigen; -e check_mode=setup sammelt Facts neu
|
||||
## @author <AUTOR – aus ai/context/responsible.md>
|
||||
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
|
||||
## @version 1.0.0 (07.10.2026)
|
||||
## @usage ansible-playbook $ANS_PLAYDIR/mini_check.yml [-e hostlist=…] [-e check_mode=setup]
|
||||
#-------------------------------------------------------------------------------
|
||||
---
|
||||
- name: Mini-Check
|
||||
hosts: "{{ hostlist | default('<STANDARDHOST>') }}"
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Erreichbarkeit prüfen
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Facts neu sammeln
|
||||
ansible.builtin.setup:
|
||||
when: (check_mode | default('')) == 'setup'
|
||||
|
||||
- name: Facts aus Cache/Host laden
|
||||
ansible.builtin.setup:
|
||||
gather_subset: [min]
|
||||
when: ansible_facts['distribution'] is not defined
|
||||
|
||||
- name: Kernfakten anzeigen
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ inventory_hostname }}: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}, Kernel {{ ansible_facts['kernel'] }}"
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
## @brief Defaults der Rolle example – alle Variablen mit Präfix example_
|
||||
|
||||
example_packages: [example] # zu installierende Pakete
|
||||
example_service: example # systemd-Unit
|
||||
example_config_file: /etc/example/example.conf # Ziel der eigenen Konfiguration
|
||||
example_config_check_cmd: "example --check-config" # Prüfbefehl vor jedem Restart
|
||||
example_smoke_test: true # verify.yml ausführen
|
||||
example_secrets_source: vault # vault | vars (Test)
|
||||
example_test_secrets: {} # nur mit secrets_source=vars, Datei außerhalb des Repos
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
## @brief Handler-Kette: erst Konfiguration prüfen, dann neu starten
|
||||
|
||||
- name: Konfiguration prüfen
|
||||
ansible.builtin.command: "{{ example_config_check_cmd }}"
|
||||
changed_when: false
|
||||
listen: example config changed
|
||||
|
||||
- name: Dienst neu starten
|
||||
ansible.builtin.service:
|
||||
name: "{{ example_service }}"
|
||||
state: restarted
|
||||
listen: example config changed
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: example
|
||||
author: "<AUTOR – aus ai/context/responsible.md>"
|
||||
description: "<Dienst> installieren und konfigurieren"
|
||||
license: proprietary
|
||||
min_ansible_version: "2.15"
|
||||
platforms:
|
||||
- name: Debian
|
||||
versions: [trixie]
|
||||
dependencies: []
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
## @brief Eigene Konfigurationsdateien schreiben (Templates aus files/example/)
|
||||
|
||||
- name: Konfiguration schreiben
|
||||
ansible.builtin.template:
|
||||
src: "{{ ans_files }}/example/example.conf.j2"
|
||||
dest: "{{ example_config_file }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
backup: true
|
||||
diff: false # Template enthält Geheimnisse – nicht entfernen
|
||||
notify: example config changed
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
## @brief Pakete installieren
|
||||
|
||||
- name: Pakete installieren
|
||||
ansible.builtin.apt:
|
||||
name: "{{ example_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
## @brief Rolle example – Ablauf: prüfen → Geheimnisse → installieren → konfigurieren → testen
|
||||
|
||||
- name: Plattform prüfen (Debian >= 13 erwartet)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts['os_family'] == 'Debian'
|
||||
- ansible_facts['distribution_major_version'] | int >= 13
|
||||
fail_msg: "{{ inventory_hostname }}: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }} – Rolle ist für Debian >= 13 gebaut."
|
||||
quiet: true
|
||||
tags: [always]
|
||||
|
||||
- name: Geheimnisse laden
|
||||
ansible.builtin.import_tasks: secrets.yml
|
||||
tags: [always]
|
||||
|
||||
- name: Installieren
|
||||
ansible.builtin.import_tasks: install.yml
|
||||
tags: [install]
|
||||
|
||||
- name: Konfigurieren
|
||||
ansible.builtin.import_tasks: config.yml
|
||||
tags: [config]
|
||||
|
||||
- name: Handler jetzt ausführen (Config-Check vor Restart)
|
||||
ansible.builtin.meta: flush_handlers
|
||||
tags: [always]
|
||||
|
||||
- name: Dienst aktiv und gestartet
|
||||
ansible.builtin.service:
|
||||
name: "{{ example_service }}"
|
||||
state: started
|
||||
enabled: true
|
||||
tags: [install, config]
|
||||
|
||||
- name: Smoke-Test
|
||||
ansible.builtin.import_tasks: verify.yml
|
||||
when: example_smoke_test | bool
|
||||
tags: [verify]
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
## @brief Geheimnisse aus dem Secret-Store (Default) oder aus Testvariablen laden
|
||||
|
||||
- name: Geheimnisse aus dem Secret-Store lesen
|
||||
ansible.builtin.set_fact:
|
||||
example_secrets: "{{ lookup('community.hashi_vault.vault_kv2_get', ans_vault_kv_path ~ '/example', engine_mount_point=ans_vault_kv_mount).secret }}"
|
||||
when: example_secrets_source == 'vault'
|
||||
no_log: true
|
||||
|
||||
- name: Geheimnisse aus Testvariablen übernehmen
|
||||
ansible.builtin.set_fact:
|
||||
example_secrets: "{{ example_test_secrets }}"
|
||||
when: example_secrets_source == 'vars'
|
||||
no_log: true
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
## @brief Smoke-Test: Dienst läuft und antwortet
|
||||
|
||||
- name: Dienststatus abfragen
|
||||
ansible.builtin.command: systemctl is-active {{ example_service }}
|
||||
register: example_active
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: example_active.stdout != 'active'
|
||||
@@ -0,0 +1,21 @@
|
||||
# .env – Einstellungen für die Scripts in diesem Verzeichnis
|
||||
# Wird von b_init gesourct (Bash-Syntax). Echte .env nie ins Git – nur .env.example.
|
||||
# Vorrang: Default < .env < Umgebung (export / VAR=… ./script.sh) < Kommandozeile
|
||||
# Relative Pfade gelten relativ zum Script-Verzeichnis, nicht zum aufrufenden $PWD.
|
||||
|
||||
## Logging ---------------------------------------------------------------------
|
||||
B_LOG_LEVEL=info # debug | info | warn | error (-d setzt debug)
|
||||
B_LOG_CONSOLE=1 # 0 = still (-q)
|
||||
B_LOG_FILE= # z. B. "log/${SCRIPT_NAME%.sh}.log"; leer = keine Datei (-l DATEI)
|
||||
B_LOG_FILE_LEVEL= # leer = wie B_LOG_LEVEL; z. B. debug für ausführliche Logdatei
|
||||
B_LOG_COLOR=auto # auto | 1 | 0
|
||||
|
||||
## Optionen vorbelegen (überschreibt den Default aus der Definition) ------------
|
||||
#OPTION_DRY_RUN=1
|
||||
|
||||
## Zusätzliche Optionen für alle Scripts dieses Verzeichnisses – immer mit += ---
|
||||
# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value
|
||||
#B_OPTS+=( "k|keep|flag|0|Zwischendateien behalten" )
|
||||
|
||||
## Projektwerte – mit ${VAR:-default}, damit die Umgebung gewinnt ---------------
|
||||
#DIR_BACKUP="${DIR_BACKUP:-/srv/backup}"
|
||||
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------
|
||||
## @file _functions.sh
|
||||
## @brief Gemeinsame Bash-Basis: .env laden, Optionen, Logging, Usage, Trap
|
||||
## @author <AUTOR – siehe ai/context/responsible.md>
|
||||
## @owner <VERANTWORTLICH – siehe ai/context/responsible.md>
|
||||
## @version 1.0.0 (07.10.2026)
|
||||
##
|
||||
## @usage source "${SCRIPT_DIR}/_functions.sh"; B_OPTS+=(…); b_init "$@"; set -- "${ARGS[@]}"
|
||||
## @note Nur sourcen, nicht ausführen. Benötigt bash >= 4.4.
|
||||
## Kopien NICHT lokal ändern – Projektspezifisches gehört in
|
||||
## _functions.local.sh (wird automatisch geladen, falls vorhanden).
|
||||
## Standard: ai/common/bash.md
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
echo "_functions.sh wird gesourct, nicht ausgeführt." >&2
|
||||
exit 2
|
||||
fi
|
||||
[[ -n "${_B_LOADED:-}" ]] && return 0
|
||||
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
|
||||
echo "FEHLER: bash >= 4.4 nötig (gefunden: ${BASH_VERSION})." >&2
|
||||
return 1
|
||||
fi
|
||||
_B_LOADED=1
|
||||
B_FUNCTIONS_VERSION="1.0.0"
|
||||
|
||||
# --- Pfade (pfadsicher, unabhängig vom aufrufenden $PWD) ----------------------
|
||||
# Das Script setzt SCRIPT_PATH/DIR/NAME selbst; hier nur Fallback.
|
||||
: "${SCRIPT_PATH:=$(readlink -f -- "${BASH_SOURCE[1]:-$0}")}"
|
||||
: "${SCRIPT_DIR:=${SCRIPT_PATH%/*}}"
|
||||
: "${SCRIPT_NAME:=${SCRIPT_PATH##*/}}"
|
||||
B_LIB_DIR="$(readlink -f -- "${BASH_SOURCE[0]}")"
|
||||
B_LIB_DIR="${B_LIB_DIR%/*}"
|
||||
B_CALL_DIR="${PWD}" # Verzeichnis, aus dem aufgerufen wurde
|
||||
B_TMP_DIR="" # wird von b_tmpdir gesetzt
|
||||
|
||||
# --- Standard-Optionen (gelten für jedes Script) ------------------------------
|
||||
# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value
|
||||
# Variable im Script: OPTION_<LANG> (Großbuchstaben, '-' → '_'), z. B. OPTION_DRY_RUN
|
||||
B_OPTS_STD=(
|
||||
"h|help|flag|0|diese Hilfe anzeigen"
|
||||
"d|debug|flag|0|ausführliche Ausgabe (Log-Level debug)"
|
||||
"q|quiet|flag|0|keine Konsolenausgabe (Logdatei bleibt)"
|
||||
"n|dry-run|flag|0|nur anzeigen, nichts ändern"
|
||||
"l|logfile|value:DATEI||zusätzlich in DATEI loggen"
|
||||
)
|
||||
declare -p B_OPTS &>/dev/null || B_OPTS=() # Script/.env hängen mit B_OPTS+=(…) an
|
||||
declare -ga ARGS=() # Positionsargumente nach b_init
|
||||
|
||||
declare -grA _B_LEVELS=([debug]=10 [info]=20 [warn]=30 [error]=40)
|
||||
declare -gA _B_OPT_SHORT=() _B_OPT_TYPE=() _B_OPT_META=() _B_OPT_DEFAULT=() _B_OPT_DESC=()
|
||||
declare -ga _B_OPT_ORDER=()
|
||||
|
||||
#===============================================================================
|
||||
# Ausgabe / Logging
|
||||
#===============================================================================
|
||||
|
||||
## @brief Zentrale Ausgabe auf Konsole und/oder Logdatei, gefiltert nach Level
|
||||
## @param $1 Level: debug | info | warn | error
|
||||
## @param $@ Text
|
||||
## @return 0 ok, 2 unbekanntes Level
|
||||
blog() {
|
||||
local level="${1:-}"; shift || true
|
||||
local n="${_B_LEVELS[$level]:-}" msg="$*" c="" r=""
|
||||
[[ -n "$n" ]] || { printf 'blog: unbekanntes Level "%s"\n' "$level" >&2; return 2; }
|
||||
|
||||
if (( ${B_LOG_CONSOLE:-1} )) && (( n >= ${_B_LEVELS[${B_LOG_LEVEL:-info}]:-20} )); then
|
||||
if [[ "${B_LOG_COLOR:-auto}" == 1 || ( "${B_LOG_COLOR:-auto}" == auto && -t 2 ) ]]; then
|
||||
r=$'\e[0m'
|
||||
case "$level" in debug) c=$'\e[2m' ;; warn) c=$'\e[33m' ;; error) c=$'\e[31m' ;; esac
|
||||
fi
|
||||
case "$level" in
|
||||
info) printf '%s\n' "$msg" ;;
|
||||
debug) printf '%s[DEBUG] %s%s\n' "$c" "$msg" "$r" >&2 ;;
|
||||
warn) printf '%s[WARN] %s%s\n' "$c" "$msg" "$r" >&2 ;;
|
||||
error) printf '%s[FEHLER] %s%s\n' "$c" "$msg" "$r" >&2 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ -n "${B_LOG_FILE:-}" ]] \
|
||||
&& (( n >= ${_B_LEVELS[${B_LOG_FILE_LEVEL:-${B_LOG_LEVEL:-info}}]:-20} )); then
|
||||
printf '%(%Y-%m-%d %H:%M:%S)T %-5s %s[%d] %s\n' -1 "${level^^}" "$SCRIPT_NAME" "$$" "$msg" \
|
||||
>> "$B_LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
## @brief Ersatz für echo: Info-Meldung über blog
|
||||
## @param $@ Text
|
||||
## @return 0
|
||||
becho() { blog info "$@"; }
|
||||
|
||||
## @brief Debug-Meldung (nur bei -d bzw. B_LOG_LEVEL=debug)
|
||||
## @param $@ Text
|
||||
## @return 0
|
||||
bdebug() { blog debug "$@"; }
|
||||
|
||||
## @brief Warnung (stderr)
|
||||
## @param $@ Text
|
||||
## @return 0
|
||||
bwarn() { blog warn "$@"; }
|
||||
|
||||
## @brief Fehlermeldung (stderr)
|
||||
## @param $@ Text
|
||||
## @return 0
|
||||
berror() { blog error "$@"; }
|
||||
|
||||
## @brief Fehlermeldung ausgeben und Script beenden (EXIT-Trap räumt auf)
|
||||
## @param $1 Text
|
||||
## @param $2 Exit-Code (Default 1; 2 = Aufruffehler, zeigt Hilfe-Hinweis)
|
||||
## @return – (beendet das Script)
|
||||
bdie() {
|
||||
local rc="${2:-1}"
|
||||
berror "${1:-Abbruch}"
|
||||
(( rc == 2 )) && blog info "Hilfe: ${SCRIPT_NAME} --help"
|
||||
exit "$rc"
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
# Hilfen für den Script-Alltag
|
||||
#===============================================================================
|
||||
|
||||
## @brief Befehl ausführen – oder bei --dry-run nur anzeigen
|
||||
## @param $@ Befehl mit Argumenten (kein String, kein eval)
|
||||
## @return Exit-Code des Befehls, 0 im Dry-Run
|
||||
brun() {
|
||||
if (( ${OPTION_DRY_RUN:-0} )); then
|
||||
becho "[dry-run] $(printf '%q ' "$@")"
|
||||
return 0
|
||||
fi
|
||||
bdebug "run: $(printf '%q ' "$@")"
|
||||
"$@"
|
||||
}
|
||||
|
||||
## @brief Prüft, ob Programme vorhanden sind
|
||||
## @param $@ Programmnamen
|
||||
## @return 0 alle da, 1 mindestens eins fehlt (wird gemeldet)
|
||||
b_require() {
|
||||
local cmd rc=0
|
||||
for cmd in "$@"; do
|
||||
command -v "$cmd" &>/dev/null || { berror "Programm fehlt: $cmd"; rc=1; }
|
||||
done
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
## @brief Prüft, ob Variablen gesetzt und nicht leer sind
|
||||
## @param $@ Variablennamen
|
||||
## @return 0 alle gesetzt, 1 mindestens eine fehlt (wird gemeldet)
|
||||
b_require_vars() {
|
||||
local v rc=0
|
||||
for v in "$@"; do
|
||||
[[ -n "${!v:-}" ]] || { berror "Variable nicht gesetzt: $v (in .env oder Umgebung)"; rc=1; }
|
||||
done
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
## @brief Legt ein temporäres Verzeichnis an (B_TMP_DIR), wird beim Exit gelöscht
|
||||
## @return 0 ok, 1 mktemp fehlgeschlagen
|
||||
b_tmpdir() {
|
||||
[[ -n "$B_TMP_DIR" ]] && return 0
|
||||
B_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.XXXXXX")" || return 1
|
||||
bdebug "Temp-Verzeichnis: $B_TMP_DIR"
|
||||
}
|
||||
|
||||
## @brief Macht einen Pfad absolut; relative Pfade gelten relativ zu $2
|
||||
## @param $1 Pfad
|
||||
## @param $2 Basisverzeichnis (Default: SCRIPT_DIR)
|
||||
## @return 0; Ergebnis auf stdout
|
||||
b_abspath() {
|
||||
local p="$1" base="${2:-$SCRIPT_DIR}"
|
||||
[[ "$p" == /* ]] || p="${base%/}/${p}"
|
||||
printf '%s\n' "$p"
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
# Usage aus der Präambel
|
||||
#===============================================================================
|
||||
|
||||
## @brief Gibt die Präambel des Scripts als Hilfe aus ('## ' wird entfernt,
|
||||
## die Zeile '## @OPTIONS@' wird durch die Optionsliste ersetzt)
|
||||
## @return 0
|
||||
b_usage() {
|
||||
local line part=0
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^#-{10,} ]]; then
|
||||
(( ++part >= 2 )) && break
|
||||
continue
|
||||
fi
|
||||
(( part == 1 )) && [[ "$line" == '##'* ]] || continue
|
||||
line="${line#'##'}"; line="${line# }"
|
||||
if [[ "$line" == *'@OPTIONS@'* ]]; then _b_opts_help; else printf '%s\n' "$line"; fi
|
||||
done < "$SCRIPT_PATH"
|
||||
return 0
|
||||
}
|
||||
|
||||
## @brief Optionsliste für die Hilfe (aus B_OPTS_STD + B_OPTS)
|
||||
## @return 0
|
||||
_b_opts_help() {
|
||||
local long s left d
|
||||
for long in "${_B_OPT_ORDER[@]}"; do
|
||||
s="${_B_OPT_SHORT[$long]}"
|
||||
left="${s:+-$s | }--${long}"
|
||||
[[ "${_B_OPT_TYPE[$long]}" == value ]] && left+=" ${_B_OPT_META[$long]}"
|
||||
d="${_B_OPT_DEFAULT[$long]}"
|
||||
if [[ "${_B_OPT_TYPE[$long]}" == value && -n "$d" ]]; then
|
||||
printf ' %-28s %s (Default: %s)\n' "$left" "${_B_OPT_DESC[$long]}" "$d"
|
||||
else
|
||||
printf ' %-28s %s\n' "$left" "${_B_OPT_DESC[$long]}"
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
# Optionen
|
||||
#===============================================================================
|
||||
|
||||
## @brief Variablenname zu einer Langoption: dry-run → OPTION_DRY_RUN
|
||||
## @param $1 Langname
|
||||
## @return 0; Name auf stdout
|
||||
_b_optvar() { local n="${1^^}"; printf 'OPTION_%s\n' "${n//-/_}"; }
|
||||
|
||||
## @brief Baut die Optionstabellen aus B_OPTS_STD + B_OPTS und prüft sie
|
||||
## @return 0 ok, 2 Definitionsfehler
|
||||
_b_opts_build() {
|
||||
local def s long type meta deflt desc rest
|
||||
local -A seen_short=()
|
||||
_B_OPT_ORDER=()
|
||||
for def in "${B_OPTS_STD[@]}" "${B_OPTS[@]}"; do
|
||||
IFS='|' read -r s long type deflt desc rest <<< "$def"
|
||||
meta="${type#*:}"; type="${type%%:*}"; [[ "$meta" == "$type" ]] && meta="WERT"
|
||||
if [[ -z "$long" || ! "$long" =~ ^[a-z][a-z0-9-]*$ || ! "$type" =~ ^(flag|value)$ || -n "$rest" ]]; then
|
||||
berror "Ungültige Optionsdefinition: \"$def\" (kurz|lang|flag|value[:META]|default|Text)"
|
||||
return 2
|
||||
fi
|
||||
if [[ -n "$s" ]]; then
|
||||
[[ -n "${seen_short[$s]:-}" && "${seen_short[$s]}" != "$long" ]] && {
|
||||
berror "Kurzoption -$s doppelt vergeben (${seen_short[$s]}, $long)"; return 2; }
|
||||
seen_short[$s]="$long"
|
||||
fi
|
||||
[[ -n "${_B_OPT_TYPE[$long]:-}" ]] || _B_OPT_ORDER+=("$long")
|
||||
_B_OPT_SHORT[$long]="$s"; _B_OPT_TYPE[$long]="$type"; _B_OPT_META[$long]="$meta"
|
||||
_B_OPT_DEFAULT[$long]="$deflt"; _B_OPT_DESC[$long]="$desc"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
## @brief Setzt OPTION_<LANG> für eine erkannte Option
|
||||
## @param $1 Langname
|
||||
## @param $2 Wert
|
||||
## @return 0 ok, 2 ungültiger Wert
|
||||
_b_opt_set() {
|
||||
local long="$1" val="$2" var
|
||||
var="$(_b_optvar "$long")"
|
||||
if [[ "${_B_OPT_TYPE[$long]}" == flag && ! "$val" =~ ^[01]$ ]]; then
|
||||
berror "Option --$long erwartet 0 oder 1, nicht \"$val\""
|
||||
return 2
|
||||
fi
|
||||
printf -v "$var" '%s' "$val"
|
||||
_B_CLI_SET[$long]=1
|
||||
return 0
|
||||
}
|
||||
|
||||
## @brief Parst die Kommandozeile → OPTION_* und ARGS (Positionsargumente)
|
||||
## @param $@ Argumente des Scripts
|
||||
## @return 0 ok, 2 Aufruffehler
|
||||
_b_opts_parse() {
|
||||
local arg name val s i long
|
||||
ARGS=()
|
||||
while (( $# )); do
|
||||
arg="$1"; shift
|
||||
case "$arg" in
|
||||
--) ARGS+=("$@"); break ;;
|
||||
--?*)
|
||||
name="${arg#--}"; val=""
|
||||
if [[ "$name" == *=* ]]; then val="${name#*=}"; name="${name%%=*}"; fi
|
||||
[[ -n "${_B_OPT_TYPE[$name]:-}" ]] || { berror "Unbekannte Option: --$name"; return 2; }
|
||||
if [[ "${_B_OPT_TYPE[$name]}" == value && "$arg" != *=* ]]; then
|
||||
(( $# )) || { berror "Option --$name braucht einen Wert"; return 2; }
|
||||
val="$1"; shift
|
||||
elif [[ "${_B_OPT_TYPE[$name]}" == flag && "$arg" != *=* ]]; then
|
||||
val=1
|
||||
fi
|
||||
_b_opt_set "$name" "$val" || return 2
|
||||
;;
|
||||
-?*)
|
||||
for (( i = 1; i < ${#arg}; i++ )); do
|
||||
s="${arg:i:1}"; long=""
|
||||
for name in "${_B_OPT_ORDER[@]}"; do
|
||||
[[ "${_B_OPT_SHORT[$name]}" == "$s" ]] && { long="$name"; break; }
|
||||
done
|
||||
[[ -n "$long" ]] || { berror "Unbekannte Option: -$s"; return 2; }
|
||||
if [[ "${_B_OPT_TYPE[$long]}" == value ]]; then
|
||||
val="${arg:i+1}" # -lDATEI oder -l DATEI
|
||||
if [[ -z "$val" ]]; then
|
||||
(( $# )) || { berror "Option -$s braucht einen Wert"; return 2; }
|
||||
val="$1"; shift
|
||||
fi
|
||||
_b_opt_set "$long" "$val" || return 2
|
||||
break
|
||||
fi
|
||||
_b_opt_set "$long" 1 || return 2
|
||||
done
|
||||
;;
|
||||
*) ARGS+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
# Trap / Aufräumen
|
||||
#===============================================================================
|
||||
|
||||
## @brief EXIT-Trap: ruft cleanup() des Scripts (falls definiert), löscht B_TMP_DIR
|
||||
## @return – (beendet mit dem ursprünglichen Exit-Code)
|
||||
_b_on_exit() {
|
||||
local rc=$?
|
||||
trap - EXIT
|
||||
set +e
|
||||
if declare -F cleanup &>/dev/null; then
|
||||
cleanup "$rc" || bwarn "cleanup() meldete Fehler"
|
||||
fi
|
||||
[[ -n "$B_TMP_DIR" && -d "$B_TMP_DIR" ]] && rm -rf -- "$B_TMP_DIR"
|
||||
bdebug "Ende ${SCRIPT_NAME} (rc=${rc})"
|
||||
exit "$rc"
|
||||
}
|
||||
|
||||
## @brief ERR-Trap: meldet fehlgeschlagenen Befehl mit Funktion und Zeile
|
||||
## (bewusstes 'return N' hat die Funktion schon selbst gemeldet → nur debug)
|
||||
## @param $1 Zeilennummer
|
||||
## @param $2 Befehl
|
||||
## @return 0 (Abbruch übernimmt set -e)
|
||||
_b_on_err() {
|
||||
local rc=$? lvl=error
|
||||
[[ "$2" == return* ]] && lvl=debug
|
||||
blog "$lvl" "rc=${rc} in ${FUNCNAME[1]:-main}() Zeile ${1}: ${2}"
|
||||
return 0
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
# Einstieg
|
||||
#===============================================================================
|
||||
|
||||
## @brief Initialisiert das Script: Trap, .env, Defaults, Optionen, Logging, Hilfe
|
||||
## Vorrang: Default < .env < Umgebung (B_*, OPTION_*) < Kommandozeile
|
||||
## @param $@ Argumente des Scripts (danach: set -- "${ARGS[@]}")
|
||||
## @return 0 ok; beendet bei --help (0) oder Aufruffehler (2)
|
||||
b_init() {
|
||||
local v long var env_file
|
||||
local -A env_keep=()
|
||||
declare -gA _B_CLI_SET=()
|
||||
|
||||
trap _b_on_exit EXIT
|
||||
trap '_b_on_err "$LINENO" "$BASH_COMMAND"' ERR
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
# 1. exportierte Steuer-Variablen aus der Umgebung merken (gewinnen gegen .env)
|
||||
for v in $(compgen -e); do
|
||||
[[ "$v" =~ ^(B_[A-Z0-9_]+|OPTION_[A-Z0-9_]+)$ ]] && env_keep[$v]="${!v}"
|
||||
done
|
||||
|
||||
# 2. .env laden (Bash-Syntax; relativ zum Script, nicht zum $PWD)
|
||||
env_file="$(b_abspath "${B_ENV_FILE:-.env}")"
|
||||
if [[ -f "$env_file" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$env_file" || bdie "Fehler beim Laden von $env_file"
|
||||
fi
|
||||
for v in "${!env_keep[@]}"; do printf -v "$v" '%s' "${env_keep[$v]}"; done
|
||||
|
||||
# 3. Defaults für alles, was weder .env noch Umgebung gesetzt hat
|
||||
: "${B_LOG_LEVEL:=info}" "${B_LOG_CONSOLE:=1}" "${B_LOG_FILE:=}" \
|
||||
"${B_LOG_FILE_LEVEL:=}" "${B_LOG_COLOR:=auto}"
|
||||
[[ -n "${_B_LEVELS[$B_LOG_LEVEL]:-}" ]] || { bwarn "B_LOG_LEVEL=$B_LOG_LEVEL ungültig → info"; B_LOG_LEVEL=info; }
|
||||
[[ -n "$B_LOG_FILE" ]] && B_LOG_FILE="$(b_abspath "$B_LOG_FILE" "$SCRIPT_DIR")"
|
||||
|
||||
_b_opts_build || exit 2
|
||||
for long in "${_B_OPT_ORDER[@]}"; do
|
||||
var="$(_b_optvar "$long")"
|
||||
[[ -v "$var" ]] || printf -v "$var" '%s' "${_B_OPT_DEFAULT[$long]}"
|
||||
done
|
||||
|
||||
# 4. Kommandozeile
|
||||
_b_opts_parse "$@" || bdie "Aufruffehler" 2
|
||||
|
||||
# 5. Standard-Optionen anwenden
|
||||
(( OPTION_DEBUG )) && B_LOG_LEVEL=debug
|
||||
(( OPTION_QUIET )) && B_LOG_CONSOLE=0
|
||||
if [[ -n "${_B_CLI_SET[logfile]:-}" ]]; then
|
||||
B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$B_CALL_DIR")" # CLI: relativ zum $PWD
|
||||
elif [[ -n "$OPTION_LOGFILE" ]]; then
|
||||
B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$SCRIPT_DIR")"
|
||||
fi
|
||||
if [[ -n "$B_LOG_FILE" ]]; then
|
||||
mkdir -p -- "${B_LOG_FILE%/*}" 2>/dev/null && touch -- "$B_LOG_FILE" 2>/dev/null \
|
||||
|| { B_LOG_FILE=""; bwarn "Logdatei nicht beschreibbar – nur Konsole"; }
|
||||
fi
|
||||
|
||||
(( OPTION_HELP )) && { b_usage; exit 0; }
|
||||
|
||||
bdebug "Start ${SCRIPT_NAME} (_functions ${B_FUNCTIONS_VERSION}) in ${SCRIPT_DIR}, Aufruf aus ${B_CALL_DIR}"
|
||||
bdebug ".env: $( [[ -f "$env_file" ]] && echo "$env_file" || echo "keine" ) · Log: ${B_LOG_LEVEL}/${B_LOG_FILE:-ohne Datei}"
|
||||
for long in "${_B_OPT_ORDER[@]}"; do
|
||||
var="$(_b_optvar "$long")"; bdebug " ${var}=${!var}"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- projektspezifische Erweiterungen -----------------------------------------
|
||||
if [[ -f "${B_LIB_DIR}/_functions.local.sh" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "${B_LIB_DIR}/_functions.local.sh"
|
||||
fi
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
#-------------------------------------------------------------------------------
|
||||
## @file script_template.sh
|
||||
## @brief <Was das Script tut – ein Satz>
|
||||
## @author <AUTOR – aus ai/context/responsible.md>
|
||||
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
|
||||
## @assist <optional: KI-unterstützt (Modell), geprüft von AUTOR>
|
||||
## @version 0.1.0 (TT.MM.JJJJ)
|
||||
##
|
||||
## @usage script_template.sh [Optionen] <ziel>
|
||||
## @options
|
||||
## @OPTIONS@
|
||||
## @env .env neben dem Script (Vorlage: .env.example)
|
||||
## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler
|
||||
#-------------------------------------------------------------------------------
|
||||
set -Eeuo pipefail
|
||||
|
||||
# --- Bootstrap (unverändert lassen) -------------------------------------------
|
||||
SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")"
|
||||
SCRIPT_DIR="${SCRIPT_PATH%/*}"
|
||||
SCRIPT_NAME="${SCRIPT_PATH##*/}"
|
||||
# shellcheck source=_functions.sh
|
||||
source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; }
|
||||
|
||||
# Script-eigene Optionen; -h -d -q -n -l kommen automatisch aus _functions.sh
|
||||
B_OPTS+=(
|
||||
"f|force|flag|0|vorhandene Ziele überschreiben"
|
||||
"o|outdir|value:DIR|out|Zielverzeichnis (relativ = zum Script)"
|
||||
)
|
||||
b_init "$@"
|
||||
set -- "${ARGS[@]}"
|
||||
|
||||
#===============================================================================
|
||||
# Funktionen
|
||||
#===============================================================================
|
||||
|
||||
## @brief Aufräumen beim Beenden (vom EXIT-Trap aufgerufen, auch bei Fehlern)
|
||||
## @param $1 Exit-Code des Scripts
|
||||
## @return 0
|
||||
cleanup() {
|
||||
return 0
|
||||
}
|
||||
|
||||
## @brief Hauptablauf
|
||||
## @param $1 Ziel
|
||||
## @return 0 ok, 1 Fehler
|
||||
main() {
|
||||
(( $# == 1 )) || bdie "Genau ein <ziel> erwartet" 2
|
||||
local target="$1" outdir
|
||||
outdir="$(b_abspath "$OPTION_OUTDIR")"
|
||||
|
||||
becho "Ziel: ${target} → ${outdir}"
|
||||
[[ -e "$outdir" && "$OPTION_FORCE" != 1 ]] && { berror "$outdir existiert (--force zum Überschreiben)"; return 1; }
|
||||
brun mkdir -p -- "$outdir"
|
||||
return 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,23 @@
|
||||
# --- Stack ---
|
||||
COMPOSE_PROJECT_NAME=<stack> # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag)
|
||||
APP_TAG=<version>
|
||||
|
||||
# --- Logging (Graylog) ---
|
||||
GELF_HOST=<GELF_HOST>
|
||||
GELF_PORT=12201
|
||||
|
||||
# --- Traefik (Defaults: traefik-net / le – nur bei Abweichung setzen) ---
|
||||
TRAEFIK_ENABLE=false # erst auf true, wenn Stack sauber läuft (LE-Limits!)
|
||||
APP_FQDN=<app.domain.tld>
|
||||
APP_INTERNAL_PORT=<port>
|
||||
#TRAEFIK_NETWORK=traefik-net
|
||||
#TRAEFIK_CERTRESOLVER=le
|
||||
|
||||
# --- Hosts ---
|
||||
EXTRA_HOST_1=<fqdn>:<ip>
|
||||
|
||||
# --- Pfade (Bind-Mounts, relativ zum Stack) ---
|
||||
DIR_DATA=./data
|
||||
DIR_CONFIG=./config
|
||||
|
||||
# --- Image-spezifische Variablen (laut Orig-Doku, s. README) ---
|
||||
@@ -0,0 +1,29 @@
|
||||
# <stack>
|
||||
|
||||
> @author <AUTOR> · @owner <VERANTWORTLICH> · Stand TT.MM.JJJJ
|
||||
|
||||
## 1. Zweck und Zugriff
|
||||
<Was der Stack tut> · FQDN: `<app.domain.tld>` · Port: <…>
|
||||
|
||||
## 2. Original-Doku
|
||||
| Quelle | URL | Abgerufen |
|
||||
|---|---|---|
|
||||
| Image | <Docker Hub / GHCR> | TT.MM.JJJJ |
|
||||
| Hersteller | <Doku-URL> | TT.MM.JJJJ |
|
||||
|
||||
## 3. ENV-Variablen laut Original-Doku
|
||||
| Variable | Bedeutung | Default | gesetzt in .env? |
|
||||
|---|---|---|---|
|
||||
| | | | |
|
||||
|
||||
## 4. Volumes/Pfade
|
||||
| Container-Pfad | Zweck | Bind-Mount (`.env`) |
|
||||
|---|---|---|
|
||||
| | | `DIR_DATA` |
|
||||
|
||||
## 5. Ports und Healthcheck
|
||||
- intern: <…>, extern: <… oder nur Traefik>
|
||||
- Healthcheck: `<endpunkt>` – Begründung: <…>
|
||||
|
||||
## 6. Besonderheiten
|
||||
- <Capabilities, Init-/Unseal-Schritte, Update-Hinweise, Abweichungen vom Standard>
|
||||
@@ -0,0 +1,48 @@
|
||||
#-------------------------------------------------------------------------------
|
||||
## @file docker-compose.yml
|
||||
## @brief <Stack-Zweck in einem Satz>
|
||||
## @author <AUTOR – aus ai/context/responsible.md>
|
||||
## @owner <VERANTWORTLICH – aus ai/context/responsible.md>
|
||||
## @version 0.1.0 (TT.MM.JJJJ)
|
||||
## @usage docker compose up -d (im Stack-Verzeichnis, .env aus .env.example)
|
||||
#-------------------------------------------------------------------------------
|
||||
x-logging: &default-logging
|
||||
driver: gelf
|
||||
options:
|
||||
gelf-address: "udp://${GELF_HOST}:${GELF_PORT}"
|
||||
tag: "${COMPOSE_PROJECT_NAME}"
|
||||
labels: "com.docker.compose.project,com.docker.compose.service"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: <hersteller>/<image>:${APP_TAG}
|
||||
container_name: ${COMPOSE_PROJECT_NAME}
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
# ports: nur wenn Zugriff ohne Traefik nötig ist
|
||||
# - "${APP_PORT}:<intern>"
|
||||
extra_hosts: # nur wenn benötigt; ein Eintrag je Variable
|
||||
- "${EXTRA_HOST_1}"
|
||||
volumes:
|
||||
- ${DIR_DATA}:/<pfad-im-container>
|
||||
- ${DIR_CONFIG}:/<config-pfad>:ro
|
||||
networks:
|
||||
- proxy
|
||||
labels: # Traefik bleibt aus, bis TRAEFIK_ENABLE=true
|
||||
- "traefik.enable=${TRAEFIK_ENABLE:-false}"
|
||||
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik-net}"
|
||||
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${APP_FQDN:-${COMPOSE_PROJECT_NAME}.invalid}`)"
|
||||
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
|
||||
- "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=${APP_INTERNAL_PORT:-80}"
|
||||
logging: *default-logging
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:<intern>/<pfad>"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
name: ${TRAEFIK_NETWORK:-traefik-net}
|
||||
external: true
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
id: adr_<NNNN>_<slug>
|
||||
title: <Entscheidung als Aussage, z. B. „HashiCorp Vault als Secret-Store“>
|
||||
type: decision
|
||||
status: draft # draft|active|superseded
|
||||
updated: <JJJJ-MM-TT>
|
||||
owner: <Kürzel aus responsible.md>
|
||||
tags: []
|
||||
related: []
|
||||
supersedes:
|
||||
classification: intern
|
||||
---
|
||||
# ADR <NNNN>: <Titel>
|
||||
|
||||
## Kontext
|
||||
<Welches Problem, welche Randbedingungen, welche Kräfte wirken?>
|
||||
|
||||
## Entscheidung
|
||||
<Was wir tun – ein klarer Satz, dann Details.>
|
||||
|
||||
## Alternativen
|
||||
| Option | Pro | Contra | Warum nicht |
|
||||
|---|---|---|---|
|
||||
| | | | |
|
||||
|
||||
## Konsequenzen
|
||||
- positiv: <…>
|
||||
- negativ/Aufwand: <…>
|
||||
- Folgearbeiten: <…>
|
||||
|
||||
## Quellen
|
||||
- <URL> (abgerufen TT.MM.JJJJ)
|
||||
|
||||
## Änderungen
|
||||
- TT.MM.JJJJ – vorgeschlagen (<Kürzel>)
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: <id> # = Dateiname ohne .md
|
||||
title: <Titel>
|
||||
type: <system|network|service|decision|howto|research>
|
||||
status: draft # draft|active|deprecated|superseded
|
||||
updated: <JJJJ-MM-TT>
|
||||
owner: <Kürzel aus responsible.md>
|
||||
tags: []
|
||||
sources: []
|
||||
related: []
|
||||
classification: intern
|
||||
---
|
||||
# <Titel>
|
||||
|
||||
> Kurzfassung in 1–2 Sätzen: Was ist das, wozu dient es?
|
||||
|
||||
## Fakten
|
||||
| Merkmal | Wert |
|
||||
|---|---|
|
||||
| | |
|
||||
|
||||
## Details
|
||||
<Aufbau, Konfiguration, Abhängigkeiten – sachlich, mit Tabellen und Code-Blöcken>
|
||||
|
||||
## Betrieb
|
||||
- Bereitstellung: <Ansible-Rolle / Docker-Stack / manuell (→ howto)>
|
||||
- Prüfung/Healthcheck: <…>
|
||||
- Backup/Restore: <…>
|
||||
|
||||
## Quellen
|
||||
- <URL> (abgerufen TT.MM.JJJJ)
|
||||
|
||||
## Änderungen
|
||||
- TT.MM.JJJJ – angelegt (<Kürzel>)
|
||||
Reference in new issue
Block a user