Compare commits
1 commit
main
...
feat/syste
| Author | SHA1 | Date | |
|---|---|---|---|
|
53988d3ee2 |
40 changed files with 424 additions and 1509 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -21,5 +21,3 @@ AGENTS.md
|
|||
k8s/charts/README.md
|
||||
.opencode/
|
||||
opencode.json
|
||||
*.pyc
|
||||
.venv/
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ yaml-files:
|
|||
|
||||
ignore: |
|
||||
k8s/charts/
|
||||
**/sealedsecrets.yaml
|
||||
|
||||
|
||||
extends: default
|
||||
|
|
|
|||
|
|
@ -13,9 +13,3 @@ ethtool -K eno1 tx off rx off
|
|||
```bash
|
||||
helm upgrade argo-cd argo/argo-cd -n argo-cd -f k8s/helm/argo-cd/values.yaml
|
||||
```
|
||||
|
||||
- create a new user and send them an email
|
||||
|
||||
```bash
|
||||
scripts/users.py add -u catalin -n cătălin -e catalin@roboces.dev
|
||||
```
|
||||
|
|
|
|||
|
|
@ -41,6 +41,3 @@ spec:
|
|||
service:
|
||||
dns:
|
||||
type: LoadBalancer
|
||||
web:
|
||||
type: LoadBalancer
|
||||
port: 8006
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: audiobookshelf
|
||||
namespace: argo-cd
|
||||
spec:
|
||||
destination:
|
||||
name: ''
|
||||
namespace: apps-roboces
|
||||
server: https://kubernetes.default.svc
|
||||
sources:
|
||||
- chart: audiobookshelf
|
||||
repoURL: https://charts.christianhuth.de
|
||||
targetRevision: 2.4.0
|
||||
helm:
|
||||
valuesObject:
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 8081
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
hosts:
|
||||
- host: audiobooks.roboces.dev
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: audiobooks.fuku
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
audiobookshelf:
|
||||
timezone: Europe/Madrid
|
||||
config:
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClassName: truenas-nfs-csi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
metadata:
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClassName: truenas-nfs-csi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
backup:
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClassName: truenas-nfs-csi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
library:
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClassName: truenas-nfs-csi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 500Gi
|
||||
project: roboces
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: authelia
|
||||
namespace: argo-cd
|
||||
spec:
|
||||
destination:
|
||||
name: ''
|
||||
namespace: apps-roboces
|
||||
server: https://kubernetes.default.svc
|
||||
sources:
|
||||
- chart: authelia
|
||||
repoURL: https://charts.authelia.com
|
||||
targetRevision: 0.11.6
|
||||
helm:
|
||||
valuesObject:
|
||||
enabled: true
|
||||
pod:
|
||||
autoscaling:
|
||||
enabled: true
|
||||
kind: Deployment
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 1024Mi
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 9091
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
secret:
|
||||
existingSecret: secrets-authelia
|
||||
mountPath: /secrets
|
||||
additionalSecrets:
|
||||
secrets-authelia:
|
||||
path: secrets-authelia
|
||||
items:
|
||||
- key: authentication.ldap.password.txt
|
||||
path: authentication.ldap.password.txt
|
||||
- key: identity_validation.reset_password.jwt.hmac.key
|
||||
path: identity_validation.reset_password.jwt.hmac.key
|
||||
- key: session.encryption.key
|
||||
path: session.encryption.key
|
||||
- key: smtp_password.txt
|
||||
path: smtp_password.txt
|
||||
- key: storage.encryption.key
|
||||
path: storage.encryption.key
|
||||
- key: oidc.audiobookshelf.client_secret
|
||||
path: oidc.audiobookshelf.client_secret
|
||||
- key: identity_providers.oidc.jwks.key.pem
|
||||
path: identity_providers.oidc.jwks.key.pem
|
||||
persistence:
|
||||
enabled: true
|
||||
existingClaim: authelia-data
|
||||
configMap:
|
||||
log:
|
||||
level: info
|
||||
session:
|
||||
name: authelia_session
|
||||
same_site: lax
|
||||
expiration: 1 hour
|
||||
inactivity: 5 minutes
|
||||
remember_me: 1 month
|
||||
cookies:
|
||||
- domain: roboces.dev
|
||||
subdomain: auth
|
||||
- domain: auth.fuku
|
||||
subdomain: ''
|
||||
authentication_backend:
|
||||
ldap:
|
||||
enabled: true
|
||||
implementation: lldap
|
||||
address: ldap://lldap.apps-fuku.svc.cluster.local:3890
|
||||
base_dn: dc=fuku,dc=local
|
||||
additional_users_dn: ou=people
|
||||
users_filter: (&({username_attribute}={input})(objectClass=person))
|
||||
additional_groups_dn: ou=groups
|
||||
groups_filter: (member={dn})
|
||||
user: uid=admin,ou=people,dc=fuku,dc=local
|
||||
attributes:
|
||||
distinguished_name: dn
|
||||
username: uid
|
||||
display_name: cn
|
||||
mail: mail
|
||||
member_of: memberOf
|
||||
group_name: cn
|
||||
password:
|
||||
disabled: false
|
||||
secret_name: secrets-authelia
|
||||
path: authentication.ldap.password.txt
|
||||
storage:
|
||||
local:
|
||||
enabled: true
|
||||
path: /config/db.sqlite3
|
||||
notifier:
|
||||
smtp:
|
||||
enabled: true
|
||||
address: submissions://mail.fukurokuju.dev:465
|
||||
sender: Authelia <auth@fukurokuju.dev>
|
||||
subject: "Your verification code is {code}"
|
||||
identifier: auth.fuku
|
||||
startup_check_address: test@authelia.com
|
||||
disable_html_emails: false
|
||||
disable_require_tls: false
|
||||
disable_starttls: false
|
||||
username: auth@fukurokuju.dev
|
||||
password:
|
||||
disabled: false
|
||||
secret_name: secrets-authelia
|
||||
path: smtp_password.txt
|
||||
tls:
|
||||
server_name: mail.fukurokuju.dev
|
||||
skip_verify: false
|
||||
minimum_version: TLS1.2
|
||||
access_control:
|
||||
default_policy: two_factor
|
||||
identity_providers:
|
||||
oidc:
|
||||
enabled: true
|
||||
hmac_secret:
|
||||
path: identity_providers.oidc.hmac.key
|
||||
jwks:
|
||||
- key_id: authelia
|
||||
algorithm: RS256
|
||||
use: sig
|
||||
key:
|
||||
path: /secrets/secrets-authelia/identity_providers.oidc.jwks.key.pem
|
||||
clients:
|
||||
- client_id: audiobookshelf
|
||||
client_name: Audiobookshelf
|
||||
client_secret:
|
||||
path: /secrets/secrets-authelia/oidc.audiobookshelf.client_secret
|
||||
public: false
|
||||
authorization_policy: two_factor
|
||||
require_pkce: true
|
||||
pkce_challenge_method: S256
|
||||
redirect_uris:
|
||||
- https://audiobooks.roboces.dev/audiobookshelf/auth/openid/callback
|
||||
- https://audiobooks.roboces.dev/audiobookshelf/auth/openid/mobile-redirect
|
||||
- https://audiobooks.fuku/audiobookshelf/auth/openid/callback
|
||||
- https://audiobooks.fuku/audiobookshelf/auth/openid/mobile-redirect
|
||||
- audiobookshelf://oauth
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- groups
|
||||
- email
|
||||
response_types:
|
||||
- code
|
||||
grant_types:
|
||||
- authorization_code
|
||||
access_token_signed_response_alg: none
|
||||
userinfo_signed_response_alg: none
|
||||
token_endpoint_auth_method: client_secret_basic
|
||||
- repoURL: https://git.roboces.dev/catalin/fukuops.git
|
||||
path: k8s/services/authelia
|
||||
targetRevision: main
|
||||
project: roboces
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
|
|
@ -12,7 +12,7 @@ spec:
|
|||
sources:
|
||||
- chart: authentik
|
||||
repoURL: https://charts.goauthentik.io/
|
||||
targetRevision: 2026.8.*
|
||||
targetRevision: 2026.5.*
|
||||
helm:
|
||||
valuesObject:
|
||||
authentik:
|
||||
|
|
@ -64,8 +64,8 @@ spec:
|
|||
minAvailable: 1
|
||||
service:
|
||||
type: LoadBalancer
|
||||
servicePortHttp: 9001
|
||||
servicePortHttps: 9444
|
||||
servicePortHttp: 9000
|
||||
servicePortHttps: 9443
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/service.serversscheme: https
|
||||
traefik.ingress.kubernetes.io/service.serverstransport: apps-fuku-skipverify-authentik@kubernetescrd # yamllint disable rule:line-length
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ spec:
|
|||
sources:
|
||||
- chart: forgejo
|
||||
repoURL: code.forgejo.org/forgejo-helm
|
||||
targetRevision: 17.1.5
|
||||
targetRevision: 17.1.4
|
||||
helm:
|
||||
valuesObject:
|
||||
replicaCount: 1
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: lldap
|
||||
namespace: argo-cd
|
||||
spec:
|
||||
destination:
|
||||
name: ''
|
||||
namespace: apps-fuku
|
||||
server: https://kubernetes.default.svc
|
||||
sources:
|
||||
- repoURL: git.roboces.dev/catalin/fukuops
|
||||
chart: lldap
|
||||
targetRevision: 1.0.1
|
||||
helm:
|
||||
valuesObject:
|
||||
lldap:
|
||||
service:
|
||||
type: LoadBalancer
|
||||
httpPort: 17170
|
||||
ldapPort: 3890
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
hosts:
|
||||
- host: ldap.fuku
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
- host: ldap.fukurokuju.dev
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
config:
|
||||
ldapBaseDn: "dc=fuku,dc=local"
|
||||
httpUrl: "https://ldap.fukurokuju.dev"
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClass: "truenas-nfs-csi"
|
||||
accessMode: ReadWriteMany
|
||||
size: 5Gi
|
||||
secret:
|
||||
existingSecretName: "lldap-secrets"
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
project: fuku
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
|
|
@ -18,7 +18,7 @@ spec:
|
|||
targetRevision: main
|
||||
- chart: meilisearch
|
||||
repoURL: https://meilisearch.github.io/meilisearch-kubernetes
|
||||
targetRevision: 0.38.*
|
||||
targetRevision: 0.37.*
|
||||
helm:
|
||||
valuesObject:
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: pgadmin
|
||||
namespace: argo-cd
|
||||
spec:
|
||||
destination:
|
||||
name: ''
|
||||
namespace: apps-fuku
|
||||
server: https://kubernetes.default.svc
|
||||
project: fuku
|
||||
sources:
|
||||
- chart: pgadmin4
|
||||
repoURL: https://helm.runix.net
|
||||
targetRevision: 1.66.0
|
||||
helm:
|
||||
valuesObject:
|
||||
image:
|
||||
tag: "9.7"
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 8010
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: traefik
|
||||
hosts:
|
||||
- host: pgadmin.fuku
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
env:
|
||||
email: admin@fukurokuju.dev
|
||||
enhanced_cookie_protection: "False"
|
||||
existingSecret: secrets-pgadmin
|
||||
persistentVolume:
|
||||
enabled: true
|
||||
existingClaim: pgadmin-pgadmin4
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
size: 10Gi
|
||||
storageClass: truenas-nfs-csi
|
||||
test:
|
||||
enabled: false
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
|
|
@ -20,9 +20,6 @@ spec:
|
|||
valuesObject:
|
||||
service:
|
||||
type: LoadBalancer
|
||||
httpPort: 9002
|
||||
httpsPort: 9445
|
||||
edgePort: 8005
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: traefik
|
||||
|
|
@ -30,7 +27,6 @@ spec:
|
|||
- host: containers.fukurokuju.dev
|
||||
paths:
|
||||
- path: /
|
||||
port: 9002
|
||||
tls: []
|
||||
persistence:
|
||||
storageClass: truenas-nfs-csi
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ spec:
|
|||
sources:
|
||||
- chart: renovate
|
||||
repoURL: https://docs.renovatebot.com/helm-charts
|
||||
targetRevision: 46.254.*
|
||||
targetRevision: 46.251.*
|
||||
helm:
|
||||
valuesObject:
|
||||
renovate:
|
||||
|
|
|
|||
18
k8s/argo-apps/system-upgrade-controller.yaml
Normal file
18
k8s/argo-apps/system-upgrade-controller.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: system-upgrade-controller
|
||||
namespace: argo-cd
|
||||
spec:
|
||||
destination:
|
||||
name: ''
|
||||
namespace: system-upgrade
|
||||
server: https://kubernetes.default.svc
|
||||
source:
|
||||
repoURL: https://git.roboces.dev/catalin/fukuops.git
|
||||
path: k8s/services/system-upgrade-controller
|
||||
targetRevision: feat/system-upgrade-controller
|
||||
project: management
|
||||
syncPolicy:
|
||||
automated: {}
|
||||
|
|
@ -26,7 +26,6 @@ spec:
|
|||
enabled: true
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 8007
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
|
|
@ -36,7 +35,7 @@ spec:
|
|||
- path: /
|
||||
pathType: Prefix
|
||||
backend: dashboard
|
||||
port: 8007
|
||||
port: 80
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend: api
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ spec:
|
|||
sources:
|
||||
- repoURL: git.roboces.dev/catalin/fukuops
|
||||
chart: vaultwarden
|
||||
targetRevision: 1.0.3
|
||||
targetRevision: 1.0.2
|
||||
helm:
|
||||
valuesObject:
|
||||
vaultwarden:
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching.
|
||||
*.tgz
|
||||
.env
|
||||
.git
|
||||
.git_cache
|
||||
.idea
|
||||
*.md
|
||||
docs/*
|
||||
tmp/*
|
||||
vendor/*
|
||||
.travis.yml
|
||||
.testignore
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v2
|
||||
name: lldap
|
||||
description: Light LDAP implementation for authentication
|
||||
type: application
|
||||
version: 1.0.0
|
||||
appVersion: stable
|
||||
annotations:
|
||||
artifacthub.io/images: "- name: lldap\n image: lldap/lldap:stable\n"
|
||||
artifacthub.io/changes: "- kind: added\n description: Initial chart release\n"
|
||||
artifacthub.io/maintainers: "- name: catalin\n email: catalin@roboces.dev\n"
|
||||
keywords:
|
||||
- lldap
|
||||
- ldap
|
||||
- authentication
|
||||
- identity
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
# lldap
|
||||
|
||||
Lightweight LDAP authentication server with a web UI.
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
helm install lldap oci://git.roboces.dev/catalin/fukuops/lldap --version 1.0.0 -n apps-roboces
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.19+
|
||||
- Helm 3+
|
||||
- A NFS storage class (default: `truenas-nfs-csi`) - only if persistence is enabled
|
||||
- An existing `Secret` with credentials (see `lldap.secret.existingSecretName`)
|
||||
|
||||
## Configuration
|
||||
|
||||
All values are nested under the `lldap:` key. Example:
|
||||
|
||||
```yaml
|
||||
lldap:
|
||||
service:
|
||||
type: LoadBalancer
|
||||
httpPort: 17170
|
||||
ldapPort: 3890
|
||||
config:
|
||||
ldapBaseDn: "dc=example,dc=com"
|
||||
httpUrl: "https://lldap.example.com"
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClass: "truenas-nfs-csi"
|
||||
accessMode: ReadWriteMany
|
||||
size: 10Gi
|
||||
secret:
|
||||
existingSecretName: lldap-secrets
|
||||
```
|
||||
|
||||
## Secret Requirements
|
||||
|
||||
The secret must contain the following keys:
|
||||
|
||||
- `LLDAP_JWT_SECRET` - JWT signing secret (generate with `openssl rand -base64 32`)
|
||||
- `LLDAP_KEY_SEED` - Key seed for password hashing (generate with `openssl rand -base64 32`)
|
||||
- `LLDAP_LDAP_USER_PASS` - Admin password for LDAP and web UI
|
||||
|
||||
Example secret:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: lldap-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
LLDAP_JWT_SECRET: <your-jwt-secret>
|
||||
LLDAP_KEY_SEED: <your-key-seed>
|
||||
LLDAP_LDAP_USER_PASS: <your-admin-password>
|
||||
```
|
||||
|
||||
## Values Reference
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `lldap.replicaCount` | int | `1` | Number of replicas |
|
||||
| `lldap.image.repository` | string | `lldap/lldap` | Image repository |
|
||||
| `lldap.image.pullPolicy` | string | `IfNotPresent` | Image pull policy |
|
||||
| `lldap.image.tag` | string | `stable` | Image tag |
|
||||
| `lldap.service.type` | string | `LoadBalancer` | Service type |
|
||||
| `lldap.service.httpPort` | int | `17170` | Web UI port |
|
||||
| `lldap.service.ldapPort` | int | `3890` | LDAP port |
|
||||
| `lldap.config.ldapBaseDn` | string | `dc=example,dc=com` | LDAP base DN |
|
||||
| `lldap.config.httpUrl` | string | `https://lldap.example.com` | Public URL for web UI |
|
||||
| `lldap.config.databaseUrl` | string | `""` | Database URL (empty = SQLite) |
|
||||
| `lldap.config.verbose` | bool | `false` | Enable verbose logging |
|
||||
| `lldap.ldaps.enabled` | bool | `false` | Enable LDAPS |
|
||||
| `lldap.ldaps.certFile` | string | `""` | TLS certificate file path |
|
||||
| `lldap.ldaps.keyFile` | string | `""` | TLS key file path |
|
||||
| `lldap.smtp.enabled` | bool | `false` | Enable SMTP for password reset |
|
||||
| `lldap.smtp.enablePasswordReset` | bool | `false` | Enable password reset via email |
|
||||
| `lldap.smtp.server` | string | `""` | SMTP server |
|
||||
| `lldap.smtp.port` | int | `465` | SMTP port |
|
||||
| `lldap.smtp.encryption` | string | `"TLS"` | SMTP encryption (NONE, TLS, STARTTLS) |
|
||||
| `lldap.smtp.user` | string | `""` | SMTP username |
|
||||
| `lldap.smtp.fromAddress` | string | `""` | From email address |
|
||||
| `lldap.smtp.fromName` | string | `""` | From name |
|
||||
| `lldap.smtp.replyTo` | string | `""` | Reply-to address |
|
||||
| `lldap.persistence.enabled` | bool | `false` | Enable persistence |
|
||||
| `lldap.persistence.storageClass` | string | `truenas-nfs-csi` | Storage class |
|
||||
| `lldap.persistence.accessMode` | string | `ReadWriteMany` | Access mode |
|
||||
| `lldap.persistence.size` | string | `1Gi` | PVC size |
|
||||
| `lldap.secret.existingSecretName` | string | `""` | Existing secret name |
|
||||
| `lldap.debug.enabled` | bool | `false` | Enable debug mode |
|
||||
| `lldap.resources` | object | `{}` | Container resources |
|
||||
| `lldap.livenessProbe` | object | (see values.yaml) | Liveness probe |
|
||||
| `lldap.readinessProbe` | object | (see values.yaml) | Readiness probe |
|
||||
| `lldap.nodeSelector` | object | `{}` | Node selector |
|
||||
| `lldap.tolerations` | array | `[]` | Tolerations |
|
||||
| `lldap.affinity` | object | `{}` | Affinity rules |
|
||||
|
||||
## Database
|
||||
|
||||
By default, lldap uses SQLite stored in `/data`. To use an external database:
|
||||
|
||||
```yaml
|
||||
lldap:
|
||||
config:
|
||||
databaseUrl: "mysql://user:password@mysql-server/my-database"
|
||||
```
|
||||
|
||||
Supported databases: SQLite (default), MySQL/MariaDB, PostgreSQL.
|
||||
|
||||
## Persistence
|
||||
|
||||
When persistence is enabled, a PVC named `lldap-data` is created. The PVC uses the `truenas-nfs-csi` storage class by default with `ReadWriteMany` access mode.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug mode to troubleshoot issues:
|
||||
|
||||
```yaml
|
||||
lldap:
|
||||
debug:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### Pod not starting
|
||||
|
||||
```bash
|
||||
kubectl get pvc -n apps-roboces -l app.kubernetes.io/name=lldap
|
||||
```
|
||||
|
||||
### Check logs
|
||||
|
||||
```bash
|
||||
kubectl logs lldap-0 -n apps-roboces
|
||||
```
|
||||
|
||||
### Verify configmap
|
||||
|
||||
```bash
|
||||
kubectl get configmap lldap-config -n apps-roboces -o yaml
|
||||
```
|
||||
|
||||
### Default Credentials
|
||||
|
||||
If no secret is provided, the default admin password is `password`. **Change this immediately in production.**
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
{{/* Expand the name of the chart. */}}
|
||||
{{- define "name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Create a default fully qualified app name. */}}
|
||||
{{- define "fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Common labels */}}
|
||||
{{- define "labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{ include "selectorLabels" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Selector labels */}}
|
||||
{{- define "selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Chart name as the PVC name */}}
|
||||
{{- define "pvcName" -}}
|
||||
{{- .Values.lldap.persistence.name | default (printf "%s-data" (include "name" .)) }}
|
||||
{{- end }}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{{- if .Values.lldap.config }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "fullname" . }}-config
|
||||
labels:
|
||||
{{- include "labels" . | nindent 4 }}
|
||||
data:
|
||||
LLDAP_LDAP_BASE_DN: {{ .Values.lldap.config.ldapBaseDn | quote }}
|
||||
LLDAP_HTTP_URL: {{ .Values.lldap.config.httpUrl | quote }}
|
||||
LLDAP_LDAP_OPTIONS__PORT: {{ .Values.lldap.config.ldapPort | quote }}
|
||||
LLDAP_HTTP_OPTIONS__PORT: {{ .Values.lldap.config.httpPort | quote }}
|
||||
LLDAP_VERBOSE: {{ .Values.lldap.config.verbose | quote }}
|
||||
{{- if .Values.lldap.config.databaseUrl }}
|
||||
LLDAP_DATABASE_URL: {{ .Values.lldap.config.databaseUrl | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.ldaps.enabled }}
|
||||
LLDAP_LDAPS_OPTIONS__ENABLED: "true"
|
||||
LLDAP_LDAPS_OPTIONS__CERT_FILE: {{ .Values.lldap.ldaps.certFile | quote }}
|
||||
LLDAP_LDAPS_OPTIONS__KEY_FILE: {{ .Values.lldap.ldaps.keyFile | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.smtp.enabled }}
|
||||
LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET: {{ .Values.lldap.smtp.enablePasswordReset | quote }}
|
||||
LLDAP_SMTP_OPTIONS__SERVER: {{ .Values.lldap.smtp.server | quote }}
|
||||
LLDAP_SMTP_OPTIONS__PORT: {{ .Values.lldap.smtp.port | quote }}
|
||||
LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION: {{ .Values.lldap.smtp.encryption | quote }}
|
||||
LLDAP_SMTP_OPTIONS__USER: {{ .Values.lldap.smtp.user | quote }}
|
||||
LLDAP_SMTP_OPTIONS__FROM: {{ printf "%s <%s>" .Values.lldap.smtp.fromName .Values.lldap.smtp.fromAddress | quote }}
|
||||
{{- if .Values.lldap.smtp.replyTo }}
|
||||
LLDAP_SMTP_OPTIONS__TO: {{ .Values.lldap.smtp.replyTo | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "fullname" . }}
|
||||
labels:
|
||||
{{- include "labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.lldap.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.lldap.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.debug.enabled }}
|
||||
initContainers:
|
||||
- name: debug
|
||||
image: alpine:3.19
|
||||
command: ["sleep", "infinity"]
|
||||
envFrom:
|
||||
{{- if .Values.lldap.config }}
|
||||
- configMapRef:
|
||||
name: {{ include "fullname" . }}-config
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.secret.existingSecretName }}
|
||||
- secretRef:
|
||||
name: {{ .Values.lldap.secret.existingSecretName }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.persistence.enabled }}
|
||||
volumeMounts:
|
||||
- name: {{ include "pvcName" . }}
|
||||
mountPath: /data
|
||||
{{- end }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
capabilities:
|
||||
drop:
|
||||
- all
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.lldap.image.repository }}:{{ .Values.lldap.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.lldap.image.pullPolicy }}
|
||||
{{- with .Values.lldap.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.lldap.service.httpPort }}
|
||||
protocol: TCP
|
||||
- name: ldap
|
||||
containerPort: {{ .Values.lldap.service.ldapPort }}
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
{{- if .Values.lldap.config }}
|
||||
- configMapRef:
|
||||
name: {{ include "fullname" . }}-config
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.secret.existingSecretName }}
|
||||
- secretRef:
|
||||
name: {{ .Values.lldap.secret.existingSecretName }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.persistence.enabled }}
|
||||
volumeMounts:
|
||||
- name: {{ include "pvcName" . }}
|
||||
mountPath: /data
|
||||
{{- end }}
|
||||
{{- with .Values.lldap.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if not .Values.lldap.debug.enabled }}
|
||||
{{- with .Values.lldap.livenessProbe }}
|
||||
livenessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.lldap.readinessProbe }}
|
||||
readinessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.persistence.enabled }}
|
||||
volumes:
|
||||
- name: {{ include "pvcName" . }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "pvcName" . }}
|
||||
{{- else }}
|
||||
volumes:
|
||||
- name: {{ include "pvcName" . }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- with .Values.lldap.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.lldap.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.lldap.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
{{- if .Values.lldap.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "fullname" . }}
|
||||
labels:
|
||||
{{- include "labels" . | nindent 4 }}
|
||||
{{- with .Values.lldap.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.lldap.ingress.className }}
|
||||
ingressClassName: {{ .Values.lldap.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.lldap.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.lldap.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType | default "Prefix" }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.lldap.service.httpPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{{- if .Values.lldap.podDisruptionBudget.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "fullname" . }}
|
||||
labels:
|
||||
{{- include "labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if .Values.lldap.podDisruptionBudget.minAvailable }}
|
||||
minAvailable: {{ .Values.lldap.podDisruptionBudget.minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .Values.lldap.podDisruptionBudget.maxUnavailable }}
|
||||
maxUnavailable: {{ .Values.lldap.podDisruptionBudget.maxUnavailable }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{{- if .Values.lldap.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "pvcName" . }}
|
||||
labels:
|
||||
{{- include "labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.lldap.persistence.accessMode | default "ReadWriteMany" }}
|
||||
{{- if .Values.lldap.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.lldap.persistence.storageClass }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.lldap.persistence.size | default "1Gi" }}
|
||||
{{- end }}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "fullname" . }}
|
||||
labels:
|
||||
{{- include "labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.lldap.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.lldap.service.httpPort }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
- port: {{ .Values.lldap.service.ldapPort }}
|
||||
targetPort: ldap
|
||||
protocol: TCP
|
||||
name: ldap
|
||||
selector:
|
||||
{{- include "selectorLabels" . | nindent 4 }}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
lldap:
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: lldap/lldap
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "stable"
|
||||
|
||||
service:
|
||||
type: LoadBalancer
|
||||
httpPort: 17170
|
||||
ldapPort: 3890
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: "traefik"
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: lldap.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
|
||||
config:
|
||||
ldapBaseDn: "dc=example,dc=com"
|
||||
httpUrl: "https://lldap.example.com"
|
||||
databaseUrl: ""
|
||||
verbose: false
|
||||
ldapPort: 3890
|
||||
httpPort: 17170
|
||||
|
||||
ldaps:
|
||||
enabled: false
|
||||
certFile: ""
|
||||
keyFile: ""
|
||||
|
||||
smtp:
|
||||
enabled: false
|
||||
enablePasswordReset: false
|
||||
server: ""
|
||||
port: 465
|
||||
encryption: "TLS"
|
||||
user: ""
|
||||
fromAddress: ""
|
||||
fromName: ""
|
||||
replyTo: ""
|
||||
|
||||
persistence:
|
||||
enabled: false
|
||||
storageClass: "truenas-nfs-csi"
|
||||
accessMode: ReadWriteMany
|
||||
size: 1Gi
|
||||
|
||||
resources: {}
|
||||
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
timeoutSeconds: 15
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
periodSeconds: 10
|
||||
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 15
|
||||
timeoutSeconds: 2
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
periodSeconds: 10
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
secret:
|
||||
existingSecretName: ""
|
||||
|
||||
podSecurityContext: {}
|
||||
securityContext: {}
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
minAvailable: 1
|
||||
|
||||
debug:
|
||||
enabled: false
|
||||
|
|
@ -2,7 +2,7 @@ apiVersion: v2
|
|||
name: vaultwarden
|
||||
description: A Helm chart for Vaultwarden password manager
|
||||
type: application
|
||||
version: 1.0.3
|
||||
version: 1.0.2
|
||||
appVersion: 1.37.1
|
||||
annotations:
|
||||
artifacthub.io/images: "- name: vaultwarden\n image: vaultwarden/server:1.37.1-alpine\n"
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ vaultwarden:
|
|||
securityContext: {}
|
||||
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 8008
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
ingress:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ server:
|
|||
redis:
|
||||
image:
|
||||
repository: docker.io/library/redis
|
||||
tag: 8.10.1-alpine
|
||||
tag: 8.10.0-alpine
|
||||
|
||||
repoServer:
|
||||
livenessProbe:
|
||||
|
|
|
|||
|
|
@ -25,8 +25,4 @@ spec:
|
|||
- ghcr.io/antoniolago/charts
|
||||
- https://repo.helmforge.dev
|
||||
- https://harish2k01.github.io/helm-charts/
|
||||
- https://helm.runix.net
|
||||
- git.roboces.dev/catalin/fukuops
|
||||
- https://kubereboot.github.io/charts
|
||||
- https://charts.goauthentik.io/
|
||||
- https://git.roboces.dev/catalin/fukuops
|
||||
|
|
|
|||
|
|
@ -25,4 +25,3 @@ spec:
|
|||
- https://git.roboces.dev/catalin/fukuops.git
|
||||
- https://democratic-csi.github.io/charts/
|
||||
- https://bitnami-labs.github.io/sealed-secrets
|
||||
- https://kubernetes-sigs.github.io/descheduler
|
||||
|
|
|
|||
|
|
@ -16,7 +16,4 @@ spec:
|
|||
sourceRepos:
|
||||
- code.forgejo.org/forgejo-helm
|
||||
- git.roboces.dev/catalin/fukuops
|
||||
- https://git.roboces.dev/catalin/fukuops.git
|
||||
- https://git.roboces.dev/catalin/huesoporro.git
|
||||
- https://charts.christianhuth.de
|
||||
- https://charts.authelia.com
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: authelia-data
|
||||
namespace: apps-roboces
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: truenas-nfs-csi
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
kind: SealedSecret
|
||||
apiVersion: bitnami.com/v1alpha1
|
||||
metadata:
|
||||
name: secrets-authelia
|
||||
namespace: apps-roboces
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
name: secrets-authelia
|
||||
namespace: apps-roboces
|
||||
type: Opaque
|
||||
encryptedData:
|
||||
authentication.ldap.password.txt: AgBwsqdgEOQ8r/yUOD82Cwr44jxqV9RPDTzMYju8IfWx7b4iDKtZVidLK4RxBCp1+MPyWmLs7H4jec3Xsr10da/V+LaS1Elib+Sp2CkFZPGj1/XAcu5w3k/BnMcsRWJEJtzU251NVon6IVEzszcGOaVsPqf6aIvnKgOJEmp1boMlnemkvtaeXVS0TuSLXCQEMDfGMh9X8D9je315Sdqpczo8YLz6jyj6RxlgXeygRXkXANd2KFOyfLNUAnzm4Tl6PMjULnXvsxLhlc35+am6IsZGVLcdRMPY12UpHeZyVIgJBZSdo/gLTJS/hLhL2sPJv8HksL6YfvDnIWqu3PxVwI3BGtW2znBQaEIFXtOIXL9EDG0NMAZSBYCRpjrPNHP7xj7ruqjg4eU1iUj6yJpIv9322L/qhFWViK9wXc3+0L2n2q4qVBgrZxcacgqtHrCYj5Uo9iluVPWwagcKFYRXBupqaUUTso7LtSsv4WbOrJzYzE9mZk+feH1ZNvBp13JfWNdRiso5rpl0tZFJ7oW4G/CJuWg2MmL3UNViYB/ntwFWZWT8dxCEq2LI3UxeawHntzvk82kFyZduLUwhLvY4WfQB4yb//gBOpc0OyHa+djwECIyy3Jh+70LG+1YeVc8yQm52eu9+mzRSwDlqspKZYfwbMPCCiQj6AiSfg9Ho5+ITq1PgEt3oo9b0JkYFmEAgT88Y6z0DRQrM9YOXCfazMg==
|
||||
identity_providers.oidc.hmac.key: AgCMb1NgRFW7jB8xAXDzTDK336k1w63FNJGJPj9fUrkM/5QT6WuAAPYPeD/3PWG/+0XjUycU4Q2PK/ei8rK6BJ1jwdSRQqBe/ybdxl3WCWGmjB2/jKLIAFQonJdgIzZiY4rUHUepQqhva/wA0eM1nCV/KYuOCIIzdNAGj3E+zyP9yquZcDxknadLuvY6XbTTCdsdwYgFP9qYy8+Vg/K8c2GGubuU34uLkfCG3afow9/gvtSDU5lsZwGtZPcYibOVULQ/CEiGiXBXs7g5W1HjqcJdPaJZDfkG2hPWh4aWMxMkjORt/9NrNmBi70e4LW5cq32wX0Mz+UblDf66Qz3wnGmZ93owAw1N0MtlI/V0W2XvSnxcXvmloHGfbll2rhFiUFPFmWq71Uv52Qc8rAMOeuJgRnrlG3QHxLFkTonMYcasQrfm8MjHpaNkemsdA6XO9VvrRVxLyN31lV+2bbeqh2wHZgCRIbU4gUyApi69Rd1TyEYVW9QSxWbPkX5B5eaCqDCZLX9aQocfElrKk/zzvY2J53s07AkvsowgwbWhwpzwAYB2ajBndAcNhzS311ve1kzZlp498ePGCdpcyuTJfVWTKoXdmJu3xSg0P7Czts/zstQYGSpp+0I4gwzYTtSuf1M841Obc78l2PaUqN5P3YIHdq4ZiMTWPHhqgerobyf6XxtXcO5iEf7zSGYexRwaTroMp/Sqx9P0eTX1rufwkmvMlPH6mClr3RMVGno7uFuC1SkRM3Ox0a4wJ0Y2B5IX/7C+Pd68Z1VQX+wJeson0X2K
|
||||
identity_providers.oidc.jwks.key.pem: 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
|
||||
identity_validation.reset_password.jwt.hmac.key: AgAQRknTOoC5tbCitLZkRtcIs3VOaubEvlhRmGtHh/OS0v6lgU2Gcl5C6jisu/+MqQba4euzMACLVgSMiEkERY2GrMijecfVf5LOkKKwQ0Xjiw4a+MlSVs5KR1Ovlz414//3GfUCttunnDRnH6xQbsQjWLCI6lv6Umb40TQHOnRmb83wmSkBa/P9WR35EQ6IcES3q7Ad4ZpNtLaavEnB8NuLZg3yaJpN+1yuZPeMgTPUtTG/e9FLdBs3S8AYFIcrUnDLViUo9DfXumMs1DlTsmABpjyIgjIeywOmCxMYZYvQCJrRGYlKZRdoCoafhAPxt67hdUYOe56cq3kqlTNTdURMbYup7cIkW2hQ8ZiyPvYk++RIhV/cPWOSjh6CLxMq26Si1c2Lg3aAVFxQcDiXbdprjHzbKzFfKO/5LVnhT7O651BqkCguPC2vVoaeuxblHjqouBD7ZmPazIQ9J/uDUmvm5UPjEN+Ln+enrLXdoJN9HMor410/PJ+9xPAHXhdrXRS6j5TlN5q1pz0gj2Qmnv3cunsyLo+ZJ97F3EkSYPVoK3sMXGn2fdKy/lc9vPMMvtH/tLdhWefxd25APahStZHv4Mb7O6Z+A4uFiP9Qao64jeLj08LWBbJPefT7Y+KthugN6cW4y/xQN4UFvpPHhGB36yb+45eIBzOJHltOqZ+wldjUHeBgMdkODAQYYbCzhDuVVm2iMGQqUi/Vk+EayaV70prcAELgRskseME2MwfrhYMAKS91tfMbL2dGTQ==
|
||||
oidc.audiobookshelf.client_secret: AgB5usnmgdDGm4MjtMTZDxhGb6qCxoMqx/2f3tZ7JYDgoVEspeL2tFBxXBOUDamCxpnuSp12O1ZcUpOw+9XAj4079vG7sWHAzjX0nIIJCNIjH3Y6+rhGh7J7nnWc9UJqzeWM6T4k9Dntyhsn96Xp5RDSUqhH68/u4OeqwzRJJofHlbqLmYq6+bq4MZfv/wZIsqfA02aWFJk7ST2qqD1Sc+oz0HkP8jXk9WwCCnRCuZoZgYUTHbPZAgOLMMRTlqTF4jHjFQ5B+IQTh2z53Agq6G8QLmU49J7RtPIunG+2Gf71KYXIeKktH8QwI9nBof1PXGYMmOvCCzqoBLsIBqYg/iAQ+kjlkbJov/3SBk+uwdj/wvG99+AJPVB2VHr8rinW0WXptzUATqYrHg0ufBxX4lKvVhqdkY7AaHPoI5ZwKi7AYftuo0IxDHGYC4GXVg9mWY7T6nNcvqB6s1epBNDjQnsCk+krxfZ83OecHUA4apwrSX4/Ytc+GkuQ/eJcdr9SoMXW7yzFfOLMh4+8syH7xigLwszw0t6lxJ37DnVbuYewhvazIU2TBWyTtQYnBeqVm2Wllh6xZiP5PyJ+VEi2fwGZVW6NX0Hh0gVmYNduj8xAJS4GRLwE2cOrN99kTMIXBdqmjXdYuAQKYDMlWU8QtTqtsoa4Y3UV/s7f7DdVtW+nIwxHFStNSQE4kOqPtUpgKbJPpPTjlPmIhW5Zm1gj3eOWIRLRV6UAkiJ2t6T2BEh+uqhcf26pbSjbfnqSEPVhfAzarnFV32Dkgue0I6rka/GPlf4fSK89JuXT9Igwdm0cKwOepYynZIf0NSSB1bWcgTTuL3N4CjteWc/XzacFkXvWuNJ3/WgN/brGZb2nRmmHEA==
|
||||
session.encryption.key: AgCjw3K7solE1INH+VIwEeP2U70HCaY25Iu6sq5C7j8nuW8JzbSCBPeY+qTS76l4sIyLlLekk7ke5kCPpZo+Jsz62UQJMGaSASRy0FKQIxgn5UIkObB+qziGjC8WNj6y/JmbvPNiIp+aw0Nd3OmVLRkvIjJDkyMf/iW1oGeLctwRop9Y+wi7lDPyEHH+CaNPCLYidhAJFG785qibqY8jTxtHsEts2T+hSXz75v5abYS+lTFtFV7RDWudkWG6dPjUKwYGw7l/yQm5p2qzzvhoyCrQItdNuEvGrZQsnXZlq4ik0C7t977uiU+fumYKFQvHzVuQTMjjo9G5AlhUG/4w2olMqOMZoJ0u+hHFBX1K1uYCUr1hd2343jd+y86iObZzTazlTGdP142lXk9+CT+akpQIVX0Mk5mwtemCYG+64ebSQm+SVB9Ffgxx3Ips6BOhqwIO0fLB1iz4BFVUp+87juyHZOP/5uC+KeV3+DHaQDrHRo6DxF5D5CRXBCcHXeWBLkDXSENgD4kvRee7fEQf60ajvi9VFTPjk86KXCbOZVlZPPrqWG7Dh+kaYOjWCQXQvFzYAq6/Dr5ZDRPJ/X989zHjPFFGRQ7Ol9uyjT/7I/2IBfdQB1Ep5BWty50PBVIqbYKRzQN0g0x5BAj5/2J9mOX/0BNGExVCNn53rfc8yDurfP0q1nbYk1t+fv0T9oI7RTPYIVWtUnshG8fwYK1PARNYBbKrGwjWtTdVxzV+ocunmmOHQhvOFrTnsfupGQ==
|
||||
smtp_password.txt: AgB8pRbQxScSBRty04Z5oen7F52Y7PnwF9gJ6fdbUTRobE7j2fWiHMQk4l1FTztW3Muz3B/MQZBgBEzulpkDJ1TrvTe5I7JFdANr3lmlKjy0CG2NcQ7Vma7Bqu2SVnL8wxi/3IBTGxVa6bplGrkUCsIr/qWLhkVknOpAaUvOUuECT8PVxUZq/s4UMddkEf4N3IQW7j/60xwzomMp4e5eZ8YTH0/ZGcuw9JOQSOoJDzvJ5B+wZF7q7j9LQc4gBPHQiFFMVlZrhHA0fdjfPxbb7QEiGdMBxeZ0JqaaFBnr4FG7hRY5/H9RS9iZCFH9QY6X86ul9ZgiJYVyrvZFKSBsW+THlLZ7MACLvOs/zdXhpc2SthccoK8ykGC/inWTnfodQpH7N4zJq7akXxFy9/em4ssjWul4coeLNc+xhyi5VauLQQfRpgijc1kVJeQbrUrMe+aE1tlRdgRx76OKiFym1t47+zN3eqRf+NegNBGPSdAtKEBKfNPrcFQmlZVR6fRZqiBxG0wE90nfq9fuw3RDnVU/0LsuxwTz99gWMWl8ySuo+EAhf+JmB26/Pi1hnByeNeD8FsVkUiC6XnwAVC+ytnaVEfatrKmSJyzRhTSuFUoFxD5YV0aQgILks4+CpQwn300s0bqkLOP0etigZCCPq4cR+t+FFYAkuVjr+xPsoww0UOgokHsZKoXYi0n96UjbT0w+zoHhqQTBrPPWhPjDogZBKBCnth5eUtgrhSA2
|
||||
storage.encryption.key: AgBt8lTRtRqn+PiTNjgmJ97/rEqKEE70KZLuVtyvBM2ek70E555vx16BsCN3ATvwcr5dw+nAwYKpHJzS+mgTloLWmz6L2HqtxhlnaKC7elFeOza7tPKmKeKV3zXQ8YEA9eWjKdUN2TzO5s10iPCp4esUy5asO5DS/QVyPHMUfufu3iwHCDgnDEGlQ0pO1LMT2FwJ2QYX4Cub+OjUVrkBm8VbNdFqIw3OBtahRX2gx+o1d55psfi08ylEMKUwkHXYtkBheDvYL7SRv8xHKHGrN1loWHZOby1GOTzOnOpL6mjJoU+4nlvEzVwxzuPOHDq24cfey7engu0IfrZ+9Fs+icFjInY7cpowlNl0CXIWQm++ljtKVOUGUB2Bp6IwY1iwu3NVlwYVoZRCfSw5rLhccXnODYjXGMAwHF1cau4K8mkx1mD48dJnQE+y6AfrOPH+tCJ0dSDyuebNNlcE9HDmJseIGDP37Z3E/S9CCGprvLCdUYeOhXERyTevm5VJoboYq1/mhonPvY0bU4w3zVV3mXgr3Xi4SOupneUL+XfuQASRz1eGoQpAGxI8h4qWkgA66f8uPTh0hdM1JfLus9oXCtFsiYJzd+VtIZE+0JtI4ztGtm2JtgH80t2eiJTQolQihfpAXazg9fQSPH6NHsFcRplqC7emxdBZnI2F+TSjqcoX+RA78z+J2OP4ISs4ngF8USmm9/rJIRUq26XNLMjktSREBh/IwrpZWgiZqU/QzMhNriClH3n4vDHWJ03Xiw==
|
||||
370
k8s/services/system-upgrade-controller/k3s-upgrade.yaml
Normal file
370
k8s/services/system-upgrade-controller/k3s-upgrade.yaml
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: system-upgrade
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: system-upgrade
|
||||
namespace: system-upgrade
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: default-controller-env
|
||||
namespace: system-upgrade
|
||||
data:
|
||||
SYSTEM_UPGRADE_CONTROLLER_DEBUG: "false"
|
||||
SYSTEM_UPGRADE_CONTROLLER_THREADS: "2"
|
||||
SYSTEM_UPGRADE_CONTROLLER_LEADER_ELECT: "true"
|
||||
SYSTEM_UPGRADE_JOB_ACTIVE_DEADLINE_SECONDS: "900"
|
||||
SYSTEM_UPGRADE_JOB_BACKOFF_LIMIT: "99"
|
||||
SYSTEM_UPGRADE_JOB_IMAGE_PULL_POLICY: Always
|
||||
SYSTEM_UPGRADE_JOB_KUBECTL_IMAGE: rancher/kubectl:v1.30.3
|
||||
SYSTEM_UPGRADE_JOB_PRIVILEGED: "true"
|
||||
SYSTEM_UPGRADE_JOB_TTL_SECONDS_AFTER_FINISH: "900"
|
||||
SYSTEM_UPGRADE_PLAN_POLLING_INTERVAL: 15m
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: system-upgrade-controller
|
||||
rules:
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apiextensions.k8s.io
|
||||
resources:
|
||||
- customresourcedefinitions
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- get
|
||||
- create
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
resourceNames:
|
||||
- system-upgrade-controller
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
- apiGroups:
|
||||
- upgrade.cattle.io
|
||||
resources:
|
||||
- plans
|
||||
- plans/status
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- patch
|
||||
- update
|
||||
- delete
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: system-upgrade-controller-drainer
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods/eviction
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets
|
||||
- daemonsets
|
||||
- replicasets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: system-upgrade
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system-upgrade-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: system-upgrade
|
||||
namespace: system-upgrade
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: system-upgrade-drainer
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: system-upgrade-controller-drainer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: system-upgrade
|
||||
namespace: system-upgrade
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: system-upgrade-controller
|
||||
namespace: system-upgrade
|
||||
rules:
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: system-upgrade
|
||||
namespace: system-upgrade
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: system-upgrade-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: system-upgrade
|
||||
namespace: system-upgrade
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: system-upgrade-controller
|
||||
namespace: system-upgrade
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
upgrade.cattle.io/controller: system-upgrade-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/component: controller
|
||||
app.kubernetes.io/name: system-upgrade-controller
|
||||
upgrade.cattle.io/controller: system-upgrade-controller
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
- key: kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- system-upgrade-controller
|
||||
serviceAccountName: system-upgrade
|
||||
tolerations:
|
||||
- key: CriticalAddonsOnly
|
||||
operator: Exists
|
||||
- key: node-role.kubernetes.io/master
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
- key: node-role.kubernetes.io/controlplane
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
- key: node-role.kubernetes.io/etcd
|
||||
operator: Exists
|
||||
effect: NoExecute
|
||||
containers:
|
||||
- name: system-upgrade-controller
|
||||
image: rancher/system-upgrade-controller:v0.14.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
runAsGroup: 65534
|
||||
allowPrivilegeEscalation: false
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: default-controller-env
|
||||
env:
|
||||
- name: SYSTEM_UPGRADE_CONTROLLER_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels['upgrade.cattle.io/controller']
|
||||
- name: SYSTEM_UPGRADE_CONTROLLER_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: SYSTEM_UPGRADE_CONTROLLER_NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
volumeMounts:
|
||||
- name: etc-ssl
|
||||
mountPath: /etc/ssl
|
||||
readOnly: true
|
||||
- name: etc-pki
|
||||
mountPath: /etc/pki
|
||||
readOnly: true
|
||||
- name: etc-ca-certificates
|
||||
mountPath: /etc/ca-certificates
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: etc-ssl
|
||||
hostPath:
|
||||
path: /etc/ssl
|
||||
type: DirectoryOrCreate
|
||||
- name: etc-pki
|
||||
hostPath:
|
||||
path: /etc/pki
|
||||
type: DirectoryOrCreate
|
||||
- name: etc-ca-certificates
|
||||
hostPath:
|
||||
path: /etc/ca-certificates
|
||||
type: DirectoryOrCreate
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: upgrade.cattle.io/v1
|
||||
kind: Plan
|
||||
metadata:
|
||||
name: k3s-server
|
||||
namespace: system-upgrade
|
||||
labels:
|
||||
k3s-upgrade: server
|
||||
spec:
|
||||
concurrency: 1
|
||||
version: v1.36.3+k3s1
|
||||
nodeSelector:
|
||||
matchExpressions:
|
||||
- {key: k3s-upgrade, operator: Exists}
|
||||
- {key: k3s-upgrade, operator: NotIn, values: ["disabled", "false"]}
|
||||
- {key: k3os.io/mode, operator: DoesNotExist}
|
||||
- {key: node-role.kubernetes.io/control-plane, operator: Exists}
|
||||
serviceAccountName: system-upgrade
|
||||
cordon: true
|
||||
upgrade:
|
||||
image: rancher/k3s-upgrade
|
||||
---
|
||||
apiVersion: upgrade.cattle.io/v1
|
||||
kind: Plan
|
||||
metadata:
|
||||
name: k3s-agent
|
||||
namespace: system-upgrade
|
||||
labels:
|
||||
k3s-upgrade: agent
|
||||
spec:
|
||||
concurrency: 2
|
||||
version: v1.36.3+k3s1
|
||||
nodeSelector:
|
||||
matchExpressions:
|
||||
- {key: k3s-upgrade, operator: Exists}
|
||||
- {key: k3s-upgrade, operator: NotIn, values: ["disabled", "false"]}
|
||||
- {key: k3os.io/mode, operator: DoesNotExist}
|
||||
- {key: node-role.kubernetes.io/control-plane, operator: DoesNotExist}
|
||||
serviceAccountName: system-upgrade
|
||||
prepare:
|
||||
image: rancher/k3s-upgrade
|
||||
args: ["prepare", "k3s-server"]
|
||||
drain:
|
||||
force: true
|
||||
skipWaitForDeleteTimeout: 60
|
||||
upgrade:
|
||||
image: rancher/k3s-upgrade
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["typer", "ruamel.yaml"]
|
||||
# ///
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from difflib import unified_diff
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Optional
|
||||
|
||||
import typer
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
app = typer.Typer(help="Add or update a secret field in an existing SealedSecret")
|
||||
|
||||
|
||||
def resolve_file(file: Path) -> Path:
|
||||
if file.is_dir():
|
||||
candidate = file / "sealedsecrets.yaml"
|
||||
if not candidate.is_file():
|
||||
raise typer.BadParameter(f"{file} is a directory and contains no sealedsecrets.yaml")
|
||||
return candidate
|
||||
if not file.is_file():
|
||||
raise typer.BadParameter(f"{file} does not exist")
|
||||
return file
|
||||
|
||||
|
||||
def looks_like_json(text: str) -> bool:
|
||||
for char in text:
|
||||
if char in (" ", "\t", "\n", "\r"):
|
||||
continue
|
||||
return char == "{"
|
||||
return False
|
||||
|
||||
|
||||
def extract_header_comments(text: str) -> tuple[str, str]:
|
||||
"""Split leading comments/blank lines from the rest of the file."""
|
||||
lines = text.splitlines(keepends=True)
|
||||
idx = 0
|
||||
while idx < len(lines):
|
||||
stripped = lines[idx].strip()
|
||||
if stripped == "" or stripped.startswith("#"):
|
||||
idx += 1
|
||||
else:
|
||||
break
|
||||
return "".join(lines[:idx]), "".join(lines[idx:])
|
||||
|
||||
|
||||
def load_documents(text: str) -> list[Any]:
|
||||
if looks_like_json(text):
|
||||
# JSON is valid YAML, but parse it explicitly so we can normalize it to YAML output.
|
||||
return [json.loads(text)]
|
||||
yaml = YAML(typ="rt")
|
||||
yaml.preserve_quotes = True
|
||||
return list(yaml.load_all(text))
|
||||
|
||||
|
||||
def _represent_none(representer, data):
|
||||
return representer.represent_scalar("tag:yaml.org,2002:null", "null")
|
||||
|
||||
|
||||
def dump_documents(docs: list[Any]) -> str:
|
||||
yaml = YAML(typ="rt")
|
||||
yaml.default_flow_style = False
|
||||
yaml.preserve_quotes = True
|
||||
yaml.width = 4096
|
||||
yaml.explicit_start = True
|
||||
yaml.representer.add_representer(type(None), _represent_none)
|
||||
from ruamel.yaml.compat import StringIO
|
||||
|
||||
stream = StringIO()
|
||||
yaml.dump_all(docs, stream)
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def detect_scope(doc: Any) -> str:
|
||||
annotations = doc.get("metadata", {}).get("annotations", {}) or {}
|
||||
for key, value in annotations.items():
|
||||
if key == "sealedsecrets.bitnami.com/cluster-wide" and str(value).lower() == "true":
|
||||
return "cluster-wide"
|
||||
if key == "sealedsecrets.bitnami.com/namespace-wide" and str(value).lower() == "true":
|
||||
return "namespace-wide"
|
||||
return "strict"
|
||||
|
||||
|
||||
def seal_value(value: str, doc: Any, cert: Optional[Path]) -> str:
|
||||
metadata = doc.get("metadata", {})
|
||||
spec = doc.get("spec", {})
|
||||
|
||||
name = metadata.get("name")
|
||||
if not name:
|
||||
raise typer.BadParameter("SealedSecret has no metadata.name")
|
||||
|
||||
namespace = metadata.get("namespace") or spec.get("template", {}).get("metadata", {}).get("namespace")
|
||||
if not namespace:
|
||||
raise typer.BadParameter("SealedSecret has no namespace in metadata or spec.template.metadata")
|
||||
|
||||
cmd = ["kubeseal", "--raw", "--from-file=/dev/stdin"]
|
||||
if cert:
|
||||
cmd.extend(["--cert", str(cert)])
|
||||
|
||||
scope = detect_scope(doc)
|
||||
if scope == "cluster-wide":
|
||||
cmd.extend(["--scope", "cluster-wide"])
|
||||
elif scope == "namespace-wide":
|
||||
cmd.extend(["--namespace", namespace, "--scope", "namespace-wide"])
|
||||
else:
|
||||
cmd.extend(["--namespace", namespace, "--name", name])
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
input=value,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
typer.echo(f"kubeseal failed:\n{result.stderr}", err=True)
|
||||
raise typer.Exit(result.returncode)
|
||||
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def find_target_document(docs: list[Any], key: str, secret_name: Optional[str]) -> Any:
|
||||
if secret_name:
|
||||
for doc in docs:
|
||||
if doc.get("metadata", {}).get("name") == secret_name:
|
||||
return doc
|
||||
raise typer.BadParameter(f"No SealedSecret named '{secret_name}' found in file")
|
||||
|
||||
if len(docs) == 1:
|
||||
return docs[0]
|
||||
|
||||
candidates = [
|
||||
doc for doc in docs
|
||||
if key in (doc.get("spec", {}).get("encryptedData", {}) or {})
|
||||
]
|
||||
if len(candidates) == 1:
|
||||
return candidates[0]
|
||||
if len(candidates) > 1:
|
||||
names = [doc.get("metadata", {}).get("name", "<unnamed>") for doc in candidates]
|
||||
raise typer.BadParameter(
|
||||
f"Key '{key}' exists in multiple SealedSecrets ({', '.join(names)}). "
|
||||
"Use --secret-name to choose one."
|
||||
)
|
||||
|
||||
names = [doc.get("metadata", {}).get("name", "<unnamed>") for doc in docs]
|
||||
raise typer.BadParameter(
|
||||
f"File contains multiple SealedSecrets and key '{key}' does not exist yet. "
|
||||
f"Available: {', '.join(names)}. Use --secret-name to choose one."
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def add(
|
||||
name: Annotated[str, typer.Option("--name", help="Key name in SealedSecret encryptedData")],
|
||||
secret: Annotated[str, typer.Option("--secret", help="Plaintext secret value")],
|
||||
file: Annotated[Path, typer.Option("--file", help="Path to sealedsecrets.yaml or its directory")],
|
||||
secret_name: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--secret-name", help="Name of the SealedSecret resource to update (required for multi-secret files when the key is new)"),
|
||||
] = None,
|
||||
cert: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--cert", help="Path to a kubeseal certificate for offline sealing"),
|
||||
] = None,
|
||||
dry_run: Annotated[bool, typer.Option("--dry-run", help="Show diff without writing")] = False,
|
||||
) -> None:
|
||||
target_path = resolve_file(file)
|
||||
original_text = target_path.read_text()
|
||||
header, body = extract_header_comments(original_text)
|
||||
|
||||
docs = load_documents(body)
|
||||
if not docs:
|
||||
raise typer.BadParameter(f"{target_path} contains no documents")
|
||||
|
||||
target = find_target_document(docs, name, secret_name)
|
||||
sealed = seal_value(secret, target, cert)
|
||||
|
||||
if "spec" not in target or target["spec"] is None:
|
||||
target["spec"] = {}
|
||||
if "encryptedData" not in target["spec"] or target["spec"]["encryptedData"] is None:
|
||||
target["spec"]["encryptedData"] = {}
|
||||
|
||||
old_value = target["spec"]["encryptedData"].get(name)
|
||||
action = "updated" if old_value is not None else "added"
|
||||
target["spec"]["encryptedData"][name] = sealed
|
||||
|
||||
new_text = header + dump_documents(docs)
|
||||
|
||||
if dry_run:
|
||||
diff = unified_diff(
|
||||
original_text.splitlines(keepends=True),
|
||||
new_text.splitlines(keepends=True),
|
||||
fromfile=str(target_path),
|
||||
tofile=str(target_path),
|
||||
)
|
||||
sys.stdout.writelines(diff)
|
||||
raise typer.Exit()
|
||||
|
||||
backup_path = target_path.with_suffix(target_path.suffix + ".bak")
|
||||
shutil.copy2(target_path, backup_path)
|
||||
target_path.write_text(new_text)
|
||||
|
||||
typer.echo(f"{action} '{name}' in SealedSecret '{target.get('metadata', {}).get('name')}' ({target_path})")
|
||||
typer.echo(f"backup saved to {backup_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
|
@ -92,19 +92,6 @@ fi
|
|||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
K8S_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
extract_lb_port() {
|
||||
local file="$1"
|
||||
local field="$2"
|
||||
local selector="$3"
|
||||
local value
|
||||
|
||||
value=$(yq -r '.. | objects | select(.type == "LoadBalancer") | '"$selector"' // empty' "$file" 2>/dev/null || true)
|
||||
if [[ -n "$value" ]] && [[ "$value" != "null" ]] && [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||
PORTS+=("$value")
|
||||
[[ $VERBOSE -eq 1 ]] && echo "[DEBUG] Found $field $value in $file"
|
||||
fi
|
||||
}
|
||||
|
||||
collect_ports() {
|
||||
PORTS=()
|
||||
|
||||
|
|
@ -117,15 +104,29 @@ collect_ports() {
|
|||
continue
|
||||
fi
|
||||
|
||||
extract_lb_port "$file" ".port" ".port"
|
||||
extract_lb_port "$file" "servicePortHttp" ".servicePortHttp"
|
||||
extract_lb_port "$file" "servicePortHttps" ".servicePortHttps"
|
||||
extract_lb_port "$file" "dns.port" ".dns.port"
|
||||
extract_lb_port "$file" "httpPort" ".httpPort"
|
||||
extract_lb_port "$file" "httpsPort" ".httpsPort"
|
||||
extract_lb_port "$file" "edgePort" ".edgePort"
|
||||
extract_lb_port "$file" "ldapPort" ".ldapPort"
|
||||
extract_lb_port "$file" "servicePort" ".servicePort"
|
||||
lb_port=$(yq -r '.. | objects | select(.type == "LoadBalancer") | select(.port != null) | .port // empty' "$file" 2>/dev/null || true)
|
||||
if [[ -n "$lb_port" ]] && [[ "$lb_port" != "null" ]] && [[ "$lb_port" =~ ^[0-9]+$ ]]; then
|
||||
PORTS+=("$lb_port")
|
||||
[[ $VERBOSE -eq 1 ]] && echo "[DEBUG] Found .port $lb_port in $file"
|
||||
fi
|
||||
|
||||
service_port_http=$(yq -r '.. | objects | select(.type == "LoadBalancer") | .servicePortHttp // empty' "$file" 2>/dev/null || true)
|
||||
if [[ -n "$service_port_http" ]] && [[ "$service_port_http" != "null" ]] && [[ "$service_port_http" =~ ^[0-9]+$ ]]; then
|
||||
PORTS+=("$service_port_http")
|
||||
[[ $VERBOSE -eq 1 ]] && echo "[DEBUG] Found servicePortHttp $service_port_http in $file"
|
||||
fi
|
||||
|
||||
service_port_https=$(yq -r '.. | objects | select(.type == "LoadBalancer") | .servicePortHttps // empty' "$file" 2>/dev/null || true)
|
||||
if [[ -n "$service_port_https" ]] && [[ "$service_port_https" != "null" ]] && [[ "$service_port_https" =~ ^[0-9]+$ ]]; then
|
||||
PORTS+=("$service_port_https")
|
||||
[[ $VERBOSE -eq 1 ]] && echo "[DEBUG] Found servicePortHttps $service_port_https in $file"
|
||||
fi
|
||||
|
||||
service_dns_port=$(yq -r '.. | objects | select(.type == "LoadBalancer") | .dns.port // empty' "$file" 2>/dev/null || true)
|
||||
if [[ -n "$service_dns_port" ]] && [[ "$service_dns_port" != "null" ]] && [[ "$service_dns_port" =~ ^[0-9]+$ ]]; then
|
||||
PORTS+=("$service_dns_port")
|
||||
[[ $VERBOSE -eq 1 ]] && echo "[DEBUG] Found dns.port $service_dns_port in $file"
|
||||
fi
|
||||
done < <(find "$K8S_DIR/k8s" -name "*.yaml" -type f 2>/dev/null) || true
|
||||
|
||||
if command -v kubectl &>/dev/null && kubectl cluster-info &>/dev/null 2>&1; then
|
||||
|
|
|
|||
293
scripts/users.py
293
scripts/users.py
|
|
@ -1,293 +0,0 @@
|
|||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["ldap3", "typer", "httpx"]
|
||||
# ///
|
||||
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from ldap3 import ALL, Connection, Server, SUBTREE
|
||||
|
||||
LLDAP_NAMESPACE = "apps-fuku"
|
||||
LLDAP_SECRET = "lldap-secrets"
|
||||
LLDAP_SERVICE = "lldap.apps-fuku.svc.cluster.local"
|
||||
LLDAP_LDAP_PORT = 3890
|
||||
LLDAP_BASE_DN = "dc=fuku,dc=local"
|
||||
LLDAP_ADMIN_USER = "uid=admin,ou=people,dc=fuku,dc=local"
|
||||
LLDAP_URL = "https://ldap.fukurokuju.dev"
|
||||
|
||||
app = typer.Typer(help="Manage Authelia users in the LLDAP backend.")
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
print(f"ERROR: {msg}", file=sys.stderr)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def run_kubectl(args: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["kubectl", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def get_secret_value(key: str) -> str:
|
||||
result = run_kubectl(
|
||||
[
|
||||
"get",
|
||||
"secret",
|
||||
LLDAP_SECRET,
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
f"jsonpath={{.data.{key}}}",
|
||||
]
|
||||
)
|
||||
return base64.b64decode(result.stdout).decode()
|
||||
|
||||
|
||||
def get_configmap_value(key: str) -> str:
|
||||
result = run_kubectl(
|
||||
[
|
||||
"get",
|
||||
"configmap",
|
||||
"lldap-config",
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
f"jsonpath={{.data.{key}}}",
|
||||
]
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_lldap_password() -> str:
|
||||
return get_secret_value("LLDAP_LDAP_USER_PASS")
|
||||
|
||||
|
||||
def get_lldap_hosts() -> list[str]:
|
||||
result = run_kubectl(
|
||||
[
|
||||
"get",
|
||||
"svc",
|
||||
"lldap",
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
"jsonpath={.status.loadBalancer.ingress[*].ip}",
|
||||
]
|
||||
)
|
||||
ips = [ip.strip() for ip in result.stdout.split() if ip.strip()]
|
||||
if not ips:
|
||||
fail("could not discover lldap LoadBalancer IPs")
|
||||
return ips
|
||||
|
||||
|
||||
def lldap_connection() -> Connection:
|
||||
password = get_lldap_password()
|
||||
hosts = get_lldap_hosts()
|
||||
last_error = None
|
||||
for host in hosts:
|
||||
try:
|
||||
server = Server(host, port=LLDAP_LDAP_PORT, use_ssl=False, get_info=ALL)
|
||||
conn = Connection(
|
||||
server,
|
||||
user=LLDAP_ADMIN_USER,
|
||||
password=password,
|
||||
auto_bind=True,
|
||||
read_only=False,
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
fail(f"could not connect to any lldap endpoint: {last_error}")
|
||||
|
||||
|
||||
def validate_username(username: str) -> None:
|
||||
if not re.match(r"^[a-zA-Z0-9_.-]+$", username):
|
||||
fail(f"invalid username: {username} (allowed: a-z, 0-9, _, ., -)")
|
||||
|
||||
|
||||
def trigger_password_reset(username: str) -> None:
|
||||
url = f"{LLDAP_URL}/auth/reset/step1/{username}"
|
||||
try:
|
||||
response = httpx.post(url, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as e:
|
||||
fail(f"failed to trigger password reset email: {e}")
|
||||
|
||||
print(f"Password reset email triggered for '{username}'.")
|
||||
|
||||
|
||||
def generate_random_password() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def get_group_id(conn: Connection, group_name: str) -> int | None:
|
||||
conn.search(
|
||||
f"ou=groups,{LLDAP_BASE_DN}",
|
||||
f"(cn={group_name})",
|
||||
SUBTREE,
|
||||
attributes=["uid"],
|
||||
)
|
||||
if not conn.entries:
|
||||
return None
|
||||
uid = conn.entries[0].uid.value
|
||||
try:
|
||||
return int(uid)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def user_exists(conn: Connection, username: str) -> bool:
|
||||
conn.search(
|
||||
f"ou=people,{LLDAP_BASE_DN}",
|
||||
f"(uid={username})",
|
||||
SUBTREE,
|
||||
attributes=["uid"],
|
||||
)
|
||||
return bool(conn.entries)
|
||||
|
||||
|
||||
def email_in_use(conn: Connection, email: str) -> bool:
|
||||
conn.search(
|
||||
f"ou=people,{LLDAP_BASE_DN}",
|
||||
f"(mail={email})",
|
||||
SUBTREE,
|
||||
attributes=["uid"],
|
||||
)
|
||||
return bool(conn.entries)
|
||||
|
||||
|
||||
@app.command("list", help="List existing users ")
|
||||
def cmd_list() -> None:
|
||||
with lldap_connection() as conn:
|
||||
conn.search(
|
||||
f"ou=people,{LLDAP_BASE_DN}",
|
||||
"(objectClass=person)",
|
||||
SUBTREE,
|
||||
attributes=["uid", "cn", "mail", "memberOf"],
|
||||
)
|
||||
if not conn.entries:
|
||||
print("No users found.")
|
||||
return
|
||||
print(f"{'USERNAME':<20} {'DISPLAY NAME':<30} {'EMAIL':<30} GROUPS")
|
||||
for entry in conn.entries:
|
||||
uid = entry.uid.value if entry.uid else ""
|
||||
cn = entry.cn.value if entry.cn else ""
|
||||
mail = entry.mail.value if entry.mail else ""
|
||||
groups = ",".join(
|
||||
g.split(",")[0].replace("cn=", "") for g in entry.memberOf.values
|
||||
) if entry.memberOf else ""
|
||||
print(f"{uid:<20} {cn:<30} {mail:<30} {groups}")
|
||||
|
||||
|
||||
@app.command("add", help="Add a user. Omit --password to send an invite email")
|
||||
def cmd_add(
|
||||
username: str = typer.Option(..., "--username", "-u", help="Username"),
|
||||
displayname: str = typer.Option(..., "--displayname", "-n", help="Display name"),
|
||||
password: str | None = typer.Option(None, "--password", "-p", help="Password"),
|
||||
email: str = typer.Option("", "--email", "-e", help="Email address"),
|
||||
groups: list[str] = typer.Option(
|
||||
["users"], "--group", "-g", help="Group to add the user to (repeatable)"
|
||||
),
|
||||
) -> None:
|
||||
validate_username(username)
|
||||
|
||||
invite_mode = password is None
|
||||
if invite_mode and not email:
|
||||
fail("--email is required when --password is omitted")
|
||||
|
||||
effective_email = email or f"{username}@roboces.dev"
|
||||
|
||||
with lldap_connection() as conn:
|
||||
if user_exists(conn, username):
|
||||
fail(f"user '{username}' already exists")
|
||||
if email_in_use(conn, effective_email):
|
||||
fail(f"email '{effective_email}' is already in use")
|
||||
|
||||
if invite_mode:
|
||||
password = generate_random_password()
|
||||
|
||||
user_dn = f"uid={username},ou=people,{LLDAP_BASE_DN}"
|
||||
attrs = {
|
||||
"objectClass": ["inetOrgPerson", "posixAccount", "mailAccount", "person"],
|
||||
"uid": username,
|
||||
"cn": displayname,
|
||||
"mail": effective_email,
|
||||
}
|
||||
if not conn.add(user_dn, attributes=attrs):
|
||||
fail(f"failed to create user: {conn.result}")
|
||||
|
||||
if not conn.extend.standard.modify_password(user=user_dn, new_password=password):
|
||||
fail(f"failed to set password: {conn.result}")
|
||||
|
||||
for group_name in groups:
|
||||
group_id = get_group_id(conn, group_name)
|
||||
if group_id is None:
|
||||
print(f" WARNING: group '{group_name}' not found, skipping")
|
||||
continue
|
||||
if not conn.add_user_to_group(user_dn, group_id):
|
||||
print(f" WARNING: could not add to group '{group_name}': {conn.result}")
|
||||
else:
|
||||
print(f" Added to group '{group_name}'.")
|
||||
|
||||
if invite_mode:
|
||||
trigger_password_reset(username)
|
||||
print(f"User '{username}' created and invite email sent to '{effective_email}'.")
|
||||
else:
|
||||
print(f"User '{username}' created.")
|
||||
|
||||
|
||||
@app.command("remove", help="Remove a user")
|
||||
def cmd_remove(
|
||||
username: str = typer.Option(..., "--username", "-u", help="Username"),
|
||||
) -> None:
|
||||
validate_username(username)
|
||||
with lldap_connection() as conn:
|
||||
user_dn = f"uid={username},ou=people,{LLDAP_BASE_DN}"
|
||||
conn.search(user_dn, "(objectClass=*)", SUBTREE, attributes=["uid"])
|
||||
if not conn.entries:
|
||||
fail(f"user '{username}' not found")
|
||||
if not conn.delete(user_dn):
|
||||
fail(f"failed to delete user: {conn.result}")
|
||||
print(f"User '{username}' removed from LLDAP.")
|
||||
|
||||
|
||||
@app.command("reset-password", help="Trigger a password reset email for an existing user")
|
||||
def cmd_reset_password(
|
||||
username: str = typer.Option(..., "--username", "-u", help="Username"),
|
||||
confirm: bool = typer.Option(
|
||||
True,
|
||||
"--confirm/--no-confirm",
|
||||
help="Prompt for confirmation before sending the reset email",
|
||||
),
|
||||
) -> None:
|
||||
validate_username(username)
|
||||
with lldap_connection() as conn:
|
||||
if not user_exists(conn=conn, username=username):
|
||||
fail(f"user '{username}' not found")
|
||||
|
||||
if confirm:
|
||||
typer.confirm(
|
||||
f"Send password reset email to '{username}'?",
|
||||
abort=True,
|
||||
)
|
||||
|
||||
trigger_password_reset(username)
|
||||
print(f"Password reset email triggered for '{username}'.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("PYTHONUNBUFFERED", "1")
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
app()
|
||||
|
|
@ -114,26 +114,12 @@ resource "adguard_rewrite" "vault" {
|
|||
answer = "192.168.1.12"
|
||||
}
|
||||
|
||||
resource "adguard_rewrite" "authelia-roboces" {
|
||||
resource "adguard_rewrite" "pocketid" {
|
||||
domain = "auth.roboces.dev"
|
||||
answer = "192.168.1.12"
|
||||
}
|
||||
|
||||
resource "adguard_rewrite" "authelia" {
|
||||
domain = "auth.fuku"
|
||||
answer = "192.168.1.12"
|
||||
}
|
||||
|
||||
resource "adguard_rewrite" "dns" {
|
||||
domain = "dns.fuku"
|
||||
answer = "192.168.1.12"
|
||||
}
|
||||
resource "adguard_rewrite" "ldap" {
|
||||
domain = "ldap.fuku"
|
||||
answer = "192.168.1.12"
|
||||
}
|
||||
|
||||
resource "adguard_rewrite" "audiobooks" {
|
||||
domain = "audiobooks.fuku"
|
||||
answer = "192.168.1.12"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue