Compare commits
401
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c2cba3ac1 | ||
|
|
7f0bd5f609 | ||
|
|
043923fc64 | ||
|
|
f0f8a35b0f | ||
|
|
f5f389b440 | ||
|
|
7cca330438 | ||
|
|
5936de3e56 | ||
|
|
c98ea8b957 | ||
|
|
34c33697bb | ||
|
|
92bd920113 | ||
|
|
8007f82d52 | ||
|
|
60b9766449 | ||
|
|
0ebb7264bc | ||
|
|
ce21c60eba | ||
|
|
53638f831d | ||
|
|
4953da2dd7 | ||
|
|
4ac65f743c | ||
|
|
8f2e9d66c8 | ||
|
|
c7d42fb90a | ||
|
|
003b1e5dca | ||
|
|
b3463705c3 | ||
|
|
52e1f50a80 | ||
|
|
cdc2f10fe8 | ||
|
|
89fbdef10e | ||
|
|
ac795feeed | ||
|
|
3af5ecd07f | ||
|
|
82949613db | ||
|
|
47d788ca13 | ||
|
|
77be606912 | ||
|
|
4eab6a43c8 | ||
|
|
6c24d4fb17 | ||
|
|
e36595a045 | ||
|
|
e07537ff8b | ||
|
|
303eaaa71b | ||
|
|
1e66b5f344 | ||
|
|
d638a2c1f9 | ||
|
|
b8512c6033 | ||
|
|
3e057ea18d | ||
|
|
77113fb629 | ||
|
|
a3a0ab92b7 | ||
|
|
68fb5eb45e | ||
|
|
1c58c78892 | ||
|
|
82124017c0 | ||
|
|
e502f46f43 | ||
|
|
a4f8218b5e | ||
|
|
d162a50bba | ||
|
|
9ca8514a0a | ||
|
|
6fa809a8d2 | ||
|
|
c6d8df2317 | ||
|
|
c28d7323b3 | ||
|
|
25f547ff78 | ||
|
|
50911b4ec1 | ||
|
|
663675f9a0 | ||
|
|
8b28bd24ff | ||
|
|
b5d6f75330 | ||
|
|
f5b5ecaafa | ||
|
|
7799b676ca | ||
|
|
24d3686f60 | ||
|
|
b8b3bba264 | ||
|
|
abfbc04067 | ||
|
|
23ed72826a | ||
|
|
7288058df6 | ||
|
|
6715f9e9af | ||
|
|
ccec1102ef | ||
|
|
360a6fc5dc | ||
|
|
9a806724af | ||
|
|
ed1ddaad5d | ||
|
|
726b3ee544 | ||
|
|
13309b26e0 | ||
|
|
eddc256bed | ||
|
|
52f821cbba | ||
|
|
0dbc2fff13 | ||
|
|
91ec83bc1c | ||
|
|
9fec1dae39 | ||
|
|
e5626b7b2d | ||
|
|
8fb12a2176 | ||
|
|
fe0c7067b6 | ||
|
|
d0f0843774 | ||
|
|
81a5b207ac | ||
|
|
e3d5970ae3 | ||
|
|
85f05c26cb | ||
|
|
68630eb773 | ||
|
|
dad9cf2104 | ||
|
|
60886e8be2 | ||
|
|
4528321225 | ||
|
|
b246a3dea1 | ||
|
|
4c59a2d2fe | ||
|
|
fcc7b0d611 | ||
|
|
9633fe3a10 | ||
|
|
d9f1c8325a | ||
|
|
861d89d36a | ||
|
|
71cddd6a91 | ||
|
|
30e6f05584 | ||
|
|
bc8d74fd28 | ||
|
|
d2d4efb0d7 | ||
|
|
b752bf88bd | ||
|
|
587611ca88 | ||
|
|
ace23ad1f9 | ||
|
|
92aa731e44 | ||
|
|
87ca3fd40c
|
||
|
|
82bcd30ed8
|
||
|
|
620262d98c | ||
|
|
831f3a46b0 | ||
|
|
f7902e74e8
|
||
|
|
eb8d1b361e
|
||
|
|
6e2cafb206
|
||
|
|
67b0c0824f
|
||
|
|
8e72e0a920
|
||
|
|
73a1132beb | ||
|
|
a128523c24 | ||
|
|
ee881acd0e | ||
|
|
bacb2f4b9f | ||
|
|
91211e7b78 | ||
|
|
9b3a7aadb4
|
||
|
|
b123621ead
|
||
|
|
a2df8504f5
|
||
|
|
fb43306571
|
||
|
|
f424d91405
|
||
|
|
88a8f2987f
|
||
|
|
17027b232b
|
||
|
|
6ecbbb39b4
|
||
|
|
175cbc8860
|
||
|
|
6363d050b0
|
||
|
|
c855764bc6
|
||
|
|
7d92b85e21
|
||
|
|
9364392bc0 | ||
|
|
4355f451d4 | ||
|
|
3eaef5dc90 | ||
|
|
69ffd3682e | ||
|
|
1930600c40 | ||
|
|
d74a705d53
|
||
|
|
3c383db9a7
|
||
|
|
7fb0a0e179 | ||
|
|
227e5fda27
|
||
|
|
20bce5b31c | ||
|
|
70d7855f06
|
||
|
|
c905bbd039 | ||
|
|
fc83176522
|
||
|
|
7ba6bc44f2 | ||
|
|
0506aaaac8
|
||
|
|
bd9724da69
|
||
|
|
3bad433f1a
|
||
|
|
2bd7a5176f
|
||
|
|
a9ff01261b
|
||
|
|
95cec59263 | ||
|
|
1362ebc3c2
|
||
|
|
2de6131ba7
|
||
|
|
1762962f32
|
||
|
|
7fb9e46c05
|
||
|
|
b33488342a
|
||
|
|
d53b14b1de
|
||
|
|
a6a6d933da
|
||
|
|
0803f3efff | ||
|
|
ec0420962b | ||
|
|
b407202e53 | ||
|
|
b9b8474455 | ||
|
|
76853637bc | ||
|
|
dac3777fc4 | ||
|
|
bb5a3697f2 | ||
|
|
e73aacb900 | ||
|
|
ea483da645 | ||
|
|
85d35f86a7 | ||
|
|
3d03ab1ea4 | ||
|
|
4ca3ccdad3 | ||
|
|
10e26cda72 | ||
|
|
c648dfd147 | ||
|
|
2f97821dc6 | ||
|
|
3d78b90f3a | ||
|
|
3dc8228e22 | ||
|
|
93171ad8e6 | ||
|
|
dca7ad0902 | ||
|
|
26d10f4e71 | ||
|
|
68c5eac164 | ||
|
|
30f0f05150 | ||
|
|
a97560eaf3 | ||
|
|
2dce972be2 | ||
|
|
c2ad1122e5 | ||
|
|
4c356924e7 | ||
|
|
51777f904f | ||
|
|
37550da086 | ||
|
|
12d59cb6f9 | ||
|
|
714d4896c6 | ||
|
|
e369875117 | ||
|
|
31e61a9513 | ||
|
|
9b21f8056c | ||
|
|
4623fbd8bd | ||
|
|
18d1e21690 | ||
|
|
20b8c93275 | ||
|
|
5f88ecf02b | ||
|
|
0093e5ac52 | ||
|
|
bb821e138a | ||
|
|
1ea669220b | ||
|
|
f068a3e6a0 | ||
|
|
b7854447af | ||
|
|
7a3708f70c | ||
|
|
01ae2dd5cf | ||
|
|
82595804bf | ||
|
|
31b3c5bc31 | ||
|
|
1cdbfb2d7b | ||
|
|
6232b77026 | ||
|
|
22ffd779cc | ||
|
|
d85b3f02f5 | ||
|
|
17b2dfdc2e | ||
|
|
b641e3bd94 | ||
|
|
e19660fdf4 | ||
|
|
36922a177c
|
||
|
|
f3d04e935c
|
||
|
|
e5797e60b7 | ||
|
|
444bb97f8e | ||
|
|
0c5cf29f83
|
||
|
|
4f01cdac31 | ||
|
|
43c38767a1
|
||
|
|
cb40b10ecf | ||
|
|
a68c01a68f | ||
|
|
bdcdb1cb3d | ||
|
|
fe2b80b51f | ||
|
|
b8d5bc747d | ||
|
|
6f77b7d845
|
||
|
|
ea286e117d | ||
|
|
6a050669e8 | ||
|
|
b9c11b8eab | ||
|
|
6dac841b2c | ||
|
|
bed58c84ef | ||
|
|
526a2b617a | ||
|
|
56e5d79e3e | ||
|
|
1e08391e0e | ||
|
|
cd0ce06246 | ||
|
|
0a31601b77 | ||
|
|
e9a9271d2f | ||
|
|
25eb89c902 | ||
|
|
d988b0f7db | ||
|
|
e873f37159 | ||
|
|
8362f45bdc | ||
|
|
fd5ea6cadd | ||
|
|
aa3598ee3d | ||
|
|
c0205fa49b
|
||
|
|
a22707770f
|
||
|
|
646f988a86
|
||
|
|
414d17d839
|
||
|
|
812e4eb87a
|
||
|
|
70b3b203fb | ||
|
|
d857f3838d | ||
|
|
f8620349a9 | ||
|
|
b1acff5c85 | ||
|
|
beb6dca6a2 | ||
|
|
aed6ff31f7 | ||
|
|
73684af21b | ||
|
|
cd5c90adcc | ||
|
|
578a1ca544 | ||
|
|
12ed20a306 | ||
|
|
400e7b6595
|
||
|
|
f58e96a25d | ||
|
|
a3e5f5a78b | ||
|
|
1a09a62a4c
|
||
|
|
2593b54402
|
||
|
|
42826a037c
|
||
|
|
d7a68237e5
|
||
|
|
bbb8bdf0a7 | ||
|
|
f4d3bd9c6d | ||
|
|
6b919df7c6
|
||
|
|
3ee0b96ed5 | ||
|
|
d25d213d9b | ||
|
|
a3b7c4c889
|
||
|
|
3dbb50c924 | ||
|
|
54da82432b | ||
|
|
e5eb234c41 | ||
|
|
721348e67f | ||
|
|
d58ae2a5e7
|
||
|
|
aa516c3445 | ||
|
|
b5cc7d9a0d
|
||
|
|
5dfa9c879b
|
||
|
|
3c5702a0fb | ||
|
|
dd0ca27f4f | ||
|
|
7f7d0de090 | ||
|
|
bee3f16cb2 | ||
|
|
303d23968d
|
||
|
|
b7ecf88052
|
||
|
|
5068591b8b
|
||
|
|
8e57f21e44
|
||
|
|
073d9ff569
|
||
|
|
c6d00e525b | ||
|
|
539994a145
|
||
|
|
95f0afbbee
|
||
|
|
9f86bd1142
|
||
|
|
af9668e8e6 | ||
|
|
53b71a33ad
|
||
|
|
bf72007b14
|
||
|
|
0bad0a817e
|
||
|
|
525fed3b92 | ||
|
|
fe5e5c5e35
|
||
|
|
72aa022048
|
||
|
|
f18d4d9be4
|
||
|
|
45ce789f58
|
||
|
|
d7c05fd058
|
||
|
|
c13056fba1 | ||
|
|
5fe8af82d5
|
||
|
|
6d97246997 | ||
|
|
6970279311 | ||
|
|
02f4e0ab42
|
||
|
|
4a25622552
|
||
|
|
0138fbd276
|
||
|
|
94b5f39207
|
||
|
|
403e88d548 | ||
|
|
d03a4844fb | ||
|
|
48ff08529e | ||
|
|
81592b6142
|
||
|
|
9480576966 | ||
|
|
9b43a9bef4 | ||
|
|
2b03335af5 | ||
|
|
ea738ec14c | ||
|
|
e4e9d96a0b
|
||
|
|
89576032b9 | ||
|
|
a9edfc0a25 | ||
|
|
acb8009307 | ||
|
|
21f4e46028 | ||
|
|
0234524635 | ||
|
|
26f5fb2fb9 | ||
|
|
122e6f6986 | ||
|
|
ce0bc4613a | ||
|
|
8f4f460ff3 | ||
|
|
c048efd569 | ||
|
|
1f1e13ff39 | ||
|
|
c80a6c5351 | ||
|
|
4fe3d660f1 | ||
|
|
9675eac2bf | ||
|
|
dc7fe64fbc | ||
|
|
502810a12e | ||
|
|
c047cc291d | ||
|
|
f2b951673c | ||
|
|
6b7c0df586 | ||
|
|
fb2f420520 | ||
|
|
60c7d4ff17 | ||
|
|
d20ec696a3 | ||
|
|
c030b4cffa | ||
|
|
e0f7ab6561 | ||
|
|
f0682319a7 | ||
|
|
dbd3f36f76 | ||
|
|
4471da827c | ||
|
|
163ea867ce | ||
|
|
dc75dbaf7c | ||
|
|
db0f0f7bb6 | ||
|
|
5e4c60bb30 | ||
|
|
a699ceb935 | ||
|
|
17cae71952 | ||
|
|
f4c695f95e | ||
|
|
6bdb16ad21 | ||
|
|
6eb62f41cc | ||
|
|
586f0e3f7d | ||
|
|
0e46193f53 | ||
|
|
aed28ed15c | ||
|
|
f3b73bae11 | ||
|
|
be049cfa0d | ||
|
|
bfb21adff7 | ||
|
|
1c75382a6e | ||
|
|
9c5e037567 | ||
|
|
4f9e7ea990 | ||
|
|
9f784d2c31 | ||
|
|
a07e27bff6 | ||
|
|
c31369f2b7 | ||
|
|
3bfcd6edf4 | ||
|
|
504cbc81a0 | ||
|
|
aa7239ee83 | ||
|
|
6d9427cf2a | ||
|
|
70d60ed40a | ||
|
|
aca6824309 | ||
|
|
d48a01775d | ||
|
|
99d50b43fe | ||
|
|
23e955bde7 | ||
|
|
7d7a0e5c8a | ||
|
|
a767107277 | ||
|
|
e68e37c285 | ||
|
|
fc6a11397b | ||
|
|
d776124f26 | ||
|
|
8a0ed85e7c | ||
|
|
15f0f35ce4 | ||
|
|
1ec145d4cf | ||
|
|
8b6815f314 | ||
|
|
2d05a1911c | ||
|
|
6f2f70ad09 | ||
|
|
ce66a546f1 | ||
|
|
6cb49be951 | ||
|
|
3bcabcce4b | ||
|
|
a54b7b000f | ||
|
|
5b1fa11b5e | ||
|
|
380288d103 | ||
|
|
8fae8bf75d | ||
|
|
d4e0e7f37a | ||
|
|
cb92bff0c5 | ||
|
|
bfbe841154 | ||
|
|
7cbc5bf4f3 | ||
|
|
6843befac3 | ||
|
|
0dfb09590d | ||
|
|
625eb9561b | ||
|
|
b367b64879 | ||
|
|
a30bda4940 | ||
|
|
b722467991 | ||
|
|
5f8fd05266 | ||
|
|
1c7afe5bb3 | ||
|
|
4ff80c07b0 | ||
|
|
3a5652daa7 | ||
|
|
4e18cd0eb3 |
No files matched your search
@@ -0,0 +1,191 @@
|
|||||||
|
# Инструкция: Анализ хранилища Kubernetes и настройка NFS
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
Проанализировать текущую конфигурацию хранилища Kubernetes и подготовить план внедрения NFS StorageClass для сохранения данных при удалении namespace.
|
||||||
|
|
||||||
|
## 1. Собрать информацию о кластере
|
||||||
|
|
||||||
|
### 1.1. Версия Kubernetes и тип дистрибутива
|
||||||
|
```bash
|
||||||
|
kubectl version --short
|
||||||
|
# или
|
||||||
|
kubectl version
|
||||||
|
```
|
||||||
|
|
||||||
|
Определить, используется ли k3s, k8s, microk8s и т.д.:
|
||||||
|
```bash
|
||||||
|
# Проверить наличие k3s
|
||||||
|
which k3s
|
||||||
|
# Проверить процесс
|
||||||
|
ps aux | grep -E 'kube|k3s'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2. StorageClass
|
||||||
|
```bash
|
||||||
|
kubectl get storageclass -o wide
|
||||||
|
```
|
||||||
|
|
||||||
|
Запомнить:
|
||||||
|
- `PROVISIONER` — какой драйвер используется
|
||||||
|
- `RECLAIMPOLICY` — Delete или Retain
|
||||||
|
- Какой StorageClass помечен как `(default)`
|
||||||
|
|
||||||
|
### 1.3. Существующие PV и PVC
|
||||||
|
```bash
|
||||||
|
kubectl get pv -o wide
|
||||||
|
kubectl get pvc --all-namespaces
|
||||||
|
```
|
||||||
|
|
||||||
|
Посмотреть, какие PVC привязаны к каким PV, и какой reclaimPolicy у PV.
|
||||||
|
|
||||||
|
### 1.4. Нода и диски
|
||||||
|
```bash
|
||||||
|
# Список нод
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
|
||||||
|
# На каждой ноде (через ssh или локально):
|
||||||
|
lsblk
|
||||||
|
df -h
|
||||||
|
cat /etc/fstab
|
||||||
|
```
|
||||||
|
|
||||||
|
Определить:
|
||||||
|
- Есть ли отдельный раздел/диск для данных
|
||||||
|
- Куда смонтированы разделы
|
||||||
|
- Сколько свободного места
|
||||||
|
- Есть ли монтирование NTFS-разделов (как `/media/forust/Programs`)
|
||||||
|
|
||||||
|
### 1.5. Где local-path хранит данные (для k3s)
|
||||||
|
```bash
|
||||||
|
ls -la /var/lib/rancher/k3s/storage/ 2>/dev/null
|
||||||
|
# или для microk8s
|
||||||
|
ls -la /var/snap/microk8s/common/ 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Анализ: сохраняются ли данные при удалении namespace?
|
||||||
|
|
||||||
|
| Сценарий | Результат |
|
||||||
|
|---|---|
|
||||||
|
| `kubectl delete ns <ns>` | Все PVC в namespace удаляются |
|
||||||
|
| PVC → PV c `reclaimPolicy: Delete` | PV и данные удалены |
|
||||||
|
| PVC → PV c `reclaimPolicy: Retain` | PV остаётся (статус Released), данные целы |
|
||||||
|
|
||||||
|
**Вывод:** Если reclaimPolicy в StorageClass = `Delete`, то данные **пропадут**. Если `Retain` — сохранятся.
|
||||||
|
|
||||||
|
## 3. План внедрения NFS
|
||||||
|
|
||||||
|
### 3.1. Проверить, установлен ли NFS
|
||||||
|
```bash
|
||||||
|
which nfsstat exportfs mount.nfs
|
||||||
|
systemctl status nfs-server 2>/dev/null || systemctl status nfs-kernel-server 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2. Выбрать директорию для NFS-экспорта
|
||||||
|
|
||||||
|
Варианты (выбрать подходящий):
|
||||||
|
- `/var/lib/k8s-nfs/` — на корневом разделе
|
||||||
|
- `<путь к отдельному разделу>/k8s-nfs/` — если есть отдельный диск/раздел
|
||||||
|
- Не рекомендуется использовать NTFS-раздел (проблемы с правами и производительностью)
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
- Файловая система: ext4 или xfs (не ntfs!)
|
||||||
|
- Достаточно свободного места
|
||||||
|
- Права: `755`, владелец root
|
||||||
|
|
||||||
|
### 3.3. Установить NFS-сервер
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
apt update && apt install -y nfs-kernel-server
|
||||||
|
|
||||||
|
# RHEL/Fedora
|
||||||
|
dnf install -y nfs-utils
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4. Настроить экспорт
|
||||||
|
|
||||||
|
Создать директорию:
|
||||||
|
```bash
|
||||||
|
mkdir -p /var/lib/k8s-nfs
|
||||||
|
chmod 755 /var/lib/k8s-nfs
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавить в `/etc/exports`:
|
||||||
|
```
|
||||||
|
/var/lib/k8s-nfs *(rw,sync,no_subtree_check,no_root_squash)
|
||||||
|
```
|
||||||
|
|
||||||
|
Применить:
|
||||||
|
```bash
|
||||||
|
exportfs -rav
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить:
|
||||||
|
```bash
|
||||||
|
showmount -e localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5. Выбрать способ интеграции с Kubernetes
|
||||||
|
|
||||||
|
#### Вариант A: nfs-subdir-external-provisioner (проще)
|
||||||
|
```bash
|
||||||
|
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
|
||||||
|
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
|
||||||
|
--namespace kube-system \
|
||||||
|
--set nfs.server=127.0.0.1 \
|
||||||
|
--set nfs.path=/var/lib/k8s-nfs \
|
||||||
|
--set storageClass.name=nfs \
|
||||||
|
--set storageClass.defaultClass=false \
|
||||||
|
--set storageClass.reclaimPolicy=Retain
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Вариант B: NFS CSI Driver
|
||||||
|
```bash
|
||||||
|
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
|
||||||
|
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs --namespace kube-system
|
||||||
|
```
|
||||||
|
|
||||||
|
После установки CSI драйвера создать StorageClass:
|
||||||
|
```yaml
|
||||||
|
apiVersion: storage.k8s.io/v1
|
||||||
|
kind: StorageClass
|
||||||
|
metadata:
|
||||||
|
name: nfs
|
||||||
|
provisioner: nfs.csi.k8s.io
|
||||||
|
parameters:
|
||||||
|
server: 127.0.0.1
|
||||||
|
share: /var/lib/k8s-nfs
|
||||||
|
reclaimPolicy: Retain
|
||||||
|
volumeBindingMode: Immediate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6. Проверить результат
|
||||||
|
```bash
|
||||||
|
kubectl get storageclass
|
||||||
|
kubectl get pods -n kube-system | grep -E 'nfs|provisioner'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Итоговая конфигурация
|
||||||
|
|
||||||
|
После внедрения в кластере будет два StorageClass:
|
||||||
|
|
||||||
|
| Имя | Provisioner | ReclaimPolicy | Назначение |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `local-path` (default) | rancher.io/local-path | Delete | Временные данные, stateless |
|
||||||
|
| `nfs` | nfs-subdir-external-provisioner или nfs.csi.k8s.io | Retain | Данные, которые нужно сохранять |
|
||||||
|
|
||||||
|
**Главное преимущество:** PVC c `storageClassName: nfs` при удалении namespace сохраняют данные на диске, так как NFS-провизор использует `reclaimPolicy: Retain` или файлы физически остаются в NFS-экспорте.
|
||||||
|
|
||||||
|
## 5. Ответы на частые вопросы
|
||||||
|
|
||||||
|
**В:** Не упадёт ли local-path при установке NFS?
|
||||||
|
**О:** Нет, они независимы. local-path продолжает работать как обычно.
|
||||||
|
|
||||||
|
**В:** Данные NFS и local-path будут на одном диске?
|
||||||
|
**О:** Да, можно настроить оба на одном разделе, в разных каталогах.
|
||||||
|
|
||||||
|
**В:** Что если у меня несколько нод?
|
||||||
|
**О:** NFS сервер нужно поднять на одной ноде, а с других нод должна быть доступна шари. Для multi-node лучше использовать отдельный сервер или distributed storage (Longhorn, Rook/Ceph).
|
||||||
|
|
||||||
|
**В:** Можно ли использовать существующий NTFS-раздел для NFS?
|
||||||
|
**О:** Не рекомендуется — NTFS не поддерживает права Linux (no_root_squash не сработает корректно), возможны проблемы с блокировками и производительностью.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{json,jsonc}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[{Makefile,makefile}]
|
||||||
|
indent_style = tab
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: gcr.forust.xyz
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-prettier:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check formatting with Prettier
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t prettier_files < <(
|
||||||
|
git ls-files \
|
||||||
|
| grep -E '\.(md|json|ya?ml|html|css)$' \
|
||||||
|
| grep -Ev '^(\.docs/|\.zed/|errorpages/html/|homepages/(forust_files|xdfnx_files)/)'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#prettier_files[@]}" -eq 0 ]; then
|
||||||
|
echo "No Prettier-managed files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
node:22-alpine \
|
||||||
|
sh -lc 'npx --yes prettier@3 --check --ignore-unknown "$@"' sh "${prettier_files[@]}"
|
||||||
|
|
||||||
|
lint-ruff:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint Python with Ruff
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
ghcr.io/astral-sh/ruff:latest \
|
||||||
|
check .
|
||||||
|
|
||||||
|
lint-yaml:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint YAML syntax
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t yaml_files < <(
|
||||||
|
git ls-files '*.yaml' '*.yml' \
|
||||||
|
':!node_modules/**' \
|
||||||
|
':!**/.venv/**'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#yaml_files[@]}" -eq 0 ]; then
|
||||||
|
echo "No YAML files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
cytopia/yamllint:latest \
|
||||||
|
-c .yamllint "${yaml_files[@]}"
|
||||||
|
|
||||||
|
lint-dockerfiles:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint Dockerfiles
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t dockerfiles < <(
|
||||||
|
git ls-files ':(glob)**/Dockerfile' ':(glob)**/Dockerfile.*'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#dockerfiles[@]}" -eq 0 ]; then
|
||||||
|
echo "No Dockerfiles found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
--entrypoint hadolint \
|
||||||
|
hadolint/hadolint:latest-debian \
|
||||||
|
-c .hadolint.yaml "${dockerfiles[@]}"
|
||||||
|
|
||||||
|
validate:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate Kubernetes manifests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t manifests < <(
|
||||||
|
git ls-files ':(glob)**/k8s/**/*.yaml' ':(glob)**/k8s/**/*.yml' \
|
||||||
|
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#manifests[@]}" -eq 0 ]; then
|
||||||
|
echo "No Kubernetes manifests found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
ghcr.io/yannh/kubeconform:latest \
|
||||||
|
-strict \
|
||||||
|
-ignore-missing-schemas \
|
||||||
|
-summary \
|
||||||
|
"${manifests[@]}"
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: [lint-prettier, lint-ruff, lint-yaml, lint-dockerfiles, validate]
|
||||||
|
if: github.event_name != 'pull_request' && (github.ref_name == 'main' || github.ref_name == 'dev')
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
outputs:
|
||||||
|
services: ${{ steps.services.outputs.services }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Detect changed docker-built services
|
||||||
|
id: services
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
base="${{ github.event.before }}"
|
||||||
|
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
base="$(git rev-list --max-parents=0 HEAD)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t changed_files < <(git diff --name-only "$base" "${GITHUB_SHA}")
|
||||||
|
|
||||||
|
services=()
|
||||||
|
|
||||||
|
add_service() {
|
||||||
|
local name="$1"
|
||||||
|
local seen=0
|
||||||
|
for existing in "${services[@]}"; do
|
||||||
|
if [ "$existing" = "$name" ]; then
|
||||||
|
seen=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$seen" -eq 0 ]; then
|
||||||
|
services+=("$name")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in "${changed_files[@]}"; do
|
||||||
|
case "$file" in
|
||||||
|
dtek_notif/*)
|
||||||
|
add_service dtek_notif
|
||||||
|
;;
|
||||||
|
errorpages/*)
|
||||||
|
add_service errorpages
|
||||||
|
;;
|
||||||
|
userbot/*)
|
||||||
|
add_service userbot
|
||||||
|
;;
|
||||||
|
homepages/*)
|
||||||
|
add_service homepages
|
||||||
|
;;
|
||||||
|
edu_master/phpsessid-bot/*|edu_master/webinar-checker/*|edu_master/compose.yaml)
|
||||||
|
add_service edu_master
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#services[@]}" -eq 0 ]; then
|
||||||
|
echo "No docker-built services changed."
|
||||||
|
echo "services=" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${services[@]}" | tee /tmp/services.txt
|
||||||
|
echo "services=$(paste -sd, /tmp/services.txt)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
if: steps.services.outputs.services != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" \
|
||||||
|
-u "${{ secrets.REGISTRY_USERNAME }}" \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
- name: Build and push changed images
|
||||||
|
if: steps.services.outputs.services != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
IFS=, read -r -a services <<< "${{ steps.services.outputs.services }}"
|
||||||
|
|
||||||
|
for service in "${services[@]}"; do
|
||||||
|
case "$service" in
|
||||||
|
dtek_notif)
|
||||||
|
image="${REGISTRY}/forust/dtek-notif"
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" dtek_notif
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
errorpages)
|
||||||
|
image="${REGISTRY}/forust/error-pages"
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" errorpages
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
userbot)
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
for target in runtime panel; do
|
||||||
|
case "$target" in
|
||||||
|
runtime)
|
||||||
|
context="userbot"
|
||||||
|
image="${REGISTRY}/forust/userbot"
|
||||||
|
;;
|
||||||
|
panel)
|
||||||
|
context="userbot/panel"
|
||||||
|
image="${REGISTRY}/forust/userbot-panel"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" "$context"
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
homepages)
|
||||||
|
for service in forust xdfnx; do
|
||||||
|
case "$service" in
|
||||||
|
forust)
|
||||||
|
image="${REGISTRY}/forust/forust-homepage"
|
||||||
|
;;
|
||||||
|
xdfnx)
|
||||||
|
image="${REGISTRY}/forust/xdfnx-homepage"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" -f "homepages/Dockerfile.${service}" homepages
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
edu_master)
|
||||||
|
for service in session-keeper webinar-checker; do
|
||||||
|
case "$service" in
|
||||||
|
session-keeper)
|
||||||
|
context="edu_master/phpsessid-bot"
|
||||||
|
image="${REGISTRY}/forust/session-keeper"
|
||||||
|
;;
|
||||||
|
webinar-checker)
|
||||||
|
context="edu_master/webinar-checker"
|
||||||
|
image="${REGISTRY}/forust/webinar-checker"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" "$context"
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
deploy-userbot-panel:
|
||||||
|
needs: build
|
||||||
|
if: github.ref_name == 'main' && contains(needs.build.outputs.services, 'userbot')
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab, prod]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Apply and roll out userbot panel
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
kubectl apply -f userbot/k8s/base/panel.yaml
|
||||||
|
kubectl get secret userbot-common-secrets -n default -o json \
|
||||||
|
| jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
|
||||||
|
| kubectl apply -f -
|
||||||
|
# Keep legacy deployments (forust/anna) in sync with manifests; they have no replicas field, so apply leaves scaling to the user manager only.
|
||||||
|
kubectl apply -f userbot/k8s/base/userbots.yaml
|
||||||
|
kubectl rollout restart deployment/userbot-panel -n userbot
|
||||||
|
kubectl rollout status deployment/userbot-panel -n userbot --timeout=180s
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-main
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
redeploy:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab, prod]
|
||||||
|
steps:
|
||||||
|
- name: Redeploy workstation
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
||||||
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
||||||
|
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
# Set APPLY_PRUNE=true to enable kubectl apply --prune. Requires every
|
||||||
|
# manifest to carry label app.kubernetes.io/managed-by=homelab-deploy,
|
||||||
|
# otherwise previously applied resources get deleted on the next run.
|
||||||
|
APPLY_PRUNE: ${{ vars.APPLY_PRUNE }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${DEPLOY_HOST:?missing DEPLOY_HOST}"
|
||||||
|
: "${DEPLOY_USER:?missing DEPLOY_USER}"
|
||||||
|
: "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}"
|
||||||
|
|
||||||
|
deploy_port="${DEPLOY_PORT:-22}"
|
||||||
|
deploy_path="${DEPLOY_PATH:-/srv/homelab}"
|
||||||
|
|
||||||
|
ssh_key="$RUNNER_TEMP/deploy_key"
|
||||||
|
mkdir -p "$RUNNER_TEMP"
|
||||||
|
printf '%s\n' "$DEPLOY_KEY" > "$ssh_key"
|
||||||
|
chmod 600 "$ssh_key"
|
||||||
|
|
||||||
|
ssh_opts=(
|
||||||
|
-i "$ssh_key"
|
||||||
|
-p "$deploy_port"
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o StrictHostKeyChecking=accept-new
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||||
|
"DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="${DEPLOY_PATH:-/srv/homelab}"
|
||||||
|
|
||||||
|
if [ ! -d "$repo/.git" ]; then
|
||||||
|
echo "Repository not found at $repo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$repo" fetch origin main
|
||||||
|
git -C "$repo" reset --hard origin/main
|
||||||
|
|
||||||
|
# Runtime selection: a service is k8s-managed when $SERVICE/k8s/active
|
||||||
|
# exists. Otherwise it is compose-managed, and only k8s/routing/*
|
||||||
|
# manifests (external Services / EndpointSlices / ServersTransport /
|
||||||
|
# Ingresses that route to docker backends) are applied.
|
||||||
|
# migrate: touch SERVICE/k8s/active (+ move routing files up)
|
||||||
|
# rollback: rm SERVICE/k8s/active
|
||||||
|
collect_k8s() {
|
||||||
|
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
|
||||||
|
! -path '*/routing/*' ! -path '*/overlays/*' \
|
||||||
|
! -name 'kustomization.y*ml' ! -name '*.example.y*ml' \
|
||||||
|
! -name '*values.y*ml' ! -name 'patch-*.y*ml' \
|
||||||
|
| sort
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_k8s_inactive() {
|
||||||
|
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
|
||||||
|
\( -name 'namespace.y*ml' -o -path '*/routing/*' \) \
|
||||||
|
! -path '*/overlays/*' ! -name '*.example.y*ml' \
|
||||||
|
| sort
|
||||||
|
}
|
||||||
|
|
||||||
|
mapfile -t compose_stacks < <(
|
||||||
|
find "$repo" -type f \( -name 'compose.yaml' -o -name 'compose.yml' \) | sort
|
||||||
|
)
|
||||||
|
|
||||||
|
mapfile -t k8s_manifests < <(
|
||||||
|
for kd in $(find "$repo" -type d -name k8s ! -path '*/.git/*' | sort); do
|
||||||
|
if [ -f "$kd/active" ]; then
|
||||||
|
collect_k8s "$kd"
|
||||||
|
else
|
||||||
|
collect_k8s_inactive "$kd"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "== Validate compose stacks =="
|
||||||
|
for cf in "${compose_stacks[@]}"; do
|
||||||
|
dir=$(dirname "$cf")
|
||||||
|
if [ -f "$dir/k8s/active" ]; then
|
||||||
|
echo " skip (k8s-managed): $dir"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo " config: $cf"
|
||||||
|
docker compose -f "$cf" config --quiet
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== Validate k8s manifests (kubectl dry-run) =="
|
||||||
|
for m in "${k8s_manifests[@]}"; do
|
||||||
|
echo " apply --dry-run=client $m"
|
||||||
|
kubectl apply --dry-run=client -f "$m" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== Applying Kubernetes manifests =="
|
||||||
|
ns_files=()
|
||||||
|
other_files=()
|
||||||
|
for m in "${k8s_manifests[@]}"; do
|
||||||
|
case "$m" in
|
||||||
|
*/namespace.y?ml) ns_files+=("$m") ;;
|
||||||
|
*) other_files+=("$m") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
prune_opts=()
|
||||||
|
if [ "${APPLY_PRUNE:-false}" = "true" ]; then
|
||||||
|
prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#ns_files[@]}" -gt 0 ]; then
|
||||||
|
echo " namespaces first: ${ns_files[*]}"
|
||||||
|
kubectl apply -f "${ns_files[@]}"
|
||||||
|
fi
|
||||||
|
if [ "${#other_files[@]}" -gt 0 ]; then
|
||||||
|
echo " resources: ${other_files[*]}"
|
||||||
|
kubectl apply "${prune_opts[@]}" -f "${other_files[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== Redeploying docker compose stacks =="
|
||||||
|
for cf in "${compose_stacks[@]}"; do
|
||||||
|
dir=$(dirname "$cf")
|
||||||
|
if [ -f "$dir/k8s/active" ]; then
|
||||||
|
echo " skip (k8s-managed): $dir"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo " compose: $dir"
|
||||||
|
if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then
|
||||||
|
docker compose -f "$cf" build
|
||||||
|
docker compose -f "$cf" push
|
||||||
|
fi
|
||||||
|
docker compose -f "$cf" up -d --pull always --remove-orphans
|
||||||
|
done
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: renovate-ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-renovate:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate Renovate Compose draft
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
trap 'rm -f renovate/.env' EXIT
|
||||||
|
printf '%s\n' \
|
||||||
|
'RENOVATE_ENDPOINT=https://gitea.example/api/v1' \
|
||||||
|
'RENOVATE_TOKEN=test-token' \
|
||||||
|
'RENOVATE_REPOSITORIES=forust/homelab' \
|
||||||
|
> renovate/.env
|
||||||
|
docker compose -f renovate/renovate-compose.yaml config --quiet
|
||||||
|
|
||||||
|
- name: Validate Kubernetes manifests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
ghcr.io/yannh/kubeconform:latest \
|
||||||
|
-strict \
|
||||||
|
-ignore-missing-schemas \
|
||||||
|
-summary \
|
||||||
|
renovate/k8s/namespace.yaml \
|
||||||
|
renovate/k8s/configmap.yaml \
|
||||||
|
renovate/k8s/cronjob.yaml
|
||||||
|
|
||||||
|
- name: Validate Renovate repository config
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
renovate/renovate:44.97.2 \
|
||||||
|
renovate-config-validator renovate.json
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: renovate-run
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
repositories:
|
||||||
|
description: "Repositories to scan (comma-separated)"
|
||||||
|
required: false
|
||||||
|
default: "forust/homelab"
|
||||||
|
log_level:
|
||||||
|
description: "Renovate log level"
|
||||||
|
required: false
|
||||||
|
default: "info"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- info
|
||||||
|
- debug
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: renovate-run
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-renovate:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Renovate
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
||||||
|
RENOVATE_REPOSITORIES: ${{ inputs.repositories }}
|
||||||
|
LOG_LEVEL: ${{ inputs.log_level }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${RENOVATE_TOKEN:?missing RENOVATE_TOKEN secret — add a renovate-bot PAT in repo/org Actions secrets}"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD/renovate/config.js:/opt/renovate/config.js:ro" \
|
||||||
|
-e RENOVATE_PLATFORM=gitea \
|
||||||
|
-e RENOVATE_ENDPOINT=https://gitea.forust.xyz/api/v1 \
|
||||||
|
-e RENOVATE_TOKEN="$RENOVATE_TOKEN" \
|
||||||
|
-e RENOVATE_GITHUB_COM_TOKEN="${RENOVATE_GITHUB_COM_TOKEN:-}" \
|
||||||
|
-e RENOVATE_REPOSITORIES="${RENOVATE_REPOSITORIES:-forust/homelab}" \
|
||||||
|
-e RENOVATE_CONFIG_FILE=/opt/renovate/config.js \
|
||||||
|
-e RENOVATE_BASE_DIR=/tmp/renovate \
|
||||||
|
-e LOG_LEVEL="${LOG_LEVEL:-info}" \
|
||||||
|
renovate/renovate:44.97.2
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: gcr.forust.xyz
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-prettier:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check formatting with Prettier
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t prettier_files < <(
|
||||||
|
git ls-files \
|
||||||
|
| grep -E '\.(md|json|ya?ml|html|css)$' \
|
||||||
|
| grep -Ev '^(\.docs/|\.zed/|errorpages/html/|homepages/(forust_files|xdfnx_files)/)'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#prettier_files[@]}" -eq 0 ]; then
|
||||||
|
echo "No Prettier-managed files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
node:22-alpine \
|
||||||
|
sh -lc 'npx --yes prettier@3 --check --ignore-unknown "$@"' sh "${prettier_files[@]}"
|
||||||
|
|
||||||
|
lint-ruff:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint Python with Ruff
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
ghcr.io/astral-sh/ruff:latest \
|
||||||
|
check .
|
||||||
|
|
||||||
|
lint-yaml:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint YAML syntax
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t yaml_files < <(
|
||||||
|
git ls-files '*.yaml' '*.yml' \
|
||||||
|
':!node_modules/**' \
|
||||||
|
':!**/.venv/**'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#yaml_files[@]}" -eq 0 ]; then
|
||||||
|
echo "No YAML files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
cytopia/yamllint:latest \
|
||||||
|
-c .yamllint "${yaml_files[@]}"
|
||||||
|
|
||||||
|
lint-dockerfiles:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint Dockerfiles
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t dockerfiles < <(
|
||||||
|
git ls-files ':(glob)**/Dockerfile' ':(glob)**/Dockerfile.*'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#dockerfiles[@]}" -eq 0 ]; then
|
||||||
|
echo "No Dockerfiles found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
--entrypoint hadolint \
|
||||||
|
hadolint/hadolint:latest-debian \
|
||||||
|
-c .hadolint.yaml "${dockerfiles[@]}"
|
||||||
|
|
||||||
|
validate:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate Kubernetes manifests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t manifests < <(
|
||||||
|
git ls-files ':(glob)**/k8s/**/*.yaml' ':(glob)**/k8s/**/*.yml' \
|
||||||
|
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#manifests[@]}" -eq 0 ]; then
|
||||||
|
echo "No Kubernetes manifests found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" \
|
||||||
|
-w /work \
|
||||||
|
ghcr.io/yannh/kubeconform:latest \
|
||||||
|
-strict \
|
||||||
|
-ignore-missing-schemas \
|
||||||
|
-summary \
|
||||||
|
"${manifests[@]}"
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: [lint-prettier, lint-ruff, lint-yaml, lint-dockerfiles, validate]
|
||||||
|
if: github.event_name != 'pull_request' && (github.ref_name == 'main' || github.ref_name == 'dev')
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
outputs:
|
||||||
|
services: ${{ steps.services.outputs.services }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Detect changed docker-built services
|
||||||
|
id: services
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
base="${{ github.event.before }}"
|
||||||
|
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
base="$(git rev-list --max-parents=0 HEAD)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t changed_files < <(git diff --name-only "$base" "${GITHUB_SHA}")
|
||||||
|
|
||||||
|
services=()
|
||||||
|
|
||||||
|
add_service() {
|
||||||
|
local name="$1"
|
||||||
|
local seen=0
|
||||||
|
for existing in "${services[@]}"; do
|
||||||
|
if [ "$existing" = "$name" ]; then
|
||||||
|
seen=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$seen" -eq 0 ]; then
|
||||||
|
services+=("$name")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in "${changed_files[@]}"; do
|
||||||
|
case "$file" in
|
||||||
|
dtek_notif/*)
|
||||||
|
add_service dtek_notif
|
||||||
|
;;
|
||||||
|
errorpages/*)
|
||||||
|
add_service errorpages
|
||||||
|
;;
|
||||||
|
userbot/*)
|
||||||
|
add_service userbot
|
||||||
|
;;
|
||||||
|
homepages/*)
|
||||||
|
add_service homepages
|
||||||
|
;;
|
||||||
|
edu_master/phpsessid-bot/*|edu_master/webinar-checker/*|edu_master/compose.yaml)
|
||||||
|
add_service edu_master
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#services[@]}" -eq 0 ]; then
|
||||||
|
echo "No docker-built services changed."
|
||||||
|
echo "services=" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${services[@]}" | tee /tmp/services.txt
|
||||||
|
echo "services=$(paste -sd, /tmp/services.txt)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
if: steps.services.outputs.services != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" \
|
||||||
|
-u "${{ secrets.REGISTRY_USERNAME }}" \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
- name: Build and push changed images
|
||||||
|
if: steps.services.outputs.services != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
IFS=, read -r -a services <<< "${{ steps.services.outputs.services }}"
|
||||||
|
|
||||||
|
for service in "${services[@]}"; do
|
||||||
|
case "$service" in
|
||||||
|
dtek_notif)
|
||||||
|
image="${REGISTRY}/forust/dtek-notif"
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" dtek_notif
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
errorpages)
|
||||||
|
image="${REGISTRY}/forust/error-pages"
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" errorpages
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
userbot)
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
for target in runtime panel; do
|
||||||
|
case "$target" in
|
||||||
|
runtime)
|
||||||
|
context="userbot"
|
||||||
|
image="${REGISTRY}/forust/userbot"
|
||||||
|
;;
|
||||||
|
panel)
|
||||||
|
context="userbot/panel"
|
||||||
|
image="${REGISTRY}/forust/userbot-panel"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" "$context"
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
homepages)
|
||||||
|
for service in forust xdfnx; do
|
||||||
|
case "$service" in
|
||||||
|
forust)
|
||||||
|
image="${REGISTRY}/forust/forust-homepage"
|
||||||
|
;;
|
||||||
|
xdfnx)
|
||||||
|
image="${REGISTRY}/forust/xdfnx-homepage"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" -f "homepages/Dockerfile.${service}" homepages
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
edu_master)
|
||||||
|
for service in session-keeper webinar-checker; do
|
||||||
|
case "$service" in
|
||||||
|
session-keeper)
|
||||||
|
context="edu_master/phpsessid-bot"
|
||||||
|
image="${REGISTRY}/forust/session-keeper"
|
||||||
|
;;
|
||||||
|
webinar-checker)
|
||||||
|
context="edu_master/webinar-checker"
|
||||||
|
image="${REGISTRY}/forust/webinar-checker"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
tags=("latest")
|
||||||
|
case "${GITHUB_REF_NAME}" in
|
||||||
|
main)
|
||||||
|
tags+=("main" "prod")
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
tags+=("dev")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
build_args=()
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
build_args+=(-t "${image}:${tag}")
|
||||||
|
done
|
||||||
|
docker build "${build_args[@]}" "$context"
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
docker push "${image}:${tag}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
deploy-userbot-panel:
|
||||||
|
needs: build
|
||||||
|
if: github.ref_name == 'main' && contains(needs.build.outputs.services, 'userbot')
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab, prod]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Apply and roll out userbot panel
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
kubectl apply -f userbot/k8s/base/panel.yaml
|
||||||
|
kubectl get secret userbot-common-secrets -n default -o json \
|
||||||
|
| jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
|
||||||
|
| kubectl apply -f -
|
||||||
|
# Keep legacy deployments (forust/anna) in sync with manifests; they have no replicas field, so apply leaves scaling to the user manager only.
|
||||||
|
kubectl apply -f userbot/k8s/base/userbots.yaml
|
||||||
|
kubectl rollout restart deployment/userbot-panel -n userbot
|
||||||
|
kubectl rollout status deployment/userbot-panel -n userbot --timeout=180s
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-main
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
redeploy:
|
||||||
|
runs-on: [self-hosted, linux, arch, homelab, prod]
|
||||||
|
steps:
|
||||||
|
- name: Redeploy workstation
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
||||||
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
||||||
|
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
# Set APPLY_PRUNE=true to enable kubectl apply --prune. Requires every
|
||||||
|
# manifest to carry label app.kubernetes.io/managed-by=homelab-deploy,
|
||||||
|
# otherwise previously applied resources get deleted on the next run.
|
||||||
|
APPLY_PRUNE: ${{ vars.APPLY_PRUNE }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${DEPLOY_HOST:?missing DEPLOY_HOST}"
|
||||||
|
: "${DEPLOY_USER:?missing DEPLOY_USER}"
|
||||||
|
: "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}"
|
||||||
|
|
||||||
|
deploy_port="${DEPLOY_PORT:-22}"
|
||||||
|
deploy_path="${DEPLOY_PATH:-/srv/homelab}"
|
||||||
|
|
||||||
|
ssh_key="$RUNNER_TEMP/deploy_key"
|
||||||
|
mkdir -p "$RUNNER_TEMP"
|
||||||
|
printf '%s\n' "$DEPLOY_KEY" > "$ssh_key"
|
||||||
|
chmod 600 "$ssh_key"
|
||||||
|
|
||||||
|
ssh_opts=(
|
||||||
|
-i "$ssh_key"
|
||||||
|
-p "$deploy_port"
|
||||||
|
-o BatchMode=yes
|
||||||
|
-o StrictHostKeyChecking=accept-new
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||||
|
"DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="${DEPLOY_PATH:-/srv/homelab}"
|
||||||
|
|
||||||
|
if [ ! -d "$repo/.git" ]; then
|
||||||
|
echo "Repository not found at $repo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$repo" fetch origin main
|
||||||
|
git -C "$repo" reset --hard origin/main
|
||||||
|
|
||||||
|
# Runtime selection: a service is k8s-managed when $SERVICE/k8s/active
|
||||||
|
# exists. Otherwise it is compose-managed, and only k8s/routing/*
|
||||||
|
# manifests (external Services / EndpointSlices / ServersTransport /
|
||||||
|
# Ingresses that route to docker backends) are applied.
|
||||||
|
# migrate: touch SERVICE/k8s/active (+ move routing files up)
|
||||||
|
# rollback: rm SERVICE/k8s/active
|
||||||
|
collect_k8s() {
|
||||||
|
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
|
||||||
|
! -path '*/routing/*' ! -path '*/overlays/*' \
|
||||||
|
! -name 'kustomization.y*ml' ! -name '*.example.y*ml' \
|
||||||
|
! -name '*values.y*ml' ! -name 'patch-*.y*ml' \
|
||||||
|
| sort
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_k8s_inactive() {
|
||||||
|
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
|
||||||
|
\( -name 'namespace.y*ml' -o -path '*/routing/*' \) \
|
||||||
|
! -path '*/overlays/*' ! -name '*.example.y*ml' \
|
||||||
|
| sort
|
||||||
|
}
|
||||||
|
|
||||||
|
mapfile -t compose_stacks < <(
|
||||||
|
find "$repo" -type f \( -name 'compose.yaml' -o -name 'compose.yml' \) | sort
|
||||||
|
)
|
||||||
|
|
||||||
|
mapfile -t k8s_manifests < <(
|
||||||
|
for kd in $(find "$repo" -type d -name k8s ! -path '*/.git/*' | sort); do
|
||||||
|
if [ -f "$kd/active" ]; then
|
||||||
|
collect_k8s "$kd"
|
||||||
|
else
|
||||||
|
collect_k8s_inactive "$kd"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "== Validate compose stacks =="
|
||||||
|
for cf in "${compose_stacks[@]}"; do
|
||||||
|
dir=$(dirname "$cf")
|
||||||
|
if [ -f "$dir/k8s/active" ]; then
|
||||||
|
echo " skip (k8s-managed): $dir"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo " config: $cf"
|
||||||
|
docker compose -f "$cf" config --quiet
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== Validate k8s manifests (kubectl dry-run) =="
|
||||||
|
for m in "${k8s_manifests[@]}"; do
|
||||||
|
echo " apply --dry-run=client $m"
|
||||||
|
kubectl apply --dry-run=client -f "$m" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== Applying Kubernetes manifests =="
|
||||||
|
ns_files=()
|
||||||
|
other_files=()
|
||||||
|
for m in "${k8s_manifests[@]}"; do
|
||||||
|
case "$m" in
|
||||||
|
*/namespace.y?ml) ns_files+=("$m") ;;
|
||||||
|
*) other_files+=("$m") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
prune_opts=()
|
||||||
|
if [ "${APPLY_PRUNE:-false}" = "true" ]; then
|
||||||
|
prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#ns_files[@]}" -gt 0 ]; then
|
||||||
|
echo " namespaces first: ${ns_files[*]}"
|
||||||
|
kubectl apply -f "${ns_files[@]}"
|
||||||
|
fi
|
||||||
|
if [ -f "$repo/prometheus-stack/k8s/active" ]; then
|
||||||
|
echo "== Upgrading kube-prometheus-stack =="
|
||||||
|
helm upgrade --install prometheus-stack prometheus-community/kube-prometheus-stack \
|
||||||
|
--namespace prometheus \
|
||||||
|
--version 86.2.3 \
|
||||||
|
--values "$repo/prometheus-stack/k8s/grafana-values.yaml" \
|
||||||
|
--wait
|
||||||
|
fi
|
||||||
|
if [ -f "$repo/loki/k8s/active" ]; then
|
||||||
|
echo "== Upgrading loki/alloy =="
|
||||||
|
helm repo add grafana https://grafana.github.io/helm-charts >/dev/null 2>&1 || true
|
||||||
|
helm repo update grafana >/dev/null 2>&1 || true
|
||||||
|
helm upgrade --install loki grafana/loki \
|
||||||
|
--version 7.3.0 \
|
||||||
|
--namespace prometheus \
|
||||||
|
--values "$repo/loki/k8s/loki-values.yaml" \
|
||||||
|
--wait
|
||||||
|
helm upgrade --install alloy grafana/alloy \
|
||||||
|
--version 1.12.1 \
|
||||||
|
--namespace prometheus \
|
||||||
|
--values "$repo/loki/k8s/alloy-values.yaml" \
|
||||||
|
--wait
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#other_files[@]}" -gt 0 ]; then
|
||||||
|
echo " resources: ${other_files[*]}"
|
||||||
|
kubectl apply "${prune_opts[@]}" -f "${other_files[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== Redeploying docker compose stacks =="
|
||||||
|
for cf in "${compose_stacks[@]}"; do
|
||||||
|
dir=$(dirname "$cf")
|
||||||
|
if [ -f "$dir/k8s/active" ]; then
|
||||||
|
echo " skip (k8s-managed): $dir"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo " compose: $dir"
|
||||||
|
if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then
|
||||||
|
docker compose -f "$cf" build
|
||||||
|
docker compose -f "$cf" push
|
||||||
|
fi
|
||||||
|
docker compose -f "$cf" up -d --pull always --remove-orphans
|
||||||
|
done
|
||||||
|
EOF
|
||||||
+84
-11
@@ -2,35 +2,108 @@
|
|||||||
sync.ffs_lock
|
sync.ffs_lock
|
||||||
.sync.ffs_db
|
.sync.ffs_db
|
||||||
|
|
||||||
# Environment
|
# Copyparty
|
||||||
.env
|
*.hist/
|
||||||
.env.anna
|
|
||||||
.env.forust
|
|
||||||
|
|
||||||
# Volumes and data directories
|
# Volumes, configs and data directories
|
||||||
gitea/gitea-db/
|
gitea/gitea-db/
|
||||||
gitea/gitea-data/*
|
gitea/gitea-data/*
|
||||||
n8n/n8n-data/*
|
n8n/n8n-data/*
|
||||||
n8n/n8n-node-data/*
|
n8n/n8n-node-data/*
|
||||||
adguardhome/data/*
|
adguardhome/conf/*
|
||||||
dockmon/data/*
|
dockmon/data/*
|
||||||
portainer/portainer_data/*
|
portainer/portainer_data/*
|
||||||
metube/MeTube_downloads
|
metube/MeTube_downloads
|
||||||
uptime-kuma/data/
|
uptime-kuma/data/
|
||||||
termix/termix-data/*
|
termix/termix-data/*
|
||||||
|
cfddns/config.json
|
||||||
|
checkmk/checkmk/*
|
||||||
|
downtify/Downtify_downloads
|
||||||
|
headscale/config/*
|
||||||
|
headscale/data/*
|
||||||
|
searxng/core-config/*
|
||||||
|
|
||||||
# Homepage
|
# Steaming services files
|
||||||
homepage/files/assets/images/team/*
|
streaming/jellyfin/*
|
||||||
|
streaming/jellyseerr/*
|
||||||
|
streaming/sonarr/*
|
||||||
|
streaming/radarr/*
|
||||||
|
streaming/data/*
|
||||||
|
streaming/qbittorrent/*
|
||||||
|
streaming/prowlarr/*
|
||||||
|
|
||||||
|
# Homepage
|
||||||
|
homepages/forust_files/.well-known/*
|
||||||
|
|
||||||
# Traefik files
|
# Traefik files
|
||||||
traefik/letsencrypt/acme.json
|
traefik/letsencrypt/acme.json
|
||||||
|
traefik/dynamic/fileservers.yml
|
||||||
|
traefik/dynamic/*.local.y*ml.*
|
||||||
|
traefik/dynamic/*.external.y*ml
|
||||||
|
traefik/k8s/fileservers.y*ml
|
||||||
|
traefik/k8s/aliasHeadersStrategy.md
|
||||||
|
|
||||||
traefik/logs/*
|
traefik/logs/*
|
||||||
|
|
||||||
|
# SSL Certificates
|
||||||
|
adguardhome/certs/*
|
||||||
traefik/certs/*
|
traefik/certs/*
|
||||||
|
certs/
|
||||||
|
|
||||||
# Python
|
# Monitoring
|
||||||
|
monitoring/prometheus.yml
|
||||||
|
|
||||||
|
# Python
|
||||||
.python-version
|
.python-version
|
||||||
.venv/
|
|
||||||
venv/
|
venv/
|
||||||
|
pyc
|
||||||
|
unknown_errors.txt
|
||||||
|
moonlogs.txt
|
||||||
|
thumb.jpg
|
||||||
|
antipm_pic.jpg
|
||||||
|
musicbot/
|
||||||
|
.trunk/
|
||||||
|
previous_profiles/
|
||||||
|
.python-version
|
||||||
|
/modules/__pycache__/
|
||||||
|
__pycache__/
|
||||||
|
*.session
|
||||||
|
*.session-old
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
*-journal
|
||||||
|
/venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
#DataSecurity
|
# DataSecurity
|
||||||
replacements.txt
|
replacements.txt
|
||||||
|
|
||||||
|
# Vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.gitattributes
|
||||||
|
# Gitea/github Runners
|
||||||
|
.runner
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
edu_master/temp/
|
||||||
|
temp/*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.anna
|
||||||
|
.env.forust
|
||||||
|
.env.*
|
||||||
|
!*example
|
||||||
|
|
||||||
|
# kubernetes
|
||||||
|
*/k8s/*secret*
|
||||||
|
!*/k8s/*secret*.example
|
||||||
|
traefik/k8s/local-tls.yaml
|
||||||
|
converters/k8s/config.yaml
|
||||||
|
convertx/k8s/config.yaml
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
ignored:
|
||||||
|
- DL3008
|
||||||
|
- DL3042
|
||||||
|
- DL3018
|
||||||
|
- DL3059
|
||||||
|
trustedRegistries:
|
||||||
|
- docker.io
|
||||||
|
- ghcr.io
|
||||||
|
- quay.io
|
||||||
|
- gcr.forust.xyz
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"MD013": false,
|
||||||
|
"MD024": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD041": false,
|
||||||
|
"MD046": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
bracketSameLine: true
|
||||||
|
htmlWhitespaceSensitivity: css
|
||||||
|
printWidth: 120
|
||||||
|
tabWidth: 2
|
||||||
|
trailingComma: all
|
||||||
|
proseWrap: preserve
|
||||||
|
endOfLine: lf
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
extends: default
|
||||||
|
|
||||||
|
rules:
|
||||||
|
comments:
|
||||||
|
min-spaces-from-content: 1
|
||||||
|
comments-indentation: false
|
||||||
|
document-start: disable
|
||||||
|
line-length: disable
|
||||||
|
braces:
|
||||||
|
min-spaces-inside: 0
|
||||||
|
max-spaces-inside: 1
|
||||||
|
brackets:
|
||||||
|
min-spaces-inside: 0
|
||||||
|
max-spaces-inside: 1
|
||||||
|
indentation:
|
||||||
|
spaces: 2
|
||||||
|
indent-sequences: consistent
|
||||||
|
truthy:
|
||||||
|
allowed-values:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
- "on"
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"tab_size": 2,
|
||||||
|
"soft_wrap": "prefer_line",
|
||||||
|
"preferred_line_length": 120,
|
||||||
|
"format_on_save": "on",
|
||||||
|
"languages": {
|
||||||
|
"YAML": {
|
||||||
|
"tab_size": 2,
|
||||||
|
"hard_tabs": false,
|
||||||
|
"format_on_save": "on",
|
||||||
|
"formatter": {
|
||||||
|
"language_server": { "name": "yaml-language-server" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Python": {
|
||||||
|
"tab_size": 4,
|
||||||
|
"format_on_save": "on",
|
||||||
|
"language_servers": ["pyright", "ruff"],
|
||||||
|
"formatter": {
|
||||||
|
"language_server": { "name": "ruff" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"lsp": {
|
||||||
|
"yaml-language-server": {
|
||||||
|
"settings": {
|
||||||
|
"yaml": {
|
||||||
|
"schemas": {
|
||||||
|
"kubernetes": ["**/k8s/*.yaml", "**/k8s/*.yml"],
|
||||||
|
},
|
||||||
|
"validate": true,
|
||||||
|
"completion": true,
|
||||||
|
"format": {
|
||||||
|
"enable": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
+32
-12
@@ -1,28 +1,48 @@
|
|||||||
services:
|
services:
|
||||||
adguard:
|
adguard:
|
||||||
image: adguard/adguardhome:latest
|
image: adguard/adguardhome:v0.107.79
|
||||||
container_name: adguardhome
|
container_name: adguardhome
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
|
||||||
- TZ=${TZ}
|
|
||||||
ports:
|
ports:
|
||||||
- "53:53/tcp"
|
- "53:53/tcp"
|
||||||
- "53:53/udp"
|
- "53:53/udp"
|
||||||
|
- "853:853/tcp" # DNS over TLS
|
||||||
# - "67:67/udp" # DHCP
|
# - "67:67/udp" # DHCP
|
||||||
# - "68:68/tcp" # DHCP
|
# - "68:68/tcp" # DHCP
|
||||||
- "3000:3000/tcp"
|
# - "3000:3000/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/work:/opt/adguardhome/work
|
- data:/opt/adguardhome/work
|
||||||
- ./data/conf:/opt/adguardhome/conf
|
- ./conf:/opt/adguardhome/conf
|
||||||
networks:
|
- ./certs:/certs:ro
|
||||||
- traefik-proxy
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.http.services.adguard.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.adguard.rule=Host(`dns.forust.xyz`) || Host(`adguard.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.adguard.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.adguard.tls.certresolver=letsencrypt"
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.adguard-local.rule=Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`)"
|
||||||
|
- "traefik.http.routers.adguard-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.adguard-local.tls=true"
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.adguard-dev.rule=Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.adguard-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.adguard-dev.tls=true"
|
||||||
|
# DoH Router
|
||||||
|
- "traefik.http.routers.dns-over-https.rule=(Host(`dns.forust.xyz` || Host(`adguard.forust.xyz`)) && PathPrefix(`/dns-query`))"
|
||||||
|
- "traefik.http.routers.dns-over-https.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dns-over-https.tls.certresolver=letsencrypt"
|
||||||
|
|
||||||
|
# Glance Metadata
|
||||||
- glance.name=adguard
|
- glance.name=adguard
|
||||||
# - glance.icon=si:adguard
|
|
||||||
- glance.url=https://adguard.forust.xyz/
|
- glance.url=https://adguard.forust.xyz/
|
||||||
- glance.description=AdGuard Home is a network-wide software for blocking ads.
|
- glance.description=AdGuard Home is a network-wide software for blocking ads.
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
Whitespace-only changes.
@@ -0,0 +1,116 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: adguard-lb-service
|
||||||
|
namespace: adguard
|
||||||
|
annotations:
|
||||||
|
metallb.io/loadBalancerIPs: "192.168.80.3"
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
externalTrafficPolicy: Local
|
||||||
|
selector:
|
||||||
|
app: adguard
|
||||||
|
ports:
|
||||||
|
- name: dns-udp
|
||||||
|
port: 53
|
||||||
|
targetPort: 53
|
||||||
|
protocol: UDP
|
||||||
|
- name: dns-tcp
|
||||||
|
port: 53
|
||||||
|
targetPort: 53
|
||||||
|
protocol: TCP
|
||||||
|
- name: dot
|
||||||
|
port: 853
|
||||||
|
targetPort: 853
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: adguard-service
|
||||||
|
namespace: adguard
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: adguard
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
name: webui
|
||||||
|
targetPort: 3000
|
||||||
|
- port: 53
|
||||||
|
name: dns
|
||||||
|
targetPort: 53
|
||||||
|
protocol: UDP
|
||||||
|
- port: 53
|
||||||
|
name: dns-tcp
|
||||||
|
targetPort: 53
|
||||||
|
protocol: TCP
|
||||||
|
- port: 853
|
||||||
|
name: dot
|
||||||
|
targetPort: 853
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: adguard-deployment
|
||||||
|
namespace: adguard
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: adguard
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: adguard
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: adguard
|
||||||
|
image: adguard/adguardhome:v0.107.79
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "1.5Gi"
|
||||||
|
cpu: "300m"
|
||||||
|
requests:
|
||||||
|
memory: "500Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
name: webui
|
||||||
|
- containerPort: 53
|
||||||
|
name: dns
|
||||||
|
- containerPort: 853
|
||||||
|
name: dot
|
||||||
|
volumeMounts:
|
||||||
|
- name: adguard-data
|
||||||
|
mountPath: /opt/adguardhome/work
|
||||||
|
subPath: work
|
||||||
|
- name: adguard-data
|
||||||
|
mountPath: /opt/adguardhome/conf
|
||||||
|
subPath: conf
|
||||||
|
- name: adguard-certs
|
||||||
|
mountPath: /certs
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: adguard-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: adguard-pvc
|
||||||
|
- name: adguard-certs
|
||||||
|
secret:
|
||||||
|
secretName: adguard-certs
|
||||||
|
items:
|
||||||
|
- key: tls.crt
|
||||||
|
path: fullchain.pem
|
||||||
|
- key: tls.key
|
||||||
|
path: privkey.pem
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: adguard-pvc
|
||||||
|
namespace: adguard
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: adguard-prod
|
||||||
|
namespace: adguard
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)
|
||||||
|
kind: Rule
|
||||||
|
middlewares:
|
||||||
|
- name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
services:
|
||||||
|
- name: adguard-service
|
||||||
|
port: 3000
|
||||||
|
- match: (Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)) && PathPrefix(`/dns-query`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: adguard-service
|
||||||
|
port: 3000
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: adguard-local
|
||||||
|
namespace: adguard
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`) || Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: adguard-service
|
||||||
|
port: 3000
|
||||||
|
- match: (Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`) || Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)) && PathPrefix(`/dns-query`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: adguard-service
|
||||||
|
port: 3000
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: adguard
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
kubectl apply -f k8s/namespace.yaml && \
|
||||||
|
kubectl create secret tls adguard-certs -n adguard \
|
||||||
|
--cert=certs/fullchain.pem \
|
||||||
|
--key=certs/privkey.pem --dry-run=client -o yaml > \
|
||||||
|
k8s/secrets.yaml
|
||||||
|
|
||||||
|
# OR WITH NO FILE CREATION:
|
||||||
|
kubectl create secret tls adguard-certs -n adguard \
|
||||||
|
--cert=certs/fullchain.pem --key=certs/privkey.pem \
|
||||||
|
--save-config
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# ===================================
|
||||||
|
# Authentification app (authentik)
|
||||||
|
|
||||||
|
# PostgresQL conf
|
||||||
|
PG_PASS=change_this_cuz_its_ur_db_pass
|
||||||
|
PG_USER=authentik # it's okay
|
||||||
|
|
||||||
|
# Image Settings
|
||||||
|
AUTHENTIK_IMAGE=ghcr.io/goauthentik/server
|
||||||
|
AUTHENTIK_TAG=2025.10.2
|
||||||
|
|
||||||
|
# Networking
|
||||||
|
PORT_HTTP=9000
|
||||||
|
PORT_HTTPS=9443 # btw likely already used by portainer
|
||||||
|
|
||||||
|
AUTHENTIK_SECRET_KEY=super_secret_super_scary_authenik_key
|
||||||
|
|
||||||
|
AUTHENTIK_BOOTSTRAP_PASSWORD=pls_change_this
|
||||||
|
|
||||||
|
AUTHENTIK_ERROR_REPORTING__ENABLED=true # Or false to turn off
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: docker.io/library/postgres:15.19-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${PG_DB:-authentik}
|
||||||
|
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
|
||||||
|
POSTGRES_USER: ${PG_USER:-authentik}
|
||||||
|
healthcheck:
|
||||||
|
interval: 30s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
|
||||||
|
timeout: 5s
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- authentik
|
||||||
|
|
||||||
|
server:
|
||||||
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
|
||||||
|
command: server
|
||||||
|
container_name: authentik-server
|
||||||
|
restart: unless-stopped
|
||||||
|
# ports:
|
||||||
|
# - ${PORT_HTTP:-9000}:9000
|
||||||
|
# - ${PORT_HTTPS:-9443}:9443
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||||
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||||
|
volumes:
|
||||||
|
- ./media:/media
|
||||||
|
- ./custom-templates:/templates
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.authentik-server.loadbalancer.server.port=9000"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.authentik-server.rule=Host(`auth.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.authentik-server.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.authentik-server.tls.certresolver=letsencrypt"
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.authentik-server-local.rule=Host(`auth.workstation.internal`)"
|
||||||
|
- "traefik.http.routers.authentik-server-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.authentik-server-local.tls=true"
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.authentik-server-dev.rule=Host(`auth.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.authentik-server-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.authentik-server-dev.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.authentik-server-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- authentik
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
condition: service_healthy
|
||||||
|
worker:
|
||||||
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
|
||||||
|
restart: unless-stopped
|
||||||
|
user: root
|
||||||
|
command: worker
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||||
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./media:/media
|
||||||
|
- ./certs:/certs
|
||||||
|
- ./custom-templates:/templates
|
||||||
|
networks:
|
||||||
|
- authentik
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
|
driver: local
|
||||||
|
networks:
|
||||||
|
authentik:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
Whitespace-only changes.
@@ -0,0 +1,93 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: authentik-server-service
|
||||||
|
namespace: authentik
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: authentik-server
|
||||||
|
ports:
|
||||||
|
- port: 9000
|
||||||
|
targetPort: 9000
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: authentik-worker-service
|
||||||
|
namespace: authentik
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: authentik-worker
|
||||||
|
ports:
|
||||||
|
- port: 9000
|
||||||
|
targetPort: 9000
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: authentik-server-deployment
|
||||||
|
namespace: authentik
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: authentik-server
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: authentik-server
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: authentik-server
|
||||||
|
image: ghcr.io/goauthentik/server:2026.8.2
|
||||||
|
args: ["server"]
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: authentik-config
|
||||||
|
- secretRef:
|
||||||
|
name: authentik-secrets
|
||||||
|
ports:
|
||||||
|
- containerPort: 9000
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "700Mi"
|
||||||
|
cpu: "300m"
|
||||||
|
limits:
|
||||||
|
memory: "1.5Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: authentik-worker-deployment
|
||||||
|
namespace: authentik
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: authentik-worker
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: authentik-worker
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: authentik-worker
|
||||||
|
image: ghcr.io/goauthentik/server:2026.8.2
|
||||||
|
args: ["worker"]
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: authentik-config
|
||||||
|
- secretRef:
|
||||||
|
name: authentik-secrets
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "300m"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "700m"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: authentik-config
|
||||||
|
namespace: authentik
|
||||||
|
data:
|
||||||
|
AUTHENTIK_IMAGE: ghcr.io/goauthentik/server
|
||||||
|
AUTHENTIK_TAG: "2025.10.2"
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgres.database.svc.cluster.local
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||||
|
AUTHENTIK_ERROR_REPORTING__ENABLED: "true"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: authentik-prod
|
||||||
|
namespace: authentik
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`auth.forust.xyz`)
|
||||||
|
kind: Rule
|
||||||
|
middlewares:
|
||||||
|
- name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
services:
|
||||||
|
- name: authentik-server-service
|
||||||
|
port: 9000
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: authentik-local
|
||||||
|
namespace: authentik
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`auth.workstation.internal`) || Host(`auth.gigaforust.internal`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: authentik-server-service
|
||||||
|
port: 9000
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: authentik
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: authentik-secrets
|
||||||
|
namespace: authentik
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
AUTHENTIK_SECRET_KEY: ""
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: ""
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||||
|
AUTHENTIK_BOOTSTRAP_PASSWORD: authentik
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CLOUDFLARE_API_TOKEN=YOUR_CLOUDFLARE_API_TOKEN
|
||||||
|
DOMAINS=example.com,dns.example.com,mc.example.com,auth.example.com,ssh.example.com
|
||||||
|
IP4_DOMAINS=
|
||||||
|
IP6_DOMAINS=
|
||||||
|
IP4_PROVIDER=cloudflare.trace
|
||||||
|
IP6_PROVIDER=none # change if you want to update AAAA
|
||||||
|
UPDATE_CRON=@every 5m
|
||||||
|
UPDATE_ON_START=true
|
||||||
|
DELETE_ON_STOP=false
|
||||||
|
DELETE_ON_FAILURE=true
|
||||||
|
TTL=1
|
||||||
|
PROXIED=!is(dns.example.com) && !is(mc.example.com) && !is(ssh.example.com)
|
||||||
|
EMOJI=true
|
||||||
|
UPTIMEKUMA=https://uptime-kuma.example.com/api/push/AsaSDFGFkfklaFALSKffkfFKfkfkfkFK?status=up&msg=OK&ping=
|
||||||
|
REJECT_CLOUDFLARE_IPS=true
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
cloudflare-ddns:
|
||||||
|
image: timothyjmiller/cloudflare-ddns:2.2.0
|
||||||
|
container_name: cloudflare-ddns
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
network_mode: "host"
|
||||||
|
# https://github.com/timothymiller/cloudflare-ddns#-quick-start
|
||||||
|
environment:
|
||||||
|
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?Cloudflare API token is required}
|
||||||
|
- DOMAINS=${DOMAINS:-}
|
||||||
|
- IP4_DOMAINS=${IP4_DOMAINS:-}
|
||||||
|
- IP6_DOMAINS=${IP6_DOMAINS:-}
|
||||||
|
- IP4_PROVIDER=${IP4_PROVIDER:-cloudflare.trace}
|
||||||
|
- IP6_PROVIDER=${IP6_PROVIDER:-none}
|
||||||
|
- UPDATE_CRON=${UPDATE_CRON:-@every 5m}
|
||||||
|
- UPDATE_ON_START=${UPDATE_ON_START:-true}
|
||||||
|
- DELETE_ON_STOP=${DELETE_ON_STOP:-false}
|
||||||
|
- DELETE_ON_FAILURE=${DELETE_ON_FAILURE:-true}
|
||||||
|
- TTL=${TTL:-1} # 1=auto
|
||||||
|
# to proxy only "dns.example.com" and "wfs.example.com" use "!is(dns.domain.com) && !is (wfs.domain.com)"
|
||||||
|
- PROXIED=${PROXIED:-true}
|
||||||
|
- EMOJI=${EMOJI:-true}
|
||||||
|
- UPTIMEKUMA=${UPTIMEKUMA:-}
|
||||||
|
- HEALTHCHECKS=${HEALTHCHECKS:-}
|
||||||
|
- REJECT_CLOUDFLARE_IPS=${REJECT_CLOUDFLARE_IPS:-true}
|
||||||
|
# volumes:
|
||||||
|
# Prefer using environment variables for configuration, config.json legacy support
|
||||||
|
# - ./config.json:/config.json
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"cloudflare": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "API_TOKEN"
|
||||||
|
},
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
},
|
||||||
|
"zone_id": "your_zone-id",
|
||||||
|
"subdomains": [
|
||||||
|
{ "name": "", "proxied": true },
|
||||||
|
{ "name": "www", "proxied": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a": true,
|
||||||
|
"aaaa": false,
|
||||||
|
"purgeUnknownRecords": false,
|
||||||
|
"ttl": 300
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
secret.yaml
|
||||||
Whitespace-only changes.
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: cfddns
|
||||||
|
labels:
|
||||||
|
app: cfddns
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: cfddns
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: cfddns
|
||||||
|
spec:
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
containers:
|
||||||
|
- name: cloudflare-ddns
|
||||||
|
image: timothyjmiller/cloudflare-ddns:2.2.0
|
||||||
|
imagePullPolicy: Always
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "20Mi"
|
||||||
|
cpu: "30m"
|
||||||
|
limits:
|
||||||
|
memory: "64Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: cfddns-secrets
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cfddns-secrets
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
CLOUDFLARE_API_TOKEN: your_token
|
||||||
|
DOMAINS: "example.com,www.example.com"
|
||||||
|
IP4_PROVIDER: cloudflare.trace
|
||||||
|
IP6_PROVIDER: none
|
||||||
|
UPDATE_CRON: "@every 5m"
|
||||||
|
UPDATE_ON_START: "true"
|
||||||
|
DELETE_ON_STOP: "false"
|
||||||
|
DELETE_ON_FAILURE: "true"
|
||||||
|
TTL: "1"
|
||||||
|
PROXIED: "true"
|
||||||
|
EMOJI: "true"
|
||||||
|
REJECT_CLOUDFLARE_IPS: "true"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CMK_PASSWORD=password
|
||||||
|
TZ=Europe/Berlin
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
services:
|
||||||
|
checkmk:
|
||||||
|
image: "checkmk/check-mk-raw:2.4.0-2026.09.14"
|
||||||
|
container_name: "checkmk"
|
||||||
|
restart: unless-stopped
|
||||||
|
# ports:
|
||||||
|
# - 5000:5000
|
||||||
|
# - 6776:8000
|
||||||
|
volumes:
|
||||||
|
- sites:/omd/sites
|
||||||
|
tmpfs:
|
||||||
|
- /opt/omd/sites/cmk/tmp:uid=1000,gid=1000
|
||||||
|
environment:
|
||||||
|
- CMK_PASSWORD=${CMK_PASSWORD:-password}
|
||||||
|
- CMK_SITE_ID=cmk
|
||||||
|
- TZ=${TZ:-Etc/UTC}
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.checkmk.loadbalancer.server.port=5000"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.checkmk.rule=Host(`cmk.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.checkmk.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.checkmk.tls.certresolver=letsencrypt"
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.checkmk-local.rule=Host(`cmk.workstation.internal`)"
|
||||||
|
- "traefik.http.routers.checkmk-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.checkmk-local.tls=true"
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.checkmk-dev.rule=Host(`cmk.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.checkmk-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.checkmk-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
volumes:
|
||||||
|
sites:
|
||||||
Whitespace-only changes.
@@ -0,0 +1,75 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: checkmk-service
|
||||||
|
namespace: checkmk
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: checkmk
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
port: 5000
|
||||||
|
targetPort: 5000
|
||||||
|
- name: agent-receiver
|
||||||
|
port: 8000
|
||||||
|
targetPort: 8000
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: checkmk-deployment
|
||||||
|
namespace: checkmk
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: checkmk
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: checkmk
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: checkmk
|
||||||
|
image: checkmk/check-mk-raw:2.4.0-2026.09.14
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: checkmk-secrets
|
||||||
|
- configMapRef:
|
||||||
|
name: checkmk-config
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
containerPort: 5000
|
||||||
|
- name: agent-receiver
|
||||||
|
containerPort: 8000
|
||||||
|
volumeMounts:
|
||||||
|
- name: sites
|
||||||
|
mountPath: /omd/sites
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /opt/omd/sites/cmk/tmp
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "600m"
|
||||||
|
limits:
|
||||||
|
memory: "5Gi"
|
||||||
|
cpu: "4"
|
||||||
|
volumes:
|
||||||
|
- name: sites
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: checkmk-sites-pvc
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: checkmk-sites-pvc
|
||||||
|
namespace: checkmk
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: checkmk-config
|
||||||
|
namespace: checkmk
|
||||||
|
data:
|
||||||
|
TZ: Europe/Bratislava
|
||||||
|
CMK_SITE_ID: cmk
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: checkmk-prod
|
||||||
|
namespace: checkmk
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`cmk.forust.xyz`)
|
||||||
|
kind: Rule
|
||||||
|
middlewares:
|
||||||
|
- name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
services:
|
||||||
|
- name: checkmk-service
|
||||||
|
port: 5000
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
metadata:
|
||||||
|
name: checkmk-agent-receiver
|
||||||
|
namespace: checkmk
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- checkmk-agent
|
||||||
|
routes:
|
||||||
|
- match: HostSNI(`*`)
|
||||||
|
services:
|
||||||
|
- name: checkmk-service
|
||||||
|
port: 8000
|
||||||
|
tls:
|
||||||
|
passthrough: true
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: checkmk-local
|
||||||
|
namespace: checkmk
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`cmk.workstation.internal`) || Host(`cmk.gigaforust.internal`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: checkmk-service
|
||||||
|
port: 5000
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: checkmk
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: checkmk-secrets
|
||||||
|
namespace: checkmk
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
CMK_PASSWORD: "password"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
secret.yaml
|
||||||
Whitespace-only changes.
@@ -0,0 +1,37 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: cloudflared
|
||||||
|
labels:
|
||||||
|
app: cloudflared
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: cloudflared
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: cloudflared
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: cloudflared
|
||||||
|
image: cloudflare/cloudflared:2026.1.1
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- tunnel
|
||||||
|
- --no-autoupdate
|
||||||
|
- run
|
||||||
|
env:
|
||||||
|
- name: TUNNEL_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: cloudflared-secrets
|
||||||
|
key: TUNNEL_TOKEN
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "32Mi"
|
||||||
|
cpu: "30m"
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "200m"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cloudflared-secrets
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
TUNNEL_TOKEN: your_tunnel_token_here
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ACCOUNT_REGISTRATION=false
|
||||||
|
HTTP_ALLOWED=false
|
||||||
|
ALLOW_UNAUTHENTICAED=false
|
||||||
|
AUTO_DELETE_EVERY_N_HOURS=24
|
||||||
|
WEBROOT=/convert
|
||||||
|
HIDE_HISTORY=false
|
||||||
|
LANGUAGE=en
|
||||||
|
UNAUTHED_USER_SHARING=false
|
||||||
|
MAX_CONVERT_PROCESS=0
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
services:
|
||||||
|
convertx:
|
||||||
|
container_name: convertx
|
||||||
|
image: ghcr.io/c4illin/convertx:v0.18.0
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "9992:3000"
|
||||||
|
# https://github.com/C4illin/ConvertX#environment-variables
|
||||||
|
environment:
|
||||||
|
- JWT_SECRET=$(JWT_SECRET)
|
||||||
|
- ACCOUNT_REGISTRATION=$(ACCOUNT_REGISTRATION:-false)
|
||||||
|
- HTTP_ALLOWED=$(HTTP_ALLOWED:-false)
|
||||||
|
- ALLOW_UNAUTHENTICATED=$(ALLOW_UNAUTHENTICATED:-false)
|
||||||
|
- AUTO_DELETE_EVERY_N_HOURS=$(AUTO_DELETE_EVERY_N_HOURS:-24)
|
||||||
|
- WEBROOT=$(WEBROOT)
|
||||||
|
- HIDE_HISTORY=$(HIDE_HISTORY:-false)
|
||||||
|
- LANGUAGE=$(LANGUAGE:-en)
|
||||||
|
- UNAUTHENTICATED_USER_SHARING=$(UNAUTHENTICATED_USER_SHARING:-false)
|
||||||
|
- MAX_CONVERT_PROCESS=$(MAX_CONVERT_PROCESS:-0)
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.convertx.loadbalancer.server.port=3000"
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.convertx.rule=(Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/convert`)"
|
||||||
|
- "traefik.http.routers.convertx.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.convertx.priority=50"
|
||||||
|
- "traefik.http.routers.convertx.tls.certresolver=letsencrypt"
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.convertx-local.rule=Host(`workstation.internal`) && PathPrefix(`/convert`)"
|
||||||
|
- "traefik.http.routers.convertx-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.convertx-local.priority=50"
|
||||||
|
- "traefik.http.routers.convertx-local.tls=true"
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.convertx-dev.rule=Host(`gigaforust.internal`) && PathPrefix(`/convert`)"
|
||||||
|
- "traefik.http.routers.convertx-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.convertx-dev.priority=50"
|
||||||
|
- "traefik.http.routers.convertx-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
volumes:
|
||||||
|
- data:/app/data
|
||||||
|
|
||||||
|
bentopdf:
|
||||||
|
container_name: bentopdf
|
||||||
|
image: bentopdf/bentopdf@sha256:4eb4ec8f5030faf87c29a73d3d5a2781f28a597cf440c3ab111eb96aee550871
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.bentopdf.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
# Prod router
|
||||||
|
- "traefik.http.routers.bentopdf.rule=Host(`pdf.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.bentopdf.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.bentopdf.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.bentopdf.tls=true"
|
||||||
|
# Local router
|
||||||
|
- "traefik.http.routers.bentopdf-local.rule=Host(`pdf.wokstation.internal`)"
|
||||||
|
- "traefik.http.routers.bentopdf-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.bentopdf-local.tls=true"
|
||||||
|
# Dev router
|
||||||
|
- "traefik.http.routers.bentopdf-dev.rule=Host(`pdf.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.bentopdf-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.bentopdf-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
Whitespace-only changes.
@@ -0,0 +1,42 @@
|
|||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: bentopdf-service
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: bentopdf
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: bentopdf-deployment
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: bentopdf
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: bentopdf
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: bentopdf/bentopdf@sha256:4eb4ec8f5030faf87c29a73d3d5a2781f28a597cf440c3ab111eb96aee550871
|
||||||
|
imagePullPolicy: Always
|
||||||
|
name: bentopdf
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "50Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
ephemeral-storage: "100Mi"
|
||||||
|
limits:
|
||||||
|
memory: "700Mi"
|
||||||
|
cpu: "700m"
|
||||||
|
ephemeral-storage: "5Gi"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# test manifest with docker and k8s config keys mismatch
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: convertx-config
|
||||||
|
namespace: converters
|
||||||
|
data:
|
||||||
|
ACCOUNT_REGISTRATION: "false"
|
||||||
|
HTTP_ALLOWED: "false"
|
||||||
|
ALLOW_UNAUTHENTICAED: "false"
|
||||||
|
AUTO_DELETE_EVERY_N_HOURS: "24"
|
||||||
|
WEBROOT: "/convert"
|
||||||
|
HIDE_HISTORY: "false"
|
||||||
|
LANGUAGE: "en"
|
||||||
|
UNAUTHED_USER_SHARING: "false"
|
||||||
|
MAX_CONVERT_PROCESS: "0"
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: convertx-service
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: convertx
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
targetPort: 3000
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: convertx-deployment
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: convertx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: convertx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: ghcr.io/c4illin/convertx:v0.18.0
|
||||||
|
name: convertx
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: convertx-config
|
||||||
|
- secretRef:
|
||||||
|
name: convertx-secrets
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data
|
||||||
|
name: data
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "250Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
cpu: "1500m"
|
||||||
|
memory: "1.5Gi"
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: convertx-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: convertx-pvc
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: convertx-prod
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: (Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/convert`)
|
||||||
|
kind: Rule
|
||||||
|
priority: 50
|
||||||
|
services:
|
||||||
|
- name: convertx-service
|
||||||
|
port: 3000
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: convertx-local
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: (Host(`workstation.internal`) || Host(`gigaforust.internal`)) && PathPrefix(`/convert`)
|
||||||
|
kind: Rule
|
||||||
|
priority: 50
|
||||||
|
services:
|
||||||
|
- name: convertx-service
|
||||||
|
port: 3000
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: bentopdf-prod
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`pdf.forust.xyz`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: bentopdf-service
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: bentopdf-local
|
||||||
|
namespace: converters
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`pdf.workstation.internal`) || Host(`pdf.gigaforust.internal`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: bentopdf-service
|
||||||
|
port: 8080
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: converters
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: convertx-secrets
|
||||||
|
namespace: converters
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
jwt-secret: ""
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
spec:
|
||||||
|
plugin:
|
||||||
|
crowdsec-bouncer:
|
||||||
|
enabled: true
|
||||||
|
LogLevel: INFO
|
||||||
|
CrowdsecMode: live
|
||||||
|
CrowdsecLapiScheme: http
|
||||||
|
CrowdsecLapiHost: crowdsec-service.crowdsec.svc.cluster.local:8080
|
||||||
|
CrowdsecLapiKeyFile: "/etc/traefik/secrets/traefik-api-key"
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
container_runtime: containerd
|
||||||
|
|
||||||
|
agent:
|
||||||
|
acquisition: []
|
||||||
|
additionalAcquisition:
|
||||||
|
- labels:
|
||||||
|
type: traefik
|
||||||
|
limit: 1000
|
||||||
|
query: |
|
||||||
|
{namespace="traefik"}
|
||||||
|
source: loki
|
||||||
|
url: http://loki.prometheus.svc.cluster.local:3100/
|
||||||
|
wait_for_ready: 30s
|
||||||
|
env:
|
||||||
|
- name: COLLECTIONS
|
||||||
|
value: crowdsecurity/traefik crowdsecurity/base-http-scenarios
|
||||||
|
- name: DISABLE_COLLECTIONS
|
||||||
|
value: crowdsecurity/sshd
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
serviceMonitor:
|
||||||
|
additionalLabels:
|
||||||
|
release: prometheus-stack
|
||||||
|
enabled: true
|
||||||
|
# Static machine identity: agent pods mount pre-created LAPI credentials
|
||||||
|
# (Secret crowdsec-agent-credentials, key local_api_credentials.yaml)
|
||||||
|
# at the exact path the agent entrypoint expects. Together with the
|
||||||
|
# patched register-init (enforced by janitor-cronjob.yaml) the agent
|
||||||
|
# never calls `cscli lapi register` in steady state, so pod names,
|
||||||
|
# restarts and reboots can no longer break it.
|
||||||
|
extraVolumes:
|
||||||
|
- name: static-creds
|
||||||
|
secret:
|
||||||
|
secretName: crowdsec-agent-credentials
|
||||||
|
items:
|
||||||
|
- key: local_api_credentials.yaml
|
||||||
|
path: local_api_credentials.yaml
|
||||||
|
extraVolumeMounts:
|
||||||
|
- name: static-creds
|
||||||
|
mountPath: /tmp_config/local_api_credentials.yaml
|
||||||
|
subPath: local_api_credentials.yaml
|
||||||
|
readOnly: true
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 500Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 100Mi
|
||||||
|
|
||||||
|
config:
|
||||||
|
parsers:
|
||||||
|
s02-enrich:
|
||||||
|
mobile-whitelist.yaml: |
|
||||||
|
name: forust/mobile-whitelist
|
||||||
|
description: "Whitelist SWAN/4ka mobile network"
|
||||||
|
whitelist:
|
||||||
|
reason: "Mobile IP whitelist"
|
||||||
|
cidr:
|
||||||
|
- "84.245.64.0/18"
|
||||||
|
|
||||||
|
postoverflows:
|
||||||
|
s01-whitelist:
|
||||||
|
home-dynamic-ip.yaml: |
|
||||||
|
name: forust/home-dynamic-ip
|
||||||
|
description: "Whitelist home dynamic IP"
|
||||||
|
whitelist:
|
||||||
|
reason: "Home dynamic IP"
|
||||||
|
expression:
|
||||||
|
- evt.Overflow.Alert.Source.IP in LookupHost("ddns.forust.xyz")
|
||||||
|
|
||||||
|
lapi:
|
||||||
|
env:
|
||||||
|
- name: COLLECTIONS
|
||||||
|
value: crowdsecurity/traefik crowdsecurity/base-http-scenarios
|
||||||
|
- name: DISABLE_COLLECTIONS
|
||||||
|
value: crowdsecurity/linux crowdsecurity/sshd
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
serviceMonitor:
|
||||||
|
additionalLabels:
|
||||||
|
release: prometheus-stack
|
||||||
|
enabled: true
|
||||||
|
persistentVolume:
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
size: 100Mi
|
||||||
|
storageClassName: local-path-retain
|
||||||
|
data:
|
||||||
|
enabled: true
|
||||||
|
size: 1Gi
|
||||||
|
storageClassName: local-path-retain
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 400m
|
||||||
|
memory: 500Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 150Mi
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
storeLAPICscliCredentialsInSecret: true
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
crowdsec-overview.json: "{\n \"__inputs\": [\n {\n \"name\": \"DS_PROMETHEUS\",\n \"label\": \"Prometheus\",\n \"description\": \"\",\n \"type\": \"datasource\",\n \"pluginId\": \"prometheus\",\n \"pluginName\": \"Prometheus\"\n }\n ],\n \"__requires\": [\n {\n \"type\": \"grafana\",\n \"id\": \"grafana\",\n \"name\": \"Grafana\",\n \"version\": \"8.1.2\"\n },\n {\n \"type\": \"panel\",\n \"id\": \"graph\",\n \"name\": \"Graph (old)\",\n \"version\": \"\"\n },\n {\n \"type\": \"datasource\",\n \"id\": \"prometheus\",\n \"name\": \"Prometheus\",\n \"version\": \"1.0.0\"\n },\n {\n \"type\": \"panel\",\n \"id\": \"stat\",\n \"name\": \"Stat\",\n \"version\": \"\"\n },\n {\n \"type\": \"panel\",\n \"id\": \"timeseries\",\n \"name\": \"Time series\",\n \"version\": \"\"\n }\n ],\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": \"-- Grafana --\",\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"gnetId\": null,\n \"graphTooltip\": 0,\n \"id\": null,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"datasource\": null,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 24,\n \"panels\": [],\n \"title\": \"Summary\",\n \"type\": \"row\"\n },\n {\n \"cacheTimeout\": null,\n \"datasource\": \"${DS_PROMETHEUS}\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"#E02F44\",\n \"value\": null\n },\n {\n \"color\": \"#E02F44\",\n \"value\": 10\n },\n {\n \"color\": \"#299c46\",\n \"value\": 10\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 6,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 2,\n \"interval\": null,\n \"links\": [],\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"background\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"text\": {},\n \"textMode\": \"auto\"\n },\n \"pluginVersion\": \"8.1.2\",\n \"targets\": [\n {\n \"exemplar\": true,\n \"expr\": \"count(cs_info)\",\n \"interval\": \"\",\n \"legendFormat\": \"\",\n \"refId\": \"A\"\n }\n ],\n \"timeFrom\": null,\n \"timeShift\": null,\n \"title\": \"Running Crowdsec\",\n \"transparent\": true,\n \"type\": \"stat\"\n },\n {\n \"aliasColors\": {},\n \"bars\": false,\n \"dashLength\": 10,\n \"dashes\": false,\n \"datasource\": \"${DS_PROMETHEUS}\",\n \"decimals\": 1,\n \"fieldConfig\": {\n \"defaults\": {\n \"links\": []\n },\n \"overrides\": []\n },\n \"fill\": 1,\n \"fillGradient\": 0,\n \"gridPos\": {\n \"h\": 8,\n \"w\": 18,\n \"x\": 6,\n \"y\": 1\n },\n \"hiddenSeries\": false,\n \"id\": 8,\n \"legend\": {\n \"alignAsTable\": true,\n \"avg\": false,\n \"current\": false,\n \"max\": false,\n \"min\": false,\n \"rightSide\": true,\n \"show\": true,\n \"sort\": \"total\",\n \"sortDesc\": true,\n \"total\": true,\n \"values\": true\n },\n \"lines\": true,\n \"linewidth\": 1,\n \"nullPointMode\": \"null\",\n \"options\": {\n \"alertThreshold\": true\n },\n \"percentage\": false,\n \"pluginVersion\": \"8.1.2\",\n \"pointradius\": 2,\n \"points\": false,\n \"renLine truncated
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: manual
|
||||||
|
grafana_dashboard: "1"
|
||||||
|
name: crowdsec-crowdsec-overview
|
||||||
|
namespace: prometheus
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
crowdsec-lapi-metrics.json: "{\n \"__inputs\": [\n {\n \"name\": \"DS_PROMETHEUS\",\n \"label\": \"Prometheus\",\n \"description\": \"\",\n \"type\": \"datasource\",\n \"pluginId\": \"prometheus\",\n \"pluginName\": \"Prometheus\"\n }\n ],\n \"__requires\": [\n {\n \"type\": \"panel\",\n \"id\": \"bargauge\",\n \"name\": \"Bar gauge\",\n \"version\": \"\"\n },\n {\n \"type\": \"grafana\",\n \"id\": \"grafana\",\n \"name\": \"Grafana\",\n \"version\": \"8.1.2\"\n },\n {\n \"type\": \"datasource\",\n \"id\": \"prometheus\",\n \"name\": \"Prometheus\",\n \"version\": \"1.0.0\"\n }\n ],\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": \"-- Grafana --\",\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"gnetId\": null,\n \"graphTooltip\": 0,\n \"id\": null,\n \"iteration\": 1655915193937,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": false,\n \"datasource\": null,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 10,\n \"panels\": [],\n \"title\": \"Agents\",\n \"type\": \"row\"\n },\n {\n \"datasource\": \"${DS_PROMETHEUS}\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 1\n },\n \"id\": 2,\n \"options\": {\n \"displayMode\": \"gradient\",\n \"orientation\": \"vertical\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showUnfilled\": false,\n \"text\": {}\n },\n \"pluginVersion\": \"8.1.2\",\n \"repeat\": \"query0\",\n \"repeatDirection\": \"h\",\n \"targets\": [\n {\n \"exemplar\": false,\n \"expr\": \"sum(rate(cs_lapi_request_duration_seconds_bucket{endpoint=\\\"/v1/watchers/login\\\", instance=\\\"$lapi\\\"}[$__rate_interval])) by (le)\",\n \"format\": \"heatmap\",\n \"interval\": \"\",\n \"legendFormat\": \"{{le}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Agents Login\",\n \"type\": \"heatmap\"\n },\n {\n \"datasource\": \"${DS_PROMETHEUS}\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"none\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 1\n },\n \"id\": 6,\n \"options\": {\n \"displayMode\": \"gradient\",\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"showUnfilled\": false,\n \"text\": {}\n },\n \"pluginVersion\": \"8.1.2\",\n \"targets\": [\n {\n \"exemplar\": true,\n \"expr\": \"sum(rate(cs_lapi_request_duration_seconds_bucket{endpoint=\\\"/v1/watchers/login\\\"}[$__rate_interval])) by (le)\",\n \"format\": \"heatmap\",\n \"interval\": \"\",\n \"legendFormat\": \"{{le}}\",\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Heartbeat\",\n \"type\": \"heatmap\"\n },\n {\n \"collapsed\": false,\n \"datasource\": null,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 12,\n \"panels\": [],\n \"title\": \"Decisions\",\n \"type\": \"row\"\n },\n {\n \"datasource\": \"${DS_PROMETHEUS}\",\n Line truncated
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: manual
|
||||||
|
grafana_dashboard: "1"
|
||||||
|
name: crowdsec-crowdsec-lapi-metrics
|
||||||
|
namespace: prometheus
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
crowdsec-insight.json: "{\n \"__inputs\": [\n {\n \"name\": \"DS_PROMETHEUS\",\n \"label\": \"Prometheus\",\n \"description\": \"\",\n \"type\": \"datasource\",\n \"pluginId\": \"prometheus\",\n \"pluginName\": \"Prometheus\"\n }\n ],\n \"__requires\": [\n {\n \"type\": \"panel\",\n \"id\": \"bargauge\",\n \"name\": \"Bar gauge\",\n \"version\": \"\"\n },\n {\n \"type\": \"panel\",\n \"id\": \"gauge\",\n \"name\": \"Gauge\",\n \"version\": \"\"\n },\n {\n \"type\": \"grafana\",\n \"id\": \"grafana\",\n \"name\": \"Grafana\",\n \"version\": \"8.1.2\"\n },\n {\n \"type\": \"datasource\",\n \"id\": \"prometheus\",\n \"name\": \"Prometheus\",\n \"version\": \"1.0.0\"\n },\n {\n \"type\": \"panel\",\n \"id\": \"stat\",\n \"name\": \"Stat\",\n \"version\": \"\"\n }\n ],\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": \"-- Grafana --\",\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"gnetId\": null,\n \"graphTooltip\": 0,\n \"id\": null,\n \"iteration\": 1655915159751,\n \"links\": [],\n \"panels\": [\n {\n \"collapsed\": true,\n \"datasource\": null,\n \"gridPos\": {\n \"h\": 1,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 22,\n \"panels\": [\n {\n \"cacheTimeout\": null,\n \"datasource\": \"${DS_PROMETHEUS}\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"mappings\": [\n {\n \"options\": {\n \"match\": \"null\",\n \"result\": {\n \"text\": \"N/A\"\n }\n },\n \"type\": \"special\"\n }\n ],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"dateTimeAsIso\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 5,\n \"x\": 2,\n \"y\": 1\n },\n \"id\": 2,\n \"interval\": null,\n \"links\": [],\n \"maxDataPoints\": 100,\n \"options\": {\n \"colorMode\": \"none\",\n \"graphMode\": \"none\",\n \"justifyMode\": \"auto\",\n \"orientation\": \"horizontal\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"text\": {},\n \"textMode\": \"auto\"\n },\n \"pluginVersion\": \"8.1.2\",\n \"targets\": [\n {\n \"exemplar\": true,\n \"expr\": \"(process_start_time_seconds{instance=\\\"$instance\\\"})*1000\",\n \"interval\": \"\",\n \"legendFormat\": \"{{instance}}\",\n \"refId\": \"A\"\n }\n ],\n \"timeFrom\": null,\n \"timeShift\": null,\n \"title\": \"Up since\",\n \"type\": \"stat\"\n },\n {\n \"datasource\": \"${DS_PROMETHEUS}\",\n \"fieldConfig\": {\n \"defaults\": {\n \"displayName\": \"\",\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n },\n \"unit\": \"decbytes\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 5,\n \"x\": 7,\n \"y\": 1\n },\n \"id\": 4,\n \"options\": {\n \"orientation\": \"auto\",\n \"reduceOptions\": {\n \"calcs\": [\n \"mean\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\Line truncated
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: manual
|
||||||
|
grafana_dashboard: "1"
|
||||||
|
name: crowdsec-crowdsec-insight
|
||||||
|
namespace: prometheus
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# CrowdSec self-healing: static machine identity + enforcement loops.
|
||||||
|
#
|
||||||
|
# Problem it fixes: the chart's agent init container runs
|
||||||
|
# `cscli lapi register --machine "$POD_NAME" ...`
|
||||||
|
# unconditionally. Credentials live in an emptyDir, the machine row lives
|
||||||
|
# in LAPI's persistent DB. Any init re-run for an already-known pod name
|
||||||
|
# (kubelet restart, node reboot) dies with
|
||||||
|
# 403 Forbidden: user '<pod>' already exist
|
||||||
|
# and the DaemonSet pod sticks in Init forever. Every DS restart also
|
||||||
|
# leaves an orphan machine row that is never cleaned.
|
||||||
|
#
|
||||||
|
# Design (name-independent):
|
||||||
|
# * Agent identity is a STATIC machine `crowdsec-agent-workstation`
|
||||||
|
# whose password lives in Secret `crowdsec-agent-credentials`
|
||||||
|
# (created once, manually - like all other secrets in this repo).
|
||||||
|
# The secret is mounted into agent pods at
|
||||||
|
# /tmp_config/local_api_credentials.yaml (see extraVolumeMounts in
|
||||||
|
# crowdsec-values.yaml), which is exactly the path the agent's main
|
||||||
|
# container copies into place at startup.
|
||||||
|
# * The DS init command is patched (strategic merge, by container name)
|
||||||
|
# to SKIP registration when that file exists, keeping the legacy
|
||||||
|
# register path only as fallback. Detection marker in the patched
|
||||||
|
# command: `[ -s /tmp_config`.
|
||||||
|
# * This CronJob enforces the desired state hourly, so recovery is
|
||||||
|
# automatic even after `helm upgrade` reverts the DS patch or the
|
||||||
|
# LAPI database is wiped:
|
||||||
|
# 1. patch DS init if it still has the unconditional register
|
||||||
|
# (no-op otherwise - no restart churn);
|
||||||
|
# 2. prune machines with no heartbeat for 2h (orphan hygiene);
|
||||||
|
# 3. ensure the static machine exists, recreating it with the
|
||||||
|
# Secret password if missing (agent retry loops reconnect
|
||||||
|
# on their own - same name + same password);
|
||||||
|
# 4. prune bouncer entries idle for 30d.
|
||||||
|
#
|
||||||
|
# Manual apply (crowdsec/k8s is NOT managed by deploy.yaml):
|
||||||
|
# kubectl apply -f crowdsec/k8s/janitor-cronjob.yaml
|
||||||
|
# Force a run:
|
||||||
|
# kubectl create job -n crowdsec --from=cronjob/crowdsec-janitor janitor-now
|
||||||
|
#
|
||||||
|
# Helm upgrades: the janitor's strategic patch puts the DS field under
|
||||||
|
# the `kubectl-patch` field manager, so a plain `helm upgrade` FAILS
|
||||||
|
# with an SSA conflict on initContainers[].command. Procedure:
|
||||||
|
# 1. revert init to chart state (kills the conflict):
|
||||||
|
# helm template crowdsec crowdsec/crowdsec --version <ver> \
|
||||||
|
# -n crowdsec -f crowdsec/k8s/crowdsec-values.yaml > /tmp/r.yaml
|
||||||
|
# python3 -c "import yaml,json; ..." # build revert patch from
|
||||||
|
# the rendered DaemonSet init command, then
|
||||||
|
# kubectl patch ds crowdsec-agent -n crowdsec \
|
||||||
|
# --type strategic -p "\$(cat /tmp/revert_patch.json)"
|
||||||
|
# 2. helm upgrade --install crowdsec ... (no --force needed)
|
||||||
|
# 3. janitor-now right away (upgrade reverts init; new pods would
|
||||||
|
# sit in Init until the next hourly run otherwise).
|
||||||
|
#
|
||||||
|
# One-time bootstrap (order matters):
|
||||||
|
# 1. Create Secret + static machine (see commands in chat).
|
||||||
|
# 2. Apply this file, trigger janitor-now, wait for agent 1/1.
|
||||||
|
# 3. One-time orphan cleanup:
|
||||||
|
# kubectl exec -n crowdsec deploy/crowdsec-lapi -- \
|
||||||
|
# cscli machines prune --duration 1h --force
|
||||||
|
# 4. Only then `helm upgrade` crowdsec with the extraVolumes values.
|
||||||
|
# Upgrade reverts the DS patch; trigger janitor-now right after it
|
||||||
|
# (otherwise new pods sit in Init until the next hourly run, then
|
||||||
|
# self-heal anyway).
|
||||||
|
#
|
||||||
|
# Password rotation: update the Secret, delete the machine
|
||||||
|
# (`cscli machines delete crowdsec-agent-workstation`), trigger
|
||||||
|
# janitor-now (recreates it), then `kubectl rollout restart
|
||||||
|
# ds/crowdsec-agent -n crowdsec` (agent reads the file at startup only).
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-janitor
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: crowdsec
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-janitor
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: crowdsec
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods"]
|
||||||
|
verbs: ["get", "list"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods/exec"]
|
||||||
|
verbs: ["create"]
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["daemonsets"]
|
||||||
|
verbs: ["get", "patch"]
|
||||||
|
# `kubectl exec deploy/<name>` resolves deploy -> replicaset -> pod,
|
||||||
|
# which needs read access to these (exec itself is pods/exec above).
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments", "replicasets"]
|
||||||
|
verbs: ["get", "list"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-janitor
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: crowdsec
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: crowdsec-janitor
|
||||||
|
namespace: crowdsec
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: crowdsec-janitor
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
---
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-janitor
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: crowdsec
|
||||||
|
spec:
|
||||||
|
schedule: "17 * * * *"
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
successfulJobsHistoryLimit: 3
|
||||||
|
failedJobsHistoryLimit: 3
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
activeDeadlineSeconds: 300
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: crowdsec
|
||||||
|
spec:
|
||||||
|
serviceAccountName: crowdsec-janitor
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
containers:
|
||||||
|
- name: janitor
|
||||||
|
# Same image the chart itself uses for registration jobs;
|
||||||
|
# IfNotPresent so it works while the node is offline
|
||||||
|
# (layer cached from the chart install).
|
||||||
|
image: alpine/kubectl:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: AGENT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: crowdsec-agent-credentials
|
||||||
|
key: password
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
LAPI_EXEC="kubectl exec -n crowdsec deploy/crowdsec-lapi --"
|
||||||
|
echo "== 1. enforce patched agent init =="
|
||||||
|
CUR=$(kubectl get ds crowdsec-agent -n crowdsec \
|
||||||
|
-o jsonpath='{.spec.template.spec.initContainers[0].command[2]}')
|
||||||
|
case "$CUR" in
|
||||||
|
*'-s /tmp_config'*)
|
||||||
|
echo "init already patched"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "patching init"
|
||||||
|
WAIT='until nc "$LAPI_HOST" "$LAPI_PORT" -z'
|
||||||
|
WAIT="$WAIT; do echo waiting for lapi to start; sleep 5; done"
|
||||||
|
LINK='ln -s /staging/etc/crowdsec /etc/crowdsec'
|
||||||
|
REG='cscli lapi register --machine "$USERNAME"'
|
||||||
|
REG="$REG -u \"\$LAPI_URL\" --token \"\$REGISTRATION_TOKEN\""
|
||||||
|
CREDS=/tmp_config/local_api_credentials.yaml
|
||||||
|
CMD="$WAIT; $LINK; [ -s $CREDS ] || {"
|
||||||
|
CMD="$CMD $REG && cp"
|
||||||
|
CMD="$CMD /etc/crowdsec/local_api_credentials.yaml $CREDS; }"
|
||||||
|
ESC=$(printf '%s' "$CMD" | sed 's/"/\\"/g')
|
||||||
|
PATCH='{"spec":{"template":{"spec":{"initContainers":'
|
||||||
|
PATCH=$PATCH'[{"name":"wait-for-lapi-and-register",'
|
||||||
|
PATCH=$PATCH'"command":["sh","-c","'$ESC'"]}]}}}}'
|
||||||
|
kubectl patch ds crowdsec-agent -n crowdsec \
|
||||||
|
--type strategic -p "$PATCH"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "== 2. prune orphan machines (no heartbeat for 2h) =="
|
||||||
|
$LAPI_EXEC cscli machines prune --duration 2h --force
|
||||||
|
echo "== 3. ensure static machine exists =="
|
||||||
|
if $LAPI_EXEC cscli machines inspect \
|
||||||
|
crowdsec-agent-workstation >/dev/null 2>&1; then
|
||||||
|
echo "static machine present"
|
||||||
|
else
|
||||||
|
echo "recreating static machine"
|
||||||
|
$LAPI_EXEC cscli machines add crowdsec-agent-workstation \
|
||||||
|
--password "$AGENT_PASSWORD" --force
|
||||||
|
fi
|
||||||
|
echo "== 4. prune stale bouncers (no pull for 30d) =="
|
||||||
|
$LAPI_EXEC cscli bouncers prune -d 720h --force
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: crowdsec
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-lapi
|
||||||
|
namespace: crowdsec
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: crowdsec
|
||||||
|
type: lapi
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: traefik
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: traefik
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: crowdsec
|
||||||
|
type: agent
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: prometheus
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 6060
|
||||||
+39
-23
@@ -1,30 +1,46 @@
|
|||||||
services:
|
services:
|
||||||
dockmon:
|
dockmon:
|
||||||
image: darthnorse/dockmon:latest
|
image: darthnorse/dockmon:2.4.5
|
||||||
container_name: dockmon
|
container_name: dockmon
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
# ports:
|
||||||
- 8000:443
|
# - 8000:443
|
||||||
environment:
|
volumes:
|
||||||
- TZ=Europe/Bratislava
|
- data:/app/data
|
||||||
volumes:
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ./data:/app/data
|
healthcheck:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
test: ["CMD", "curl", "-k", "-f", "https://localhost:443/health"]
|
||||||
healthcheck:
|
interval: 30s
|
||||||
test: ["CMD", "curl", "-k", "-f", "https://localhost:443/health"]
|
timeout: 10s
|
||||||
interval: 30s
|
retries: 3
|
||||||
timeout: 10s
|
labels:
|
||||||
retries: 3
|
|
||||||
networks:
|
|
||||||
- traefik-proxy
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=traefik-proxy"
|
- "traefik.http.services.dockmon.loadbalancer.server.port=443"
|
||||||
|
- "traefik.http.services.dockmon.loadbalancer.server.scheme=https"
|
||||||
|
- "traefik.http.services.dockmon.loadbalancer.serverstransport=insecureTransport@file"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.dockmon.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dockmon.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.routers.dockmon.tls.certresolver=letsencrypt"
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`)"
|
||||||
|
- "traefik.http.routers.dockmon-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dockmon-local.tls=true"
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.dockmon-dev.rule=Host(`dockmon.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.dockmon-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dockmon-dev.tls=true"
|
||||||
|
|
||||||
|
# Glance Metadata
|
||||||
- glance.name=dockmon
|
- glance.name=dockmon
|
||||||
# - glance.icon=sh:dockmon
|
|
||||||
- glance.url=https://dockmon.forust.xyz/
|
- glance.url=https://dockmon.forust.xyz/
|
||||||
- glance.description=Dockmon is a lightweight Docker container monitoring and management tool with a user-friendly web interface.
|
- glance.description=Dockmon is a lightweight Docker container monitoring and management tool with a user-friendly web interface.
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
networks:
|
networks:
|
||||||
traefik-proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
Whitespace-only changes.
@@ -0,0 +1,68 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: dockmon-service
|
||||||
|
namespace: dockmon
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app: dockmon
|
||||||
|
ports:
|
||||||
|
- port: 443
|
||||||
|
targetPort: 443
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: dockmon-statefulset
|
||||||
|
namespace: dockmon
|
||||||
|
spec:
|
||||||
|
serviceName: dockmon-service
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: dockmon
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: dockmon
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: dockmon
|
||||||
|
image: darthnorse/dockmon:2.4.5
|
||||||
|
ports:
|
||||||
|
- containerPort: 443
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: docker-sock
|
||||||
|
mountPath: /var/run/docker.sock
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 443
|
||||||
|
scheme: HTTPS
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "200m"
|
||||||
|
limits:
|
||||||
|
memory: "1.5Gi"
|
||||||
|
cpu: "700m "
|
||||||
|
volumes:
|
||||||
|
- name: docker-sock
|
||||||
|
hostPath:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
type: Socket
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: ServersTransport
|
||||||
|
metadata:
|
||||||
|
name: dockmon-transport
|
||||||
|
namespace: dockmon
|
||||||
|
spec:
|
||||||
|
insecureSkipVerify: true
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: dockmon-prod
|
||||||
|
namespace: dockmon
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`dockmon.forust.xyz`)
|
||||||
|
kind: Rule
|
||||||
|
middlewares:
|
||||||
|
- name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
- name: security-headers@file
|
||||||
|
services:
|
||||||
|
- name: dockmon-service
|
||||||
|
port: 443
|
||||||
|
serversTransport: dockmon-transport
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: dockmon-local
|
||||||
|
namespace: dockmon
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`dockmon.workstation.internal`) || Host(`dockmon.gigaforust.internal`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: dockmon-service
|
||||||
|
port: 443
|
||||||
|
serversTransport: dockmon-transport
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: dockmon
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
services:
|
||||||
|
downtify:
|
||||||
|
container_name: downtify
|
||||||
|
image: ghcr.io/henriquesebastiao/downtify:2.13.0
|
||||||
|
restart: unless-stopped
|
||||||
|
# ports:
|
||||||
|
# - '7077:8000'
|
||||||
|
volumes:
|
||||||
|
- ./Downtify_downloads:/downloads
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.downtify.loadbalancer.server.port=8000"
|
||||||
|
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.downtify.rule=Host(`downtify.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.downtify.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.downtify.middlewares=security-chain@file"
|
||||||
|
- "traefik.http.routers.downtify.tls.certresolver=letsencrypt"
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.downtify-local.rule=Host(`downtify.workstation.internal`)"
|
||||||
|
- "traefik.http.routers.downtify-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.downtify-local.tls=true"
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.downtify-dev.rule=Host(`downtify.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.downtify-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.downtify-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: downtify-service
|
||||||
|
namespace: downtify
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: downtify
|
||||||
|
ports:
|
||||||
|
- port: 8000
|
||||||
|
targetPort: 8000
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: downtify-deployment
|
||||||
|
namespace: downtify
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: downtify
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: downtify
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: downtify
|
||||||
|
image: ghcr.io/henriquesebastiao/downtify:2.13.0
|
||||||
|
ports:
|
||||||
|
- containerPort: 8000
|
||||||
|
volumeMounts:
|
||||||
|
- name: downloads
|
||||||
|
mountPath: /downloads
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "200m"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1"
|
||||||
|
volumes:
|
||||||
|
- name: downloads
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: downtify-downloads-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: downtify-downloads-pvc
|
||||||
|
namespace: downtify
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: downtify-prod
|
||||||
|
namespace: downtify
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`downtify.forust.xyz`)
|
||||||
|
kind: Rule
|
||||||
|
middlewares:
|
||||||
|
- name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
- name: security-chain@file
|
||||||
|
services:
|
||||||
|
- name: downtify-service
|
||||||
|
port: 8000
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: downtify-local
|
||||||
|
namespace: downtify
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`downtify.workstation.internal`) || Host(`downtify.gigaforust.internal`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: downtify-service
|
||||||
|
port: 8000
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: downtify
|
||||||
@@ -3,12 +3,13 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
image: gcr.forust.xyz/forust/dtek-notif:latest
|
||||||
|
pull_policy: build
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Kyiv
|
- TZ=Europe/Kyiv
|
||||||
|
|
||||||
dns:
|
dns:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
+388
-418
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
|||||||
|
EDU_LOGIN=your_edu_login_here
|
||||||
|
EDU_PASSWORD=your_edu_password_here
|
||||||
|
EDU_URL_LOGIN=https://edu.edu.vn.ua/user/login
|
||||||
|
EDU_URL_VERIFY=https://edu.edu.vn.ua/course/userlist
|
||||||
|
PHPSESSID_INTERVAL=10
|
||||||
|
USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
|
||||||
|
WEBINAR_URL=https://edu.edu.vn.ua/webinar/useractive
|
||||||
|
WEBINAR_CHECK_INTERVAL=60
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
PLAYWRIGHT_WS=ws://playwright-service:3000/ws
|
||||||
|
TZ=Europe/Kyiv
|
||||||
|
WEBINAR_TELEGRAM_TOKEN=your_telegram_bot_token_here
|
||||||
|
WEBINAR_ADMIN_ID=123456789
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
ARG VERSION
|
|
||||||
# Use the official WaterCrawl image as the base image
|
|
||||||
FROM watercrawl/watercrawl:${VERSION:-v0.10.2}
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /var/www
|
|
||||||
|
|
||||||
# Copy the extra requirements file
|
|
||||||
COPY extra_requirements.txt /var/www/extra_requirements.txt
|
|
||||||
|
|
||||||
# Install any additional packages
|
|
||||||
RUN poetry run pip install -r /var/www/extra_requirements.txt
|
|
||||||
|
|
||||||
# The rest of the configuration is inherited from the base image
|
|
||||||
# The entrypoint and command should be defined in docker-compose.yml
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Add your additional Python packages here, one per line
|
|
||||||
+36
-249
@@ -1,262 +1,49 @@
|
|||||||
x-app: &app
|
|
||||||
build:
|
|
||||||
context: ./backend/
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- VERSION=${VERSION:-v0.10.2}
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
dns:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 1.1.1.1
|
|
||||||
environment:
|
|
||||||
- SECRET_KEY=${SECRET_KEY:-django-insecure-el4wo4a4--=f0+ag#omp@^w4eq^8v4(scda&1a(td_y2@=sh6&}
|
|
||||||
- API_ENCRYPTION_KEY=${API_ENCRYPTION_KEY:-8zSd6JIuC7ovfZ4AoxG_XmhubW6CPnQWW7Qe_4TD1TQ=}
|
|
||||||
- DEBUG=${DEBUG:-True}
|
|
||||||
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-*}
|
|
||||||
- LANGUAGE_CODE=${LANGUAGE_CODE:-en-us}
|
|
||||||
- TIME_ZONE=${TIME_ZONE:-UTC}
|
|
||||||
- USE_I18N=${USE_I18N:-True}
|
|
||||||
- USE_TZ=${USE_TZ:-True}
|
|
||||||
- STATIC_ROOT=${STATIC_ROOT:-storage/static/}
|
|
||||||
- MEDIA_ROOT=${MEDIA_ROOT:-storage/media/}
|
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
||||||
- REDIS_URL=${REDIS_URL:-redis://redis:6379/1}
|
|
||||||
- DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@${POSTGRES_HOST:-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
|
|
||||||
- CELERY_BROKER_URL=${CELERY_BROKER_URL:-redis://redis:6379/0}
|
|
||||||
- CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND:-django-db}
|
|
||||||
- REDIS_LOCKER_URL=${REDIS_LOCKER_URL:-redis://redis:6379/3}
|
|
||||||
- MINIO_ENDPOINT=minio:9000
|
|
||||||
- MINIO_EXTERNAL_ENDPOINT=nginx
|
|
||||||
- MINIO_REGION=us-east-1
|
|
||||||
- MINIO_ACCESS_KEY=minio
|
|
||||||
- MINIO_SECRET_KEY=minio123
|
|
||||||
- MINIO_USE_HTTPS=False
|
|
||||||
- MINIO_EXTERNAL_ENDPOINT_USE_HTTPS=False
|
|
||||||
- MINIO_URL_EXPIRY_HOURS=7
|
|
||||||
- MINIO_PRIVATE_BUCKET=private
|
|
||||||
- MINIO_PUBLIC_BUCKET=public
|
|
||||||
- CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS:-}
|
|
||||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
|
|
||||||
- CORS_ALLOWED_ORIGIN_REGEXES=${CORS_ALLOWED_ORIGIN_REGEXES:-}
|
|
||||||
- CORS_ALLOW_ALL_ORIGINS=${CORS_ALLOW_ALL_ORIGINS:-False}
|
|
||||||
- FRONTEND_URL=${FRONTEND_URL:-http://localhost}
|
|
||||||
- IS_LOGIN_ACTIVE=${IS_LOGIN_ACTIVE:-True}
|
|
||||||
- IS_SIGNUP_ACTIVE=${IS_SIGNUP_ACTIVE:-True}
|
|
||||||
- IS_GITHUB_LOGIN_ACTIVE=${IS_GITHUB_LOGIN_ACTIVE:-True}
|
|
||||||
- IS_GOOGLE_LOGIN_ACTIVE=${IS_GOOGLE_LOGIN_ACTIVE:-True}
|
|
||||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
|
|
||||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
|
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
|
||||||
- ACCESS_TOKEN_LIFETIME_MINUTES=${ACCESS_TOKEN_LIFETIME_MINUTES:-5}
|
|
||||||
- REFRESH_TOKEN_LIFETIME_DAYS=${REFRESH_TOKEN_LIFETIME_DAYS:-30}
|
|
||||||
- EMAIL_BACKEND=${EMAIL_BACKEND:-django.core.mail.backends.smtp.EmailBackend}
|
|
||||||
- EMAIL_HOST=${EMAIL_HOST:-}
|
|
||||||
- EMAIL_PORT=${EMAIL_PORT:-587}
|
|
||||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-True}
|
|
||||||
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-}
|
|
||||||
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-}
|
|
||||||
- DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL:-}
|
|
||||||
- SCRAPY_USER_AGENT=${SCRAPY_USER_AGENT:-WaterCrawl/0.1 (+https://github.com/watercrawl/watercrawl)}
|
|
||||||
- SCRAPY_ROBOTSTXT_OBEY=${SCRAPY_ROBOTSTXT_OBEY:-True}
|
|
||||||
- SCRAPY_CONCURRENT_REQUESTS=${SCRAPY_CONCURRENT_REQUESTS:-16}
|
|
||||||
- SCRAPY_DOWNLOAD_DELAY=${SCRAPY_DOWNLOAD_DELAY:-0}
|
|
||||||
- SCRAPY_CONCURRENT_REQUESTS_PER_DOMAIN=${SCRAPY_CONCURRENT_REQUESTS_PER_DOMAIN:-4}
|
|
||||||
- SCRAPY_CONCURRENT_REQUESTS_PER_IP=${SCRAPY_CONCURRENT_REQUESTS_PER_IP:-4}
|
|
||||||
- SCRAPY_COOKIES_ENABLED=${SCRAPY_COOKIES_ENABLED:-False}
|
|
||||||
- SCRAPY_HTTPCACHE_ENABLED=${SCRAPY_HTTPCACHE_ENABLED:-True}
|
|
||||||
- SCRAPY_HTTPCACHE_EXPIRATION_SECS=${SCRAPY_HTTPCACHE_EXPIRATION_SECS:-3600}
|
|
||||||
- SCRAPY_HTTPCACHE_DIR=${SCRAPY_HTTPCACHE_DIR:-httpcache}
|
|
||||||
- SCRAPY_LOG_LEVEL=${SCRAPY_LOG_LEVEL:-ERROR}
|
|
||||||
- SCRAPY_GOOGLE_API_KEY=${SCRAPY_GOOGLE_API_KEY:-}
|
|
||||||
- SCRAPY_GOOGLE_CSE_ID=${SCRAPY_GOOGLE_CSE_ID:-}
|
|
||||||
- SCRAPY_MAX_NUMBER_OF_SITEMAP_URLS=${SCRAPY_MAX_NUMBER_OF_SITEMAP_URLS:-20000}
|
|
||||||
- SCRAPY_SITEMAP_CRAWL_PAGE_LIMIT=${SCRAPY_SITEMAP_CRAWL_PAGE_LIMIT:-100}
|
|
||||||
- PLAYWRIGHT_SERVER=${PLAYWRIGHT_SERVER:-http://playwright:8000}
|
|
||||||
- PLAYWRIGHT_API_KEY=${PLAYWRIGHT_API_KEY:-your-secret-api-key}
|
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
|
||||||
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-}
|
|
||||||
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-}
|
|
||||||
- GOOGLE_ANALYTICS_ID=${GOOGLE_ANALYTICS_ID:-}
|
|
||||||
- IS_ENTERPRISE_MODE_ACTIVE=${IS_ENTERPRISE_MODE_ACTIVE:-False}
|
|
||||||
- MAX_CRAWL_DEPTH=${MAX_CRAWL_DEPTH:--1}
|
|
||||||
- CAPTURE_USAGE_HISTORY=${CAPTURE_USAGE_HISTORY:-True}
|
|
||||||
- MCP_SERVER=${MCP_SERVER:-http://localhost/sse}
|
|
||||||
networks:
|
|
||||||
- traefik-proxy
|
|
||||||
- default
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
x-frontend: &frontend
|
|
||||||
image: watercrawl/frontend:${VERSION:-v0.10.2}
|
|
||||||
environment:
|
|
||||||
- VITE_API_BASE_URL=${API_BASE_URL:-http://localhost/api}
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nginx:
|
redis:
|
||||||
image: nginx:alpine
|
image: redis:8.10.1-alpine
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf.template
|
|
||||||
- ./nginx/entrypoint.sh:/entrypoint.sh
|
|
||||||
environment:
|
|
||||||
- MINIO_PRIVATE_BUCKET=${MINIO_PRIVATE_BUCKET:-private}
|
|
||||||
- MINIO_PUBLIC_BUCKET=${MINIO_PUBLIC_BUCKET:-public}
|
|
||||||
command: ["/bin/sh", "/entrypoint.sh"]
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
- frontend
|
|
||||||
- minio
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- traefik-proxy
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik-proxy"
|
|
||||||
|
|
||||||
app:
|
|
||||||
<<: *app
|
|
||||||
command: [ "gunicorn", "-b", "0.0.0.0:9000", "-w", "2", "watercrawl.wsgi:application", "--access-logfile", "-", "--error-logfile", "-", "--timeout", "60" ]
|
|
||||||
|
|
||||||
celery:
|
|
||||||
<<: *app
|
|
||||||
command: [ "celery", "-A", "watercrawl", "worker", "-l", "info", "-S", "django" ]
|
|
||||||
dns:
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
|
|
||||||
celery-beat:
|
|
||||||
<<: *app
|
|
||||||
command: [ "celery", "-A", "watercrawl", "beat", "-l", "info", "-S", "django" ]
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
<<: *frontend
|
|
||||||
command: [ "npm", "run", "serve" ]
|
|
||||||
|
|
||||||
minio:
|
|
||||||
image: minio/minio:RELEASE.2024-11-07T00-52-20Z
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/minio-data:/data
|
- redis-data:/data
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
environment:
|
|
||||||
- MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL:-http://localhost/minio-console/}
|
|
||||||
- MINIO_SERVER_URL=${MINIO_SERVER_URL:-http://localhost/}
|
|
||||||
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY:-minio}
|
|
||||||
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY:-minio123}
|
|
||||||
|
|
||||||
playwright:
|
|
||||||
image: watercrawl/playwright:1.1
|
|
||||||
restart: unless-stopped
|
|
||||||
user: root
|
|
||||||
environment:
|
|
||||||
- AUTH_API_KEY=${PLAYWRIGHT_API_KEY:-your-secret-api-key}
|
|
||||||
- PORT=${PLAYWRIGHT_PORT:-8000}
|
|
||||||
- HOST=${PLAYWRIGHT_HOST:-0.0.0.0}
|
|
||||||
dns:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 1.1.1.1
|
|
||||||
networks:
|
|
||||||
- traefik-proxy
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:17.2-alpine3.21
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-postgres}
|
|
||||||
volumes:
|
|
||||||
- ./volumes/postgres-db:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready" ]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 10s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
mcp:
|
playwright-service:
|
||||||
image: watercrawl/mcp:v1.2.0
|
image: mcr.microsoft.com/playwright:v1.63.0-jammy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: [ "sse", "--base-url", "http://app:9000", '--port', '3000', '--endpoint', '/sse' ]
|
command: npx -y playwright@1.56.0 run-server --port 3000 --path /ws
|
||||||
networks:
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
redis:
|
session-keeper:
|
||||||
image: redis:latest
|
build: ./phpsessid-bot
|
||||||
|
image: gcr.forust.xyz/forust/session-keeper:latest
|
||||||
|
pull_policy: build
|
||||||
|
env_file: .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli -h redis EXISTS EDU_PHPSESSID | grep -q 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
llm:
|
webinar-checker:
|
||||||
image: ollama/ollama:latest
|
build: ./webinar-checker
|
||||||
|
image: gcr.forust.xyz/forust/webinar-checker:latest
|
||||||
|
pull_policy: build
|
||||||
|
env_file: .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
depends_on:
|
||||||
- ./volumes/ollama-models:/root/.ollama
|
redis:
|
||||||
environment:
|
condition: service_healthy
|
||||||
- OLLAMA_DISABLE_TELEMETRY=true
|
session-keeper:
|
||||||
- OLLAMA_KEEP_ALIVE=5m
|
condition: service_healthy
|
||||||
- OLLAMA_HOST=0.0.0.0:11434
|
playwright-service:
|
||||||
- OLLAMA_NUM_PARALLEL=1
|
condition: service_started
|
||||||
- OLLAMA_MAX_LOADED_MODELS=1
|
|
||||||
dns:
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
networks:
|
|
||||||
- n8n
|
|
||||||
|
|
||||||
# docker exec -it edu_master-llm-1 ollama pull neural-chat:7b-q4
|
|
||||||
# docker exec -it edu_master-llm-1 ollama pull mistral:7b-q4
|
|
||||||
|
|
||||||
# lessons-bot:
|
|
||||||
# build:
|
|
||||||
# context: edu_master/lessons_bot/
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# restart: unless-stopped
|
|
||||||
# environment:
|
|
||||||
# - LESSONS_BOT_TOKEN=${LESSONS_BOT_TOKEN}
|
|
||||||
# - N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://n8n:5678/webhook-test/get-lessons}
|
|
||||||
# - N8N_SECRET=${N8N_SECRET:-your-secret-token-here}
|
|
||||||
# - WATERCRAWL_API_URL=${WATERCRAWL_API_URL:-http://app:9000/api}
|
|
||||||
# - PHPSESSID_BOT_URL=${PHPSESSID_BOT_URL:-http://phpsessid-bot:5000}
|
|
||||||
# - EDU_HOST=${EDU_HOST:-edu.edu.vn.ua}
|
|
||||||
# depends_on:
|
|
||||||
# - n8n
|
|
||||||
# - app
|
|
||||||
# - phpsessid-bot
|
|
||||||
# dns:
|
|
||||||
# - 1.1.1.1
|
|
||||||
# - 8.8.8.8
|
|
||||||
# networks:
|
|
||||||
# - default
|
|
||||||
|
|
||||||
phpsessid-bot:
|
|
||||||
build:
|
|
||||||
context: ./phpsessid_bot/
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- EDU_HOST=${EDU_HOST:-edu.edu.vn.ua}
|
|
||||||
- EDU_LOGIN=${EDU_LOGIN}
|
|
||||||
- EDU_PASSWORD=${EDU_PASSWORD}
|
|
||||||
- BOT_PORT=${PHPSESSID_BOT_PORT:-5000}
|
|
||||||
- BOT_HOST=${PHPSESSID_BOT_HOST:-0.0.0.0}
|
|
||||||
dns:
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
n8n_data:
|
redis-data:
|
||||||
postgres-db:
|
|
||||||
minio-data:
|
|
||||||
ollama-models:
|
|
||||||
lmstudio_data:
|
|
||||||
networks:
|
|
||||||
traefik-proxy:
|
|
||||||
external: true
|
|
||||||
Whitespace-only changes.
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: edu-master
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: playwright-service
|
||||||
|
namespace: edu-master
|
||||||
|
labels:
|
||||||
|
app: edu-master-playwright
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: edu-master-playwright
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: edu-master-playwright
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: playwright
|
||||||
|
image: mcr.microsoft.com/playwright:v1.63.0-jammy
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- npx
|
||||||
|
- -y
|
||||||
|
- playwright@1.56.0
|
||||||
|
- run-server
|
||||||
|
- --port
|
||||||
|
- "3000"
|
||||||
|
- --path
|
||||||
|
- /ws
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 3
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: playwright-service
|
||||||
|
namespace: edu-master
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: edu-master-playwright
|
||||||
|
ports:
|
||||||
|
- name: ws
|
||||||
|
port: 3000
|
||||||
|
targetPort: 3000
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: edu-master
|
||||||
|
labels:
|
||||||
|
app: edu-master-redis
|
||||||
|
spec:
|
||||||
|
serviceName: redis
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: edu-master-redis
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: edu-master-redis
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:8.10.1-alpine
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
volumeMounts:
|
||||||
|
- name: redis-data
|
||||||
|
mountPath: /data
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 25m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 256Mi
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["redis-cli", "ping"]
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["redis-cli", "ping"]
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
volumes:
|
||||||
|
- name: redis-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: redis-data-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: redis-data-pvc
|
||||||
|
namespace: edu-master
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: edu-master
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: edu-master-redis
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
port: 6379
|
||||||
|
targetPort: 6379
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# One-time Job to migrate redis state from docker compose to k8s (maintenance window).
|
||||||
|
# The .example file is not applied by the deploy pipeline (mask *.example.yaml).
|
||||||
|
#
|
||||||
|
# Runbook:
|
||||||
|
# 1. docker compose -f <repo>/edu_master/compose.yaml stop # SIGTERM -> redis will flush dump.rdb
|
||||||
|
# 2. docker run --rm -v edu_master_redis-data:/data \
|
||||||
|
# -v /tmp/edu-master-backup:/backup \
|
||||||
|
# redis:alpine sh -c "cp /data/dump.rdb /backup/ && ls -la /backup"
|
||||||
|
# 3. kubectl apply -f edu_master/k8s/namespace.yaml
|
||||||
|
# 4. kubectl apply -f <only the PVC from redis.yaml> # seed must come BEFORE redis pod starts
|
||||||
|
# 5. kubectl apply -f edu_master/k8s/restore-seed-job.yaml.example
|
||||||
|
# kubectl wait --for=condition=complete job/redis-restore-seed -n edu-master --timeout=120s
|
||||||
|
# 6. kubectl delete job redis-restore-seed -n edu-master
|
||||||
|
# 7. kubectl apply -f edu_master/k8s/ -R # apply remaining manifests
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: redis-restore-seed
|
||||||
|
namespace: edu-master
|
||||||
|
spec:
|
||||||
|
backoffLimit: 2
|
||||||
|
ttlSecondsAfterFinished: 3600
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: seed
|
||||||
|
image: redis:alpine
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
ls -la /backup
|
||||||
|
cp /backup/dump.rdb /data/dump.rdb
|
||||||
|
chmod 644 /data/dump.rdb
|
||||||
|
ls -la /data
|
||||||
|
volumeMounts:
|
||||||
|
- name: redis-data
|
||||||
|
mountPath: /data
|
||||||
|
- name: backup
|
||||||
|
mountPath: /backup
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: redis-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: redis-data-pvc
|
||||||
|
- name: backup
|
||||||
|
hostPath:
|
||||||
|
path: /tmp/edu-master-backup
|
||||||
|
type: DirectoryOrCreate
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: edu-master-secrets
|
||||||
|
namespace: edu-master
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Session keeper credentials
|
||||||
|
KEEPER_LOGIN: ""
|
||||||
|
KEEPER_PASSWORD: ""
|
||||||
|
KEEPER_INTERVAL: "10"
|
||||||
|
# EDU links
|
||||||
|
EDU_URL_BASE: "https://edu.edu.vn.ua"
|
||||||
|
EDU_URL_LOGIN: "/user/login"
|
||||||
|
EDU_URL_COURSES: "/course/userlist"
|
||||||
|
EDU_URL_WEBINAR: "/webinar/useractive"
|
||||||
|
# Playwright
|
||||||
|
USER_AGENT: ""
|
||||||
|
PLAYWRIGHT_WS: "ws://playwright-service:3000/ws"
|
||||||
|
# Webinar-checker
|
||||||
|
WEBINAR_TELEGRAM_TOKEN: ""
|
||||||
|
WEBINAR_ADMIN_ID: ""
|
||||||
|
WEBINAR_CHECK_INTERVAL: "60"
|
||||||
|
# Database
|
||||||
|
REDIS_HOST: "redis"
|
||||||
|
REDIS_PORT: "6379"
|
||||||
|
TZ: "Europe/Kyiv"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: session-keeper
|
||||||
|
namespace: edu-master
|
||||||
|
labels:
|
||||||
|
app: edu-master-session-keeper
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: edu-master-session-keeper
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: edu-master-session-keeper
|
||||||
|
spec:
|
||||||
|
initContainers:
|
||||||
|
- name: wait-redis
|
||||||
|
image: redis:8.10.1-alpine
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
i=0
|
||||||
|
until redis-cli -h redis ping | grep -q PONG; do
|
||||||
|
i=$((i+1))
|
||||||
|
[ "$i" -ge 300 ] && echo "TIMEOUT: redis not ready" && exit 1
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "redis is ready"
|
||||||
|
containers:
|
||||||
|
- name: session-keeper
|
||||||
|
image: gcr.forust.xyz/forust/session-keeper:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: edu-master-secrets
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 25m
|
||||||
|
memory: 96Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 256Mi
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["/bin/sh", "-ec", "redis-cli -h redis EXISTS EDU_PHPSESSID | grep -q 1"]
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 10
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: webinar-checker
|
||||||
|
namespace: edu-master
|
||||||
|
labels:
|
||||||
|
app: edu-master-webinar-checker
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: edu-master-webinar-checker
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: edu-master-webinar-checker
|
||||||
|
spec:
|
||||||
|
# Enforces dependency order like compose depends_on:
|
||||||
|
# redis healthy -> session-keeper healthy (EXISTS EDU_PHPSESSID) -> playwright started
|
||||||
|
initContainers:
|
||||||
|
- name: wait-deps
|
||||||
|
image: redis:8.10.1-alpine
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
i=0
|
||||||
|
until redis-cli -h redis ping | grep -q PONG; do
|
||||||
|
i=$((i+1))
|
||||||
|
[ "$i" -ge 300 ] && echo "TIMEOUT: redis not ready" && exit 1
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "redis ok"
|
||||||
|
until [ "$(redis-cli -h redis EXISTS EDU_PHPSESSID)" = "1" ]; do
|
||||||
|
i=$((i+1))
|
||||||
|
[ "$i" -ge 300 ] && echo "TIMEOUT: no PHPSESSID (session-keeper down?)" && exit 1
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "PHPSESSID ok"
|
||||||
|
until nc -z playwright-service 3000; do
|
||||||
|
i=$((i+1))
|
||||||
|
[ "$i" -ge 300 ] && echo "TIMEOUT: playwright-service not reachable" && exit 1
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "playwright ok"
|
||||||
|
containers:
|
||||||
|
- name: webinar-checker
|
||||||
|
image: gcr.forust.xyz/forust/webinar-checker:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: edu-master-secrets
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: "Europe/Kyiv"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "50m"
|
||||||
|
memory: "128Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "600m"
|
||||||
|
memory: "512Mi"
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Установка зависимостей
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Копирование кода
|
|
||||||
COPY config.py .
|
|
||||||
COPY utils.py .
|
|
||||||
COPY handlers.py .
|
|
||||||
COPY main.py .
|
|
||||||
|
|
||||||
# Запуск бота
|
|
||||||
CMD ["python", "-u", "main.py"]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Telegram
|
|
||||||
BOT_TOKEN = os.getenv('LESSONS_BOT_TOKEN')
|
|
||||||
|
|
||||||
# n8n
|
|
||||||
N8N_WEBHOOK_URL = os.getenv('N8N_WEBHOOK_URL', 'http://n8n:5678/webhook/homework-check')
|
|
||||||
N8N_SECRET = os.getenv('N8N_SECRET', 'your-secret-token-here')
|
|
||||||
|
|
||||||
# WaterCrawl API
|
|
||||||
WATERCRAWL_API_URL = os.getenv('WATERCRAWL_API_URL', 'http://app:9000/api')
|
|
||||||
|
|
||||||
# PHPSESSID Bot
|
|
||||||
PHPSESSID_BOT_URL = os.getenv('PHPSESSID_BOT_URL', 'http://phpsessid-bot:5000')
|
|
||||||
|
|
||||||
# EDU site
|
|
||||||
EDU_HOST = os.getenv('EDU_HOST', 'edu.edu.vn.ua')
|
|
||||||
EDU_WEBINAR_URL = f'https://{EDU_HOST}/webinar/useractive'
|
|
||||||
|
|
||||||
# Playwright
|
|
||||||
PLAYWRIGHT_SERVER = os.getenv('PLAYWRIGHT_SERVER', 'http://playwright:8000')
|
|
||||||
PLAYWRIGHT_API_KEY = os.getenv('PLAYWRIGHT_API_KEY', 'your-secret-api-key')
|
|
||||||
WEBINAR_WAIT_TIME = int(os.getenv('WEBINAR_WAIT_TIME', '3')) # Секунды ожидания загрузки
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import logging
|
|
||||||
import requests
|
|
||||||
from telegram import Update
|
|
||||||
from telegram.ext import ContextTypes
|
|
||||||
import config
|
|
||||||
from utils import fetch_webinars, format_webinar_message
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /start"""
|
|
||||||
welcome_message = """
|
|
||||||
Привет! Я бот для проверки домашних заданий и вебинаров.
|
|
||||||
|
|
||||||
<b>Команды:</b>
|
|
||||||
/check - Проверить несделанные уроки
|
|
||||||
/webinar - Проверить активные онлайн уроки
|
|
||||||
/help - Помощь
|
|
||||||
"""
|
|
||||||
await update.message.reply_text(welcome_message, parse_mode='HTML')
|
|
||||||
|
|
||||||
|
|
||||||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /help"""
|
|
||||||
help_text = """
|
|
||||||
<b>Как пользоваться ботом:</b>
|
|
||||||
|
|
||||||
<b>/check</b> - Проверка домашних заданий
|
|
||||||
- Поиск несделанных уроков
|
|
||||||
|
|
||||||
⏱ Проверка занимает 10-30 секунд
|
|
||||||
|
|
||||||
<b>/webinar</b> - Активные онлайн уроки
|
|
||||||
- Проверка активных вебинаровв
|
|
||||||
⏱ Проверка занимает 3-5 секунд
|
|
||||||
"""
|
|
||||||
await update.message.reply_text(help_text, parse_mode='HTML')
|
|
||||||
|
|
||||||
|
|
||||||
async def check_homework(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /check - запускает проверку уроков"""
|
|
||||||
chat_id = update.effective_chat.id
|
|
||||||
user_id = update.effective_user.id
|
|
||||||
username = update.effective_user.username or "unknown"
|
|
||||||
|
|
||||||
# Отправляем уведомление что начали работу
|
|
||||||
status_message = await update.message.reply_text("Запускаю проверку уроков...")
|
|
||||||
|
|
||||||
# Формируем данные для n8n
|
|
||||||
payload = {
|
|
||||||
"chat_id": chat_id,
|
|
||||||
"user_id": user_id,
|
|
||||||
"username": username,
|
|
||||||
"timestamp": update.message.date.isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {config.N8N_SECRET}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Sending request to n8n for user {user_id}")
|
|
||||||
|
|
||||||
# Отправляем запрос в n8n
|
|
||||||
response = requests.post(
|
|
||||||
config.N8N_WEBHOOK_URL,
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
timeout=5 # Короткий таймаут т.к. это асинхронный запрос
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
await status_message.edit_text(
|
|
||||||
"✅ Запрос принят!\n"
|
|
||||||
"🔄 Парсинг сайта и анализ данных...\n"
|
|
||||||
"⏱ Это займет 10-30 секунд"
|
|
||||||
)
|
|
||||||
logger.info(f"Request accepted for user {user_id}")
|
|
||||||
else:
|
|
||||||
await status_message.edit_text(
|
|
||||||
f"Ошибка при отправке запроса. Функция в разработке\n"
|
|
||||||
f"Код: {response.status_code}"
|
|
||||||
)
|
|
||||||
logger.error(f"n8n returned status {response.status_code}")
|
|
||||||
|
|
||||||
except requests.Timeout:
|
|
||||||
await status_message.edit_text("⏱ Запрос обрабатывается (таймаут соединения)")
|
|
||||||
logger.warning(f"Timeout for user {user_id}")
|
|
||||||
except Exception as e:
|
|
||||||
await status_message.edit_text(f"❌ Ошибка: {str(e)}")
|
|
||||||
logger.error(f"Error for user {user_id}: {e}", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def check_webinar(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Команда /webinar - проверяет активные онлайн уроки"""
|
|
||||||
user_id = update.effective_user.id
|
|
||||||
|
|
||||||
# Отправляем уведомление что начали работу
|
|
||||||
status_message = await update.message.reply_text("Проверяю активные вебинары...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Checking webinars for user {user_id}")
|
|
||||||
|
|
||||||
# Получаем список вебинаров
|
|
||||||
webinars = fetch_webinars()
|
|
||||||
|
|
||||||
if webinars is None:
|
|
||||||
await status_message.edit_text(
|
|
||||||
"❌ Не удалось получить информацию о вебинарах\n"
|
|
||||||
"Попробуйте позже или обратитесь к администратору\n"
|
|
||||||
"|@MrForust|mr.forust| Либо же прямо сюда."
|
|
||||||
)
|
|
||||||
logger.error(f"Failed to fetch webinars for user {user_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Форматируем и отправляем результат
|
|
||||||
message = format_webinar_message(webinars)
|
|
||||||
await status_message.edit_text(message, parse_mode='HTML', disable_web_page_preview=True)
|
|
||||||
|
|
||||||
logger.info(f"Webinar check completed for user {user_id}: found {len(webinars)} webinars")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await status_message.edit_text(f"❌ Ошибка: {str(e)}")
|
|
||||||
logger.error(f"Error checking webinars for user {user_id}: {e}", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
||||||
"""Обработчик ошибок"""
|
|
||||||
logger.error(f"Update {update} caused error {context.error}", exc_info=context.error)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import logging
|
|
||||||
from telegram import Update
|
|
||||||
from telegram.ext import Application, CommandHandler
|
|
||||||
import config
|
|
||||||
from handlers import start, help_command, check_homework, check_webinar, error_handler
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
level=logging.INFO
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Запуск бота"""
|
|
||||||
if not config.BOT_TOKEN:
|
|
||||||
logger.error("LESSONS_BOT_TOKEN not set!")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Создаем приложение
|
|
||||||
application = Application.builder().token(config.BOT_TOKEN).build()
|
|
||||||
|
|
||||||
# Регистрируем обработчики команд
|
|
||||||
application.add_handler(CommandHandler("start", start))
|
|
||||||
application.add_handler(CommandHandler("help", help_command))
|
|
||||||
application.add_handler(CommandHandler("check", check_homework))
|
|
||||||
application.add_handler(CommandHandler("webinar", check_webinar))
|
|
||||||
|
|
||||||
# Регистрируем обработчик ошибок
|
|
||||||
application.add_error_handler(error_handler)
|
|
||||||
|
|
||||||
# Запускаем бота
|
|
||||||
logger.info("Lessons Bot started!")
|
|
||||||
logger.info(f"PHPSESSID Bot URL: {config.PHPSESSID_BOT_URL}")
|
|
||||||
logger.info(f"n8n Webhook URL: {config.N8N_WEBHOOK_URL}")
|
|
||||||
|
|
||||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
python-telegram-bot==20.7
|
|
||||||
requests==2.31.0
|
|
||||||
beautifulsoup4==4.12.2
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
lxml==4.9.3
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
import logging
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from typing import Optional, Dict, List
|
|
||||||
import config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_phpsessid() -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Получает валидный PHPSESSID через phpsessid-bot
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: PHPSESSID или None в случае ошибки
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
url = f"{config.PHPSESSID_BOT_URL}/get-session"
|
|
||||||
logger.info(f"Requesting PHPSESSID from {url}")
|
|
||||||
|
|
||||||
response = requests.post(url, timeout=10)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if data.get('success'):
|
|
||||||
phpsessid = data.get('phpsessid')
|
|
||||||
logger.info(f"Got PHPSESSID: {phpsessid[:10]}...")
|
|
||||||
return phpsessid
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to get PHPSESSID: {data.get('error')}")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
logger.error(f"PHPSESSID bot returned status {response.status_code}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting PHPSESSID: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_webinar_table(html_content: str) -> List[Dict[str, str]]:
|
|
||||||
"""
|
|
||||||
Парсит таблицу с вебинарами
|
|
||||||
|
|
||||||
Args:
|
|
||||||
html_content: HTML контент страницы
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict]: Список вебинаров или пустой список
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
soup = BeautifulSoup(html_content, 'html.parser')
|
|
||||||
|
|
||||||
# Находим таблицу с вебинарами
|
|
||||||
meetings_div = soup.find('div', {'id': 'meetings'})
|
|
||||||
if not meetings_div:
|
|
||||||
logger.warning("meetings div not found")
|
|
||||||
return []
|
|
||||||
|
|
||||||
table = meetings_div.find('table', {'class': 'table table-zebra'})
|
|
||||||
if not table:
|
|
||||||
logger.warning("table not found")
|
|
||||||
return []
|
|
||||||
|
|
||||||
tbody = table.find('tbody')
|
|
||||||
if not tbody:
|
|
||||||
logger.warning("tbody not found")
|
|
||||||
return []
|
|
||||||
|
|
||||||
rows = tbody.find_all('tr')
|
|
||||||
if not rows:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Проверяем на сообщение "Жодного онлайн уроку зараз"
|
|
||||||
first_row = rows[0]
|
|
||||||
td = first_row.find('td')
|
|
||||||
if td and 'Жодного онлайн уроку зараз' in td.get_text(strip=True):
|
|
||||||
logger.info("No webinars available")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Парсим активные вебинары
|
|
||||||
webinars = []
|
|
||||||
for row in rows:
|
|
||||||
tds = row.find_all('td')
|
|
||||||
if len(tds) >= 4:
|
|
||||||
webinar = {
|
|
||||||
'topic': tds[0].get_text(strip=True),
|
|
||||||
'course': tds[1].get_text(strip=True),
|
|
||||||
'teacher': tds[2].get_text(strip=True),
|
|
||||||
'join_link': tds[3].find('a')['href'] if tds[3].find('a') else ''
|
|
||||||
}
|
|
||||||
webinars.append(webinar)
|
|
||||||
|
|
||||||
logger.info(f"Parsed {len(webinars)} webinars")
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error parsing webinar table: {e}", exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_webinars_with_playwright() -> Optional[List[Dict[str, str]]]:
|
|
||||||
"""
|
|
||||||
Получает список активных вебинаров используя Playwright для динамического контента
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict]: Список вебинаров или None в случае ошибки
|
|
||||||
"""
|
|
||||||
# Получаем PHPSESSID
|
|
||||||
phpsessid = get_phpsessid()
|
|
||||||
if not phpsessid:
|
|
||||||
logger.error("Failed to get PHPSESSID")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Подготавливаем cookies для Playwright
|
|
||||||
cookies = [
|
|
||||||
{
|
|
||||||
'name': 'PHPSESSID',
|
|
||||||
'value': phpsessid,
|
|
||||||
'domain': config.EDU_HOST,
|
|
||||||
'path': '/'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Запрос к Playwright серверу
|
|
||||||
playwright_request = {
|
|
||||||
'url': config.EDU_WEBINAR_URL,
|
|
||||||
'cookies': cookies,
|
|
||||||
'wait_until': 'networkidle', # Ждем пока сеть успокоится
|
|
||||||
'wait_time': config.WEBINAR_WAIT_TIME * 1000, # Дополнительное ожидание в миллисекундах
|
|
||||||
'user_agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'Bearer {config.PLAYWRIGHT_API_KEY}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"Fetching webinars via Playwright from {config.EDU_WEBINAR_URL}")
|
|
||||||
logger.info(f"Will wait {config.WEBINAR_WAIT_TIME} seconds for dynamic content")
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{config.PLAYWRIGHT_SERVER}/render",
|
|
||||||
json=playwright_request,
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
logger.error(f"Playwright server returned status {response.status_code}")
|
|
||||||
logger.error(f"Response: {response.text}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
html_content = result.get('html', '')
|
|
||||||
|
|
||||||
if not html_content:
|
|
||||||
logger.error("No HTML content in Playwright response")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Парсим таблицу
|
|
||||||
webinars = parse_webinar_table(html_content)
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching webinars via Playwright: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_webinars() -> Optional[List[Dict[str, str]]]:
|
|
||||||
"""
|
|
||||||
Получает список активных вебинаров
|
|
||||||
Сначала пробует через Playwright (для динамического контента),
|
|
||||||
при неудаче - через обычный requests
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict]: Список вебинаров или None в случае ошибки
|
|
||||||
"""
|
|
||||||
# Пробуем через Playwright
|
|
||||||
logger.info("Attempting to fetch via Playwright for dynamic content")
|
|
||||||
webinars = fetch_webinars_with_playwright()
|
|
||||||
|
|
||||||
if webinars is not None:
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
# Fallback на обычный requests
|
|
||||||
logger.warning("Playwright fetch failed, falling back to simple requests")
|
|
||||||
|
|
||||||
# Получаем PHPSESSID
|
|
||||||
phpsessid = get_phpsessid()
|
|
||||||
if not phpsessid:
|
|
||||||
logger.error("Failed to get PHPSESSID")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Запрашиваем страницу с вебинарами
|
|
||||||
try:
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'ru-RU,ru;q=0.9,uk;q=0.8',
|
|
||||||
'Referer': f'https://{config.EDU_HOST}/'
|
|
||||||
}
|
|
||||||
|
|
||||||
cookies = {
|
|
||||||
'PHPSESSID': phpsessid
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"Fetching webinars from {config.EDU_WEBINAR_URL}")
|
|
||||||
response = requests.get(
|
|
||||||
config.EDU_WEBINAR_URL,
|
|
||||||
headers=headers,
|
|
||||||
cookies=cookies,
|
|
||||||
timeout=15
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
logger.error(f"Failed to fetch webinars page: {response.status_code}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Парсим таблицу
|
|
||||||
webinars = parse_webinar_table(response.text)
|
|
||||||
return webinars
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching webinars: {e}", exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def format_webinar_message(webinars: List[Dict[str, str]]) -> str:
|
|
||||||
"""
|
|
||||||
Форматирует список вебинаров для отправки в Telegram
|
|
||||||
|
|
||||||
Args:
|
|
||||||
webinars: Список вебинаров
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Отформатированное сообщение
|
|
||||||
"""
|
|
||||||
if not webinars:
|
|
||||||
return "📭 Жодного онлайн уроку зараз"
|
|
||||||
|
|
||||||
message = "🎓 <b>Активні онлайн уроки:</b>\n\n"
|
|
||||||
|
|
||||||
for i, webinar in enumerate(webinars, 1):
|
|
||||||
message += f"<b>{i}. {webinar['topic']}</b>\n"
|
|
||||||
message += f"📚 Курс: {webinar['course']}\n"
|
|
||||||
message += f"👨🏫 Вчитель: {webinar['teacher']}\n"
|
|
||||||
|
|
||||||
if webinar['join_link']:
|
|
||||||
full_link = webinar['join_link']
|
|
||||||
if not full_link.startswith('http'):
|
|
||||||
full_link = f"https://{config.EDU_HOST}{webinar['join_link']}"
|
|
||||||
message += f"🔗 <a href='{full_link}'>Увійти до уроку</a>\n"
|
|
||||||
|
|
||||||
message += "\n"
|
|
||||||
|
|
||||||
return message
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Replace environment variables in the Nginx configuration template
|
|
||||||
envsubst '${MINIO_PRIVATE_BUCKET} ${MINIO_PUBLIC_BUCKET}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# Start Nginx
|
|
||||||
exec nginx -g 'daemon off;'
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
client_max_body_size 100M;
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://frontend:80;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://app:9000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# MCP
|
|
||||||
location ~ ^/(sse|messages) {
|
|
||||||
proxy_pass http://mcp:3000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
|
|
||||||
# Important SSE settings
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
|
|
||||||
# Disable buffering so events are sent immediately
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_cache off;
|
|
||||||
|
|
||||||
# Increase timeouts so connection stays open
|
|
||||||
proxy_read_timeout 3600s;
|
|
||||||
proxy_send_timeout 3600s;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# MinIO private bucket
|
|
||||||
location /${MINIO_PRIVATE_BUCKET}/ {
|
|
||||||
proxy_pass http://minio:9000/${MINIO_PRIVATE_BUCKET}/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
# MinIO public bucket
|
|
||||||
location /${MINIO_PUBLIC_BUCKET}/ {
|
|
||||||
proxy_pass http://minio:9000/${MINIO_PUBLIC_BUCKET}/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
|
|
||||||
# MinIO API - for direct S3 operations
|
|
||||||
# location /minio/api/ {
|
|
||||||
# proxy_pass http://minio:9000/;
|
|
||||||
# proxy_set_header Host $host;
|
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# proxy_buffering off;
|
|
||||||
# }
|
|
||||||
|
|
||||||
# MinIO Console
|
|
||||||
location /minio-console/ {
|
|
||||||
proxy_pass http://minio:9001/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# Rewrite location headers
|
|
||||||
proxy_redirect / /minio-console/;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends redis-tools && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir requests==2.32.3 redis==5.2.1
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
CMD ["python", "bot.py"]
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import redis
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Load configuration (adapted to .env keys)
|
||||||
|
def _env(key, default=None):
|
||||||
|
v = os.getenv(key, default)
|
||||||
|
if isinstance(v, str) and len(v) >= 2 and ((v[0] == '"' and v[-1] == '"') or (v[0] == "'" and v[-1] == "'")):
|
||||||
|
return v[1:-1]
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
LOGIN = _env('KEEPER_LOGIN')
|
||||||
|
PASSWORD = _env('KEEPER_PASSWORD')
|
||||||
|
|
||||||
|
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
|
||||||
|
EDU_LOGIN_PATH = _env('EDU_URL_LOGIN', '/user/login')
|
||||||
|
EDU_COURSES_PATH = _env('EDU_URL_COURSES', '/course/userlist')
|
||||||
|
URL_LOGIN = f'{EDU_BASE.rstrip("/")}/{EDU_LOGIN_PATH.lstrip("/")}'
|
||||||
|
URL_VERIFY = f'{EDU_BASE.rstrip("/")}/{EDU_COURSES_PATH.lstrip("/")}'
|
||||||
|
|
||||||
|
INTERVAL = int(_env('KEEPER_INTERVAL', 10))
|
||||||
|
USER_AGENT = _env(
|
||||||
|
'USER_AGENT',
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36',
|
||||||
|
)
|
||||||
|
REDIS_HOST = _env('REDIS_HOST', 'redis')
|
||||||
|
REDIS_PORT = int(_env('REDIS_PORT', 6379))
|
||||||
|
|
||||||
|
SUCCESS_FILE = '/tmp/last_success' # noqa: S108
|
||||||
|
|
||||||
|
|
||||||
|
def touch_success_file():
|
||||||
|
"""Updates the timestamp of the success file for healthchecks."""
|
||||||
|
try:
|
||||||
|
with open(SUCCESS_FILE, 'w') as f:
|
||||||
|
f.write(str(datetime.now().timestamp()))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to touch success file: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info('Starting Session Keeper Bot')
|
||||||
|
|
||||||
|
# Connect to Redis
|
||||||
|
try:
|
||||||
|
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
|
||||||
|
redis_client.ping()
|
||||||
|
logger.info(f'Connected to Redis at {REDIS_HOST}:{REDIS_PORT}')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to connect to Redis: {e}')
|
||||||
|
return
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
# Set headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
'Cache-Control': 'max-age=0',
|
||||||
|
'Upgrade-Insecure-Requests': '1',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
'Sec-Fetch-Mode': 'navigate',
|
||||||
|
'Sec-Fetch-User': '?1',
|
||||||
|
'Sec-Fetch-Dest': 'document',
|
||||||
|
'Sec-Ch-Ua': '"Not_A Brand";v="99", "Chromium";v="142"',
|
||||||
|
'Sec-Ch-Ua-Mobile': '?0',
|
||||||
|
'Sec-Ch-Ua-Platform': '"Linux"',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
'Priority': 'u=0, i',
|
||||||
|
}
|
||||||
|
session.headers.update(headers)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.info('Attempting login...')
|
||||||
|
|
||||||
|
# Login payload
|
||||||
|
payload = {'login': LOGIN, 'password': PASSWORD}
|
||||||
|
|
||||||
|
# Perform Login
|
||||||
|
# Note: The user request shows a POST to /user/login with form data
|
||||||
|
# We need to make sure we handle the PHPSESSID correctly.
|
||||||
|
# If we already have a PHPSESSID, requests will send it.
|
||||||
|
|
||||||
|
login_response = session.post(URL_LOGIN, data=payload, allow_redirects=True)
|
||||||
|
|
||||||
|
logger.info(f'Login Response Status: {login_response.status_code}')
|
||||||
|
logger.info(f'Cookies after login: {session.cookies.get_dict()}')
|
||||||
|
|
||||||
|
# Verify Session
|
||||||
|
logger.info('Verifying session...')
|
||||||
|
verify_response = session.get(URL_VERIFY, allow_redirects=False)
|
||||||
|
|
||||||
|
logger.info(f'Verify Response Status: {verify_response.status_code}')
|
||||||
|
|
||||||
|
if verify_response.status_code == 200:
|
||||||
|
logger.info('Session verification SUCCESS (200 OK).')
|
||||||
|
touch_success_file()
|
||||||
|
|
||||||
|
# Save PHPSESSID to Redis
|
||||||
|
phpsessid = session.cookies.get('PHPSESSID')
|
||||||
|
if phpsessid:
|
||||||
|
try:
|
||||||
|
redis_client.set('EDU_PHPSESSID', phpsessid)
|
||||||
|
logger.info(f'Saved PHPSESSID to Redis: {phpsessid}')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to save PHPSESSID to Redis: {e}')
|
||||||
|
elif verify_response.status_code == 302:
|
||||||
|
logger.warning('Session verification FAILED (302 Redirect). Session might be invalid.')
|
||||||
|
else:
|
||||||
|
logger.warning(f'Session verification returned unexpected status: {verify_response.status_code}')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'An error occurred: {e}')
|
||||||
|
|
||||||
|
logger.info(f'Sleeping for {INTERVAL} minutes...')
|
||||||
|
time.sleep(INTERVAL * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Устанавливаем зависимости
|
|
||||||
RUN pip install --no-cache-dir flask requests
|
|
||||||
|
|
||||||
# Копируем код бота
|
|
||||||
COPY main.py .
|
|
||||||
|
|
||||||
# Открываем порт
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Запускаем бот
|
|
||||||
CMD ["python", "-u", "main.py"]
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import os
|
|
||||||
import logging
|
|
||||||
from flask import Flask, request, jsonify
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# Конфигурация из переменных окружения
|
|
||||||
EDU_HOST = os.getenv('EDU_HOST', 'edu.edu.vn.ua')
|
|
||||||
EDU_LOGIN = os.getenv('EDU_LOGIN', '')
|
|
||||||
EDU_PASSWORD = os.getenv('EDU_PASSWORD', '')
|
|
||||||
BOT_PORT = int(os.getenv('BOT_PORT', '5000'))
|
|
||||||
BOT_HOST = os.getenv('BOT_HOST', '0.0.0.0')
|
|
||||||
|
|
||||||
# Кэш для хранения актуальной сессии
|
|
||||||
session_cache = {
|
|
||||||
'phpsessid': None,
|
|
||||||
'expires_at': None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def login_and_get_session():
|
|
||||||
"""
|
|
||||||
Выполняет логин и возвращает новый PHPSESSID
|
|
||||||
"""
|
|
||||||
url = f"https://{EDU_HOST}/user/login"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Cache-Control': 'max-age=0',
|
|
||||||
'Sec-Ch-Ua': '"Chromium";v="141", "Not?A_Brand";v="8"',
|
|
||||||
'Sec-Ch-Ua-Mobile': '?0',
|
|
||||||
'Sec-Ch-Ua-Platform': '"Linux"',
|
|
||||||
'Accept-Language': 'ru-RU,ru;q=0.9',
|
|
||||||
'Origin': f'https://{EDU_HOST}',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Upgrade-Insecure-Requests': '1',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
||||||
'Sec-Fetch-Site': 'same-origin',
|
|
||||||
'Sec-Fetch-Mode': 'navigate',
|
|
||||||
'Sec-Fetch-User': '?1',
|
|
||||||
'Sec-Fetch-Dest': 'document',
|
|
||||||
'Referer': f'https://{EDU_HOST}/',
|
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
|
||||||
'Priority': 'u=0, i'
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'login': EDU_LOGIN,
|
|
||||||
'password': EDU_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Attempting login to {url}")
|
|
||||||
response = requests.post(
|
|
||||||
url,
|
|
||||||
data=data,
|
|
||||||
headers=headers,
|
|
||||||
allow_redirects=False,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
# Получаем PHPSESSID из cookies
|
|
||||||
phpsessid = response.cookies.get('PHPSESSID')
|
|
||||||
|
|
||||||
if phpsessid:
|
|
||||||
logger.info(f"Login successful, got PHPSESSID: {phpsessid[:10]}...")
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'phpsessid': phpsessid,
|
|
||||||
'status_code': response.status_code
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
logger.warning(f"Login failed: no PHPSESSID in response. Status: {response.status_code}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': 'No PHPSESSID in response',
|
|
||||||
'status_code': response.status_code
|
|
||||||
}
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.error(f"Login request failed: {str(e)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def validate_phpsessid(phpsessid):
|
|
||||||
"""
|
|
||||||
Проверяет валидность существующего PHPSESSID
|
|
||||||
"""
|
|
||||||
url = f"https://{EDU_HOST}/"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
|
||||||
}
|
|
||||||
|
|
||||||
cookies = {
|
|
||||||
'PHPSESSID': phpsessid
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers, cookies=cookies, timeout=10)
|
|
||||||
|
|
||||||
# Проверяем, не редиректит ли на страницу логина
|
|
||||||
is_valid = response.status_code == 200 and '/user/login' not in response.url
|
|
||||||
|
|
||||||
return {
|
|
||||||
'valid': is_valid,
|
|
||||||
'status_code': response.status_code,
|
|
||||||
'url': response.url
|
|
||||||
}
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.error(f"Validation request failed: {str(e)}")
|
|
||||||
return {
|
|
||||||
'valid': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
|
||||||
def health():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return jsonify({'status': 'ok', 'timestamp': datetime.now().isoformat()})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/get-session', methods=['POST', 'GET'])
|
|
||||||
def get_session():
|
|
||||||
"""
|
|
||||||
Основной endpoint для получения валидного PHPSESSID
|
|
||||||
Возвращает кэшированную сессию или создает новую
|
|
||||||
"""
|
|
||||||
result = login_and_get_session()
|
|
||||||
|
|
||||||
if result['success']:
|
|
||||||
session_cache['phpsessid'] = result['phpsessid']
|
|
||||||
session_cache['last_updated'] = datetime.now().isoformat()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'phpsessid': result['phpsessid'],
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': result.get('error', 'Login failed'),
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/validate-session', methods=['POST'])
|
|
||||||
def validate_session():
|
|
||||||
"""
|
|
||||||
Проверяет валидность переданного PHPSESSID
|
|
||||||
"""
|
|
||||||
data = request.get_json() or {}
|
|
||||||
phpsessid = data.get('phpsessid') or request.args.get('phpsessid')
|
|
||||||
|
|
||||||
if not phpsessid:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'PHPSESSID not provided'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
validation_result = validate_phpsessid(phpsessid)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'valid': validation_result.get('valid', False),
|
|
||||||
'details': validation_result,
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/refresh-session', methods=['POST', 'GET'])
|
|
||||||
def refresh_session():
|
|
||||||
"""
|
|
||||||
Принудительно обновляет сессию
|
|
||||||
"""
|
|
||||||
result = login_and_get_session()
|
|
||||||
|
|
||||||
if result['success']:
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'phpsessid': result['phpsessid'],
|
|
||||||
'message': 'Session refreshed successfully',
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': result.get('error', 'Failed to refresh session'),
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if not EDU_LOGIN or not EDU_PASSWORD:
|
|
||||||
logger.error("EDU_LOGIN and EDU_PASSWORD must be set!")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
logger.info(f"Starting PHPSESSID validator bot on {BOT_HOST}:{BOT_PORT}")
|
|
||||||
logger.info(f"Target host: {EDU_HOST}")
|
|
||||||
|
|
||||||
app.run(host=BOT_HOST, port=BOT_PORT, debug=False)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
flask==3.0.0
|
|
||||||
requests==2.31.0
|
|
||||||
Werkzeug==3.0.1
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir pip==25.0.1 && pip install --no-cache-dir playwright==1.56.0 redis==5.2.1 requests==2.32.3 "python-telegram-bot[job-queue]==21.10"
|
||||||
|
|
||||||
|
COPY checker.py .
|
||||||
|
|
||||||
|
CMD ["python", "checker.py"]
|
||||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 440 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user