mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d46998668 | ||
|
|
00d90bfa21 | ||
|
|
03e406322f | ||
|
|
5ee7a76443 | ||
|
|
683de402ff | ||
|
|
e8e939ad19 | ||
|
|
1b5b5145b0 | ||
|
|
cfd2d76873 | ||
|
|
9e5081ed9b | ||
|
|
3ab2b1969a | ||
|
|
a27b563113 | ||
|
|
295472a028 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -33,4 +33,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
/data/
|
/data/
|
||||||
|
|
||||||
|
/cloudflare.yaml
|
||||||
|
|||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM node:23-alpine
|
FROM node:23-slim
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml ./
|
||||||
RUN pnpm install --frozen-lockfile --prod
|
RUN pnpm install --frozen-lockfile --prod
|
||||||
COPY . .
|
COPY . .
|
||||||
CMD ["node", "src/index.ts"]
|
CMD ["node", "src/index.ts"]
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -4,7 +4,7 @@ dev-destroy:
|
|||||||
colima delete -f
|
colima delete -f
|
||||||
|
|
||||||
dev-recreate: dev-destroy
|
dev-recreate: dev-destroy
|
||||||
colima start --network-address --kubernetes -m 8 --k3s-arg="--disable=helm-controller,local-storage,traefik" # --mount ${PWD}/data:/data:w
|
colima start --network-address --kubernetes -m 8 --k3s-arg="--disable helm-controller,local-storage,traefik --docker" # --mount ${PWD}/data:/data:w
|
||||||
flux install --components="source-controller,helm-controller"
|
flux install --components="source-controller,helm-controller"
|
||||||
|
|
||||||
setup-flux:
|
setup-flux:
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: ClusterIssuer
|
|
||||||
metadata:
|
|
||||||
name: letsencrypt-prod
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
|
|
||||||
spec:
|
|
||||||
acme:
|
|
||||||
server: https://acme-v02.api.letsencrypt.org/directory
|
|
||||||
email: alice@alice.com
|
|
||||||
privateKeySecretRef:
|
|
||||||
name: letsencrypt-prod-account-key
|
|
||||||
solvers:
|
|
||||||
- dns01:
|
|
||||||
cloudflare:
|
|
||||||
email: alice@alice.com
|
|
||||||
apiTokenSecretRef:
|
|
||||||
name: cloudflare-api-token
|
|
||||||
key: api-token
|
|
||||||
3
charts/apps/audiobookshelf/Chart.yaml
Normal file
3
charts/apps/audiobookshelf/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: audiobookshelf
|
||||||
13
charts/apps/audiobookshelf/templates/client.yaml
Normal file
13
charts/apps/audiobookshelf/templates/client.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: OidcClient
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
redirectUris:
|
||||||
|
- path: /audiobookshelf/auth/openid/callback
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
matchingMode: strict
|
||||||
|
- path: /audiobookshelf/auth/openid/mobile-redirect
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
matchingMode: strict
|
||||||
52
charts/apps/audiobookshelf/templates/deployment.yaml
Normal file
52
charts/apps/audiobookshelf/templates/deployment.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: '{{ .Release.Name }}'
|
||||||
|
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
|
||||||
|
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /config
|
||||||
|
name: config
|
||||||
|
- mountPath: /metadata
|
||||||
|
name: metadata
|
||||||
|
- mountPath: /audiobooks
|
||||||
|
name: audiobooks
|
||||||
|
- mountPath: /podcasts
|
||||||
|
name: podcasts
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-config'
|
||||||
|
- name: metadata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-metadata'
|
||||||
|
- name: audiobooks
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: books
|
||||||
|
- name: podcasts
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: podcasts
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: ExternalHttpService
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
destination:
|
||||||
|
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
24
charts/apps/audiobookshelf/templates/pvc.yaml
Normal file
24
charts/apps/audiobookshelf/templates/pvc.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-config'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.globals.environment }}'
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-metadata'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.globals.environment }}'
|
||||||
15
charts/apps/audiobookshelf/templates/service.yaml
Normal file
15
charts/apps/audiobookshelf/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
7
charts/apps/audiobookshelf/values.yaml
Normal file
7
charts/apps/audiobookshelf/values.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
globals:
|
||||||
|
environment: prod
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/advplyr/audiobookshelf
|
||||||
|
tag: 2.26.1
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
subdomain: audiobookshelf
|
||||||
3
charts/apps/bytestash/Chart.yaml
Normal file
3
charts/apps/bytestash/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: ByteStash
|
||||||
13
charts/apps/bytestash/templates/_headless-service.yaml
Normal file
13
charts/apps/bytestash/templates/_headless-service.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-headless'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
ports:
|
||||||
|
- port: 5000
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
11
charts/apps/bytestash/templates/_http-service.yaml
Normal file
11
charts/apps/bytestash/templates/_http-service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: HttpService
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.environment }}'
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
destination:
|
||||||
|
host: '{{ .Release.Name }}'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
10
charts/apps/bytestash/templates/client.yaml
Normal file
10
charts/apps/bytestash/templates/client.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: OidcClient
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
redirectUris:
|
||||||
|
- path: /api/auth/oidc/callback
|
||||||
|
subdomain: bytestash
|
||||||
|
matchingMode: strict
|
||||||
55
charts/apps/bytestash/templates/deployment.yaml
Normal file
55
charts/apps/bytestash/templates/deployment.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
serviceName: '{{ .Release.Name }}-headless'
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: '{{ .Release.Name }}'
|
||||||
|
image: ghcr.io/jordan-dalby/bytestash:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
name: http
|
||||||
|
env:
|
||||||
|
- name: ALLOW_NEW_ACCOUNTS
|
||||||
|
value: 'true'
|
||||||
|
- name: DISABLE_INTERNAL_ACCOUNTS
|
||||||
|
value: 'true'
|
||||||
|
- name: OIDC_ENABLED
|
||||||
|
value: 'true'
|
||||||
|
- name: OIDC_DISPLAY_NAME
|
||||||
|
value: OIDC
|
||||||
|
- name: OIDC_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: clientId
|
||||||
|
- name: OIDC_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: clientSecret
|
||||||
|
- name: OIDC_ISSUER_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: configuration
|
||||||
|
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data/snippets
|
||||||
|
name: data
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-data'
|
||||||
11
charts/apps/bytestash/templates/external-http-service.yaml
Normal file
11
charts/apps/bytestash/templates/external-http-service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: ExternalHttpService
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
destination:
|
||||||
|
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
11
charts/apps/bytestash/templates/pvc.yaml
Normal file
11
charts/apps/bytestash/templates/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-data'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.globals.environment }}'
|
||||||
15
charts/apps/bytestash/templates/service.yaml
Normal file
15
charts/apps/bytestash/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 5000
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
3
charts/apps/bytestash/values.yaml
Normal file
3
charts/apps/bytestash/values.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
globals:
|
||||||
|
environment: prod
|
||||||
|
subdomain: bytestash
|
||||||
3
charts/apps/jellyfin/Chart.yaml
Normal file
3
charts/apps/jellyfin/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: Jellyfin
|
||||||
1
charts/apps/jellyfin/notes.md
Normal file
1
charts/apps/jellyfin/notes.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://www.authelia.com/integration/openid-connect/clients/jellyfin/
|
||||||
10
charts/apps/jellyfin/templates/client.yaml
Normal file
10
charts/apps/jellyfin/templates/client.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: OidcClient
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.environment }}'
|
||||||
|
redirectUris:
|
||||||
|
- path: /sso/OID/redirect/Authentik
|
||||||
|
subdomain: '{{ .Values.globals.subdomain }}'
|
||||||
|
matchingMode: strict
|
||||||
11
charts/apps/jellyfin/templates/config-pvc.yaml
Normal file
11
charts/apps/jellyfin/templates/config-pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-config'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.environment }}'
|
||||||
52
charts/apps/jellyfin/templates/deployment.yaml
Normal file
52
charts/apps/jellyfin/templates/deployment.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: '{{ .Release.Name }}'
|
||||||
|
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
|
||||||
|
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8096
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /config
|
||||||
|
name: config
|
||||||
|
- mountPath: /media/movies
|
||||||
|
name: movies
|
||||||
|
- mountPath: /media/tv-shows
|
||||||
|
name: tvshows
|
||||||
|
- mountPath: /media/music
|
||||||
|
name: music
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-config'
|
||||||
|
- name: movies
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: movies
|
||||||
|
- name: tvshows
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: tvshows
|
||||||
|
- name: music
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: music
|
||||||
11
charts/apps/jellyfin/templates/external-http-service.yaml
Normal file
11
charts/apps/jellyfin/templates/external-http-service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: ExternalHttpService
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
destination:
|
||||||
|
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
15
charts/apps/jellyfin/templates/service.yaml
Normal file
15
charts/apps/jellyfin/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8096
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
7
charts/apps/jellyfin/values.yaml
Normal file
7
charts/apps/jellyfin/values.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
globals:
|
||||||
|
environment: prod
|
||||||
|
image:
|
||||||
|
repository: docker.io/jellyfin/jellyfin
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
subdomain: jellyfin
|
||||||
3
charts/apps/miniflux.disable/Chart.yaml
Normal file
3
charts/apps/miniflux.disable/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: ByteStash
|
||||||
10
charts/apps/miniflux.disable/templates/client.yaml
Normal file
10
charts/apps/miniflux.disable/templates/client.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: OidcClient
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
redirectUris:
|
||||||
|
- path: /api/auth/oidc/callback
|
||||||
|
subdomain: bytestash
|
||||||
|
matchingMode: strict
|
||||||
55
charts/apps/miniflux.disable/templates/deployment.yaml
Normal file
55
charts/apps/miniflux.disable/templates/deployment.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
serviceName: '{{ .Release.Name }}-headless'
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: '{{ .Release.Name }}'
|
||||||
|
image: ghcr.io/miniflux/miniflux:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: http
|
||||||
|
env:
|
||||||
|
- name: ALLOW_NEW_ACCOUNTS
|
||||||
|
value: 'true'
|
||||||
|
- name: DISABLE_INTERNAL_ACCOUNTS
|
||||||
|
value: 'true'
|
||||||
|
- name: OIDC_ENABLED
|
||||||
|
value: 'true'
|
||||||
|
- name: OIDC_DISPLAY_NAME
|
||||||
|
value: OIDC
|
||||||
|
- name: OIDC_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: clientId
|
||||||
|
- name: OIDC_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: clientSecret
|
||||||
|
- name: OIDC_ISSUER_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: configuration
|
||||||
|
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data/snippets
|
||||||
|
name: data
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-data'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: ExternalHttpService
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
destination:
|
||||||
|
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
11
charts/apps/miniflux.disable/templates/pvc.yaml
Normal file
11
charts/apps/miniflux.disable/templates/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-data'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.globals.environment }}'
|
||||||
15
charts/apps/miniflux.disable/templates/service.yaml
Normal file
15
charts/apps/miniflux.disable/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
3
charts/apps/miniflux.disable/values.yaml
Normal file
3
charts/apps/miniflux.disable/values.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
globals:
|
||||||
|
environment: prod
|
||||||
|
subdomain: miniflux
|
||||||
3
charts/apps/n8n/Chart.yaml
Normal file
3
charts/apps/n8n/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: Jellyfin
|
||||||
6
charts/apps/n8n/templates/database.yaml
Normal file
6
charts/apps/n8n/templates/database.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: PostgresDatabase
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
73
charts/apps/n8n/templates/deployment.yaml
Normal file
73
charts/apps/n8n/templates/deployment.yaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: '{{ .Release.Name }}'
|
||||||
|
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
|
||||||
|
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 5678
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /home/node/.n8n
|
||||||
|
name: data
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: '{{ .Values.globals.timezone }}'
|
||||||
|
- name: GENERIC_TIMEZONE
|
||||||
|
value: '{{ .Values.globals.timezone }}'
|
||||||
|
- name: N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS
|
||||||
|
value: 'true'
|
||||||
|
- name: N8N_RUNNERS_ENABLED
|
||||||
|
value: 'true'
|
||||||
|
- name: DB_TYPE
|
||||||
|
value: postgresdb
|
||||||
|
- name: DB_POSTGRESDB_DATABASE
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-pg-connection'
|
||||||
|
key: database
|
||||||
|
- name: DB_POSTGRESDB_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-pg-connection'
|
||||||
|
key: host
|
||||||
|
- name: DB_POSTGRESDB_PORT
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-pg-connection'
|
||||||
|
key: port
|
||||||
|
- name: DB_POSTGRESDB_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-pg-connection'
|
||||||
|
key: user
|
||||||
|
- name: DB_POSTGRESDB_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-pg-connection'
|
||||||
|
key: password
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-data'
|
||||||
11
charts/apps/n8n/templates/external-http-service.yaml
Normal file
11
charts/apps/n8n/templates/external-http-service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: ExternalHttpService
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
destination:
|
||||||
|
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
11
charts/apps/n8n/templates/pvc.yaml
Normal file
11
charts/apps/n8n/templates/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-data'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.globals.environment }}'
|
||||||
15
charts/apps/n8n/templates/service.yaml
Normal file
15
charts/apps/n8n/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 5678
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
8
charts/apps/n8n/values.yaml
Normal file
8
charts/apps/n8n/values.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
globals:
|
||||||
|
environment: prod
|
||||||
|
timezone: Europe/Amsterdam
|
||||||
|
image:
|
||||||
|
repository: docker.n8n.io/n8nio/n8n
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
subdomain: n8n
|
||||||
3
charts/apps/ollama/Chart.yaml
Normal file
3
charts/apps/ollama/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: ollama
|
||||||
10
charts/apps/ollama/templates/client.yaml
Normal file
10
charts/apps/ollama/templates/client.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: OidcClient
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
redirectUris:
|
||||||
|
- path: /oauth/oidc/callback
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
matchingMode: strict
|
||||||
38
charts/apps/ollama/templates/deployment.yaml
Normal file
38
charts/apps/ollama/templates/deployment.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: '{{ .Release.Name }}'
|
||||||
|
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
|
||||||
|
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 11434
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /root/.ollama
|
||||||
|
name: data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-data'
|
||||||
11
charts/apps/ollama/templates/pvc.yaml
Normal file
11
charts/apps/ollama/templates/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-data'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.globals.environment }}'
|
||||||
15
charts/apps/ollama/templates/service.yaml
Normal file
15
charts/apps/ollama/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 11434
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
7
charts/apps/ollama/values.yaml
Normal file
7
charts/apps/ollama/values.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
globals:
|
||||||
|
environment: prod
|
||||||
|
image:
|
||||||
|
repository: ollama/ollama
|
||||||
|
tag: 0.11.8
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
subdomain: openwebui
|
||||||
3
charts/apps/openwebui/Chart.yaml
Normal file
3
charts/apps/openwebui/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: openwebui
|
||||||
10
charts/apps/openwebui/templates/client.yaml
Normal file
10
charts/apps/openwebui/templates/client.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: OidcClient
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
redirectUris:
|
||||||
|
- path: /oauth/oidc/callback
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
matchingMode: strict
|
||||||
70
charts/apps/openwebui/templates/deployment.yaml
Normal file
70
charts/apps/openwebui/templates/deployment.yaml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: '{{ .Release.Name }}'
|
||||||
|
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
|
||||||
|
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /app/backend/data
|
||||||
|
name: data
|
||||||
|
env:
|
||||||
|
- name: ENABLE_SIGNUP
|
||||||
|
value: 'false'
|
||||||
|
- name: WEBUI_URL # TODO: remove
|
||||||
|
value: https://openwebui.olsen.cloud
|
||||||
|
- name: ENABLE_OAUTH_PERSISTENT_CONFIG
|
||||||
|
value: 'false'
|
||||||
|
- name: ENABLE_OAUTH_SIGNUP
|
||||||
|
value: 'true'
|
||||||
|
- name: OAUTH_MERGE_ACCOUNTS_BY_EMAIL
|
||||||
|
value: 'true'
|
||||||
|
- name: OAUTH_PROVIDER_NAME
|
||||||
|
value: authentik
|
||||||
|
- name: OPENID_PROVIDER_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: configuration
|
||||||
|
- name: OAUTH_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: clientId
|
||||||
|
- name: OAUTH_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: '{{ .Release.Name }}-client'
|
||||||
|
key: clientSecret
|
||||||
|
- name: ENABLE_LOGIN_FORM
|
||||||
|
value: 'false'
|
||||||
|
- name: OPENID_REDIRECT
|
||||||
|
value: https://openwebui.olsen.cloud/oauth/oidc/callback
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: '{{ .Release.Name }}-data'
|
||||||
11
charts/apps/openwebui/templates/external-http-service.yaml
Normal file
11
charts/apps/openwebui/templates/external-http-service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: ExternalHttpService
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
environment: '{{ .Values.globals.environment }}'
|
||||||
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
|
destination:
|
||||||
|
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
11
charts/apps/openwebui/templates/pvc.yaml
Normal file
11
charts/apps/openwebui/templates/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-data'
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- 'ReadWriteOnce'
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: '1Gi'
|
||||||
|
storageClassName: '{{ .Values.globals.environment }}'
|
||||||
15
charts/apps/openwebui/templates/service.yaml
Normal file
15
charts/apps/openwebui/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
7
charts/apps/openwebui/values.yaml
Normal file
7
charts/apps/openwebui/values.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
globals:
|
||||||
|
environment: prod
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/open-webui/open-webui
|
||||||
|
tag: main
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
subdomain: openwebui
|
||||||
0
charts/apps/values.yaml
Normal file
0
charts/apps/values.yaml
Normal file
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
image:
|
image:
|
||||||
repository: ghcr.io/morten-olsen/homelab-operator
|
repository: ghcr.io/morten-olsen/homelab-operator
|
||||||
pullPolicy: Always
|
pullPolicy: IfNotPresent
|
||||||
# Overrides the image tag whose default is the chart appVersion.
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
tag: main
|
tag: main
|
||||||
|
|
||||||
|
|||||||
3
charts/root/Chart.yaml
Normal file
3
charts/root/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: root
|
||||||
33
charts/root/templates/apps.yaml
Normal file
33
charts/root/templates/apps.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: ApplicationSet
|
||||||
|
metadata:
|
||||||
|
name: homelab-apps
|
||||||
|
namespace: '{{ .Values.env }}-argo'
|
||||||
|
spec:
|
||||||
|
generators:
|
||||||
|
- git:
|
||||||
|
repoURL: '{{ .Values.repo }}'
|
||||||
|
revision: '{{ .Values.ref }}'
|
||||||
|
directories:
|
||||||
|
- path: charts/apps/*
|
||||||
|
include: '.*'
|
||||||
|
exclude: '.*.disabled'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: '{{`{{path.basename}}`}}'
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: '{{ .Values.repo }}'
|
||||||
|
targetRevision: '{{ .Values.ref }}'
|
||||||
|
path: charts/apps/{{`{{path.basename}}`}}
|
||||||
|
helm:
|
||||||
|
values: |
|
||||||
|
globals: {{ .Values.globals | toYaml | nindent 14 }}
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: '{{ .Values.globals.env }}'
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
21
charts/root/templates/root.yaml
Normal file
21
charts/root/templates/root.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: homelab-root
|
||||||
|
namespace: '{{ .Values.globals.env }}-argo'
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: '{{ .Values.repo }}'
|
||||||
|
targetRevision: '{{ .Values.ref }}'
|
||||||
|
path: charts/root
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- values.yaml
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: '{{ .Values.globals.env }}-argo'
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
4
charts/root/values.yaml
Normal file
4
charts/root/values.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
globals:
|
||||||
|
env: prod
|
||||||
|
repo: https://github.com/morten-olsen/homelab-operator.git
|
||||||
|
ref: HEAD
|
||||||
3
charts/volumes/Chart.yaml
Normal file
3
charts/volumes/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
version: 1.0.0
|
||||||
|
name: Resources
|
||||||
28
charts/volumes/templates/books-pvc.yaml
Normal file
28
charts/volumes/templates/books-pvc.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: books
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual-books
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.books.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: books
|
||||||
|
spec:
|
||||||
|
storageClassName: manual-books
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
28
charts/volumes/templates/movies-pvc.yaml
Normal file
28
charts/volumes/templates/movies-pvc.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: movies
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual-movies
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.movies.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: movies
|
||||||
|
spec:
|
||||||
|
storageClassName: manual-movies
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
28
charts/volumes/templates/music-pvc.yaml
Normal file
28
charts/volumes/templates/music-pvc.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: music
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual-music
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.music.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: music
|
||||||
|
spec:
|
||||||
|
storageClassName: manual-music
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
28
charts/volumes/templates/podcasts-pvc.yaml
Normal file
28
charts/volumes/templates/podcasts-pvc.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: podcasts
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual-podcasts
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.podcasts.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: podcasts
|
||||||
|
spec:
|
||||||
|
storageClassName: manual-podcasts
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
28
charts/volumes/templates/tv-pvc.yaml
Normal file
28
charts/volumes/templates/tv-pvc.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: tvshows
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual-tvshows
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.tvshows.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: tvshows
|
||||||
|
spec:
|
||||||
|
storageClassName: manual-tvshows
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
11
charts/volumes/values.yaml
Normal file
11
charts/volumes/values.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
host: 192.168.20.106
|
||||||
|
movies:
|
||||||
|
path: /mnt/HDD/Movies
|
||||||
|
tvshows:
|
||||||
|
path: /mnt/HDD/TV-Shows
|
||||||
|
music:
|
||||||
|
path: /mnt/HDD/Music2
|
||||||
|
books:
|
||||||
|
path: /mnt/HDD/Books
|
||||||
|
podcasts:
|
||||||
|
path: /mnt/HDD/Podcasts
|
||||||
22
istio-test.yaml
Normal file
22
istio-test.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: networking.istio.io/v1beta1
|
||||||
|
kind: ServiceEntry
|
||||||
|
metadata:
|
||||||
|
name: dev-authentik-override
|
||||||
|
namespace: dev
|
||||||
|
spec:
|
||||||
|
hosts:
|
||||||
|
- authentik.mortenolsen.nett
|
||||||
|
ports:
|
||||||
|
- number: 443
|
||||||
|
name: https
|
||||||
|
protocol: HTTPS
|
||||||
|
- number: 80
|
||||||
|
name: http
|
||||||
|
protocol: HTTP
|
||||||
|
location: MESH_EXTERNAL
|
||||||
|
resolution: STATIC
|
||||||
|
endpoints:
|
||||||
|
- address: 1.1.1.1
|
||||||
|
ports:
|
||||||
|
https: 443
|
||||||
|
http: 80
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
kind: AuthentikClient
|
kind: OidcClient
|
||||||
metadata:
|
metadata:
|
||||||
name: test-client
|
name: test-client
|
||||||
spec:
|
spec:
|
||||||
server: dev/dev-authentik-server
|
environment: dev
|
||||||
redirectUris:
|
redirectUris:
|
||||||
- url: https://localhost:3000/api/v1/authentik/oauth2/callback
|
- url: https://localhost:3000/api/v1/authentik/oauth2/callback
|
||||||
matchingMode: strict
|
matchingMode: strict
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ metadata:
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
kind: Environment
|
kind: Environment
|
||||||
metadata:
|
metadata:
|
||||||
name: dev
|
name: prod
|
||||||
namespace: dev
|
|
||||||
spec:
|
spec:
|
||||||
domain: one.dev.olsen.cloud
|
domain: olsen.cloud
|
||||||
|
networkIp: 192.168.20.180
|
||||||
tls:
|
tls:
|
||||||
issuer: letsencrypt-prod
|
issuer: lets-encrypt-prod
|
||||||
|
|||||||
14
manifests/test-service.yaml
Normal file
14
manifests/test-service.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: networking.istio.io/v1alpha3
|
||||||
|
kind: ServiceEntry
|
||||||
|
metadata:
|
||||||
|
name: test-example-com
|
||||||
|
namespace: dev
|
||||||
|
spec:
|
||||||
|
hosts:
|
||||||
|
- authentik.one.dev.olsen.cloud
|
||||||
|
# (the address field is optional if you use 'resolution: DNS')
|
||||||
|
ports:
|
||||||
|
- number: 80
|
||||||
|
name: https
|
||||||
|
protocol: HTTPS
|
||||||
|
resolution: DNS
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@goauthentik/api": "2025.6.3-1751754396",
|
"@goauthentik/api": "2025.6.3-1751754396",
|
||||||
"@kubernetes/client-node": "^1.3.0",
|
"@kubernetes/client-node": "^1.3.0",
|
||||||
|
"cloudflare": "^4.5.0",
|
||||||
|
"cron": "^4.3.3",
|
||||||
"debounce": "^2.2.0",
|
"debounce": "^2.2.0",
|
||||||
"deep-equal": "^2.2.3",
|
"deep-equal": "^2.2.3",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
@@ -35,6 +37,12 @@
|
|||||||
"yaml": "^2.8.0",
|
"yaml": "^2.8.0",
|
||||||
"zod": "^4.0.14"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
|
"imports": {
|
||||||
|
"#services/*": "./src/services/*",
|
||||||
|
"#resources/*": "./src/resources/*",
|
||||||
|
"#bootstrap/*": "./src/bootstrap/*",
|
||||||
|
"#utils/*": "./src/utils/*"
|
||||||
|
},
|
||||||
"packageManager": "pnpm@10.6.0",
|
"packageManager": "pnpm@10.6.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
@@ -14,6 +14,12 @@ importers:
|
|||||||
'@kubernetes/client-node':
|
'@kubernetes/client-node':
|
||||||
specifier: ^1.3.0
|
specifier: ^1.3.0
|
||||||
version: 1.3.0(encoding@0.1.13)
|
version: 1.3.0(encoding@0.1.13)
|
||||||
|
cloudflare:
|
||||||
|
specifier: ^4.5.0
|
||||||
|
version: 4.5.0(encoding@0.1.13)
|
||||||
|
cron:
|
||||||
|
specifier: ^4.3.3
|
||||||
|
version: 4.3.3
|
||||||
debounce:
|
debounce:
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
@@ -229,9 +235,15 @@ packages:
|
|||||||
'@types/lodash@4.17.20':
|
'@types/lodash@4.17.20':
|
||||||
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
|
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
|
||||||
|
|
||||||
|
'@types/luxon@3.7.1':
|
||||||
|
resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==}
|
||||||
|
|
||||||
'@types/node-fetch@2.6.12':
|
'@types/node-fetch@2.6.12':
|
||||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||||
|
|
||||||
|
'@types/node@18.19.123':
|
||||||
|
resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==}
|
||||||
|
|
||||||
'@types/node@22.16.5':
|
'@types/node@22.16.5':
|
||||||
resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==}
|
resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==}
|
||||||
|
|
||||||
@@ -303,6 +315,10 @@ packages:
|
|||||||
abbrev@1.1.1:
|
abbrev@1.1.1:
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -479,6 +495,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
cloudflare@4.5.0:
|
||||||
|
resolution: {integrity: sha512-fPcbPKx4zF45jBvQ0z7PCdgejVAPBBCZxwqk1k7krQNfpM07Cfj97/Q6wBzvYqlWXx/zt1S9+m8vnfCe06umbQ==}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -507,6 +526,10 @@ packages:
|
|||||||
console-control-strings@1.1.0:
|
console-control-strings@1.1.0:
|
||||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||||
|
|
||||||
|
cron@4.3.3:
|
||||||
|
resolution: {integrity: sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==}
|
||||||
|
engines: {node: '>=18.x'}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -754,6 +777,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1:
|
||||||
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
eventemitter3@5.0.1:
|
eventemitter3@5.0.1:
|
||||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||||
|
|
||||||
@@ -825,10 +852,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
form-data-encoder@1.7.2:
|
||||||
|
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||||
|
|
||||||
form-data@4.0.4:
|
form-data@4.0.4:
|
||||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
formdata-node@4.4.1:
|
||||||
|
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||||
|
engines: {node: '>= 12.20'}
|
||||||
|
|
||||||
fs-constants@1.0.0:
|
fs-constants@1.0.0:
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
@@ -1238,6 +1272,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
luxon@3.7.1:
|
||||||
|
resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
make-fetch-happen@9.1.0:
|
make-fetch-happen@9.1.0:
|
||||||
resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
|
resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -1339,6 +1377,11 @@ packages:
|
|||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
|
node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
deprecated: Use your platform's native DOMException instead
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@@ -1886,6 +1929,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
undici-types@5.26.5:
|
||||||
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
@@ -1905,6 +1951,10 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
web-streams-polyfill@4.0.0-beta.3:
|
||||||
|
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
@@ -2129,11 +2179,17 @@ snapshots:
|
|||||||
|
|
||||||
'@types/lodash@4.17.20': {}
|
'@types/lodash@4.17.20': {}
|
||||||
|
|
||||||
|
'@types/luxon@3.7.1': {}
|
||||||
|
|
||||||
'@types/node-fetch@2.6.12':
|
'@types/node-fetch@2.6.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.16.5
|
'@types/node': 22.16.5
|
||||||
form-data: 4.0.4
|
form-data: 4.0.4
|
||||||
|
|
||||||
|
'@types/node@18.19.123':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 5.26.5
|
||||||
|
|
||||||
'@types/node@22.16.5':
|
'@types/node@22.16.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
@@ -2240,6 +2296,10 @@ snapshots:
|
|||||||
abbrev@1.1.1:
|
abbrev@1.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
abort-controller@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
@@ -2258,7 +2318,6 @@ snapshots:
|
|||||||
agentkeepalive@4.6.0:
|
agentkeepalive@4.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
humanize-ms: 1.2.1
|
humanize-ms: 1.2.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
aggregate-error@3.1.0:
|
aggregate-error@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2463,6 +2522,18 @@ snapshots:
|
|||||||
clean-stack@2.2.0:
|
clean-stack@2.2.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
cloudflare@4.5.0(encoding@0.1.13):
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.123
|
||||||
|
'@types/node-fetch': 2.6.12
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
agentkeepalive: 4.6.0
|
||||||
|
form-data-encoder: 1.7.2
|
||||||
|
formdata-node: 4.4.1
|
||||||
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -2485,6 +2556,11 @@ snapshots:
|
|||||||
console-control-strings@1.1.0:
|
console-control-strings@1.1.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
cron@4.3.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/luxon': 3.7.1
|
||||||
|
luxon: 3.7.1
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -2828,6 +2904,8 @@ snapshots:
|
|||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
|
event-target-shim@5.0.1: {}
|
||||||
|
|
||||||
eventemitter3@5.0.1: {}
|
eventemitter3@5.0.1: {}
|
||||||
|
|
||||||
execa@9.6.0:
|
execa@9.6.0:
|
||||||
@@ -2903,6 +2981,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
|
|
||||||
|
form-data-encoder@1.7.2: {}
|
||||||
|
|
||||||
form-data@4.0.4:
|
form-data@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
@@ -2911,6 +2991,11 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
formdata-node@4.4.1:
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 4.0.0-beta.3
|
||||||
|
|
||||||
fs-constants@1.0.0: {}
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fs-minipass@2.1.0:
|
fs-minipass@2.1.0:
|
||||||
@@ -3064,7 +3149,6 @@ snapshots:
|
|||||||
humanize-ms@1.2.1:
|
humanize-ms@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
optional: true
|
|
||||||
|
|
||||||
iconv-lite@0.6.3:
|
iconv-lite@0.6.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3329,6 +3413,8 @@ snapshots:
|
|||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
luxon@3.7.1: {}
|
||||||
|
|
||||||
make-fetch-happen@9.1.0:
|
make-fetch-happen@9.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
agentkeepalive: 4.6.0
|
agentkeepalive: 4.6.0
|
||||||
@@ -3440,6 +3526,8 @@ snapshots:
|
|||||||
|
|
||||||
node-addon-api@7.1.1: {}
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0(encoding@0.1.13):
|
node-fetch@2.7.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
@@ -4098,6 +4186,8 @@ snapshots:
|
|||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
which-boxed-primitive: 1.1.1
|
which-boxed-primitive: 1.1.1
|
||||||
|
|
||||||
|
undici-types@5.26.5: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
unicorn-magic@0.3.0: {}
|
unicorn-magic@0.3.0: {}
|
||||||
@@ -4118,6 +4208,8 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
web-streams-polyfill@4.0.0-beta.3: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
|
|||||||
9
pyproject.toml
Normal file
9
pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "homelab-operator"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"kubediagrams>=0.5.0",
|
||||||
|
]
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
for f in "./test-manifests/"*; do
|
|
||||||
echo "Applying $f"
|
|
||||||
kubectl apply -f "$f"
|
|
||||||
done
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Load environment variables from .env file
|
|
||||||
if [ -f .env ]; then
|
|
||||||
export $(cat .env | grep -v '#' | awk '/=/ {print $1}')
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if CLOUDFLARE_API_KEY is set
|
|
||||||
if [ -z "${CLOUDFLARE_API_KEY}" ]; then
|
|
||||||
echo "Error: CLOUDFLARE_API_KEY is not set. Please add it to your .env file."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the postgres namespace if it doesn't exist
|
|
||||||
kubectl get namespace postgres > /dev/null 2>&1 || kubectl create namespace postgres
|
|
||||||
|
|
||||||
# Create the secret
|
|
||||||
kubectl create secret generic cloudflare-api-token \
|
|
||||||
--namespace cert-manager \
|
|
||||||
--from-literal=api-token="${CLOUDFLARE_API_KEY}"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { K8sService } from '../src/services/k8s/k8s.ts';
|
|
||||||
import { Services } from '../src/utils/service.ts';
|
|
||||||
|
|
||||||
const services = new Services();
|
|
||||||
const k8s = services.get(K8sService);
|
|
||||||
|
|
||||||
const manifests = await k8s.extensionsApi.listCustomResourceDefinition();
|
|
||||||
|
|
||||||
for (const manifest of manifests.items) {
|
|
||||||
for (const version of manifest.spec.versions) {
|
|
||||||
console.log(`group: ${manifest.spec.group}, plural: ${manifest.spec.names.plural}, version: ${version.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
flux install --components="source-controller,helm-controller"
|
|
||||||
kubectl create namespace homelab
|
|
||||||
25
skaffold.yaml
Normal file
25
skaffold.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: skaffold/v4beta7
|
||||||
|
kind: Config
|
||||||
|
metadata:
|
||||||
|
name: homelab-operator
|
||||||
|
|
||||||
|
build:
|
||||||
|
cluster: {}
|
||||||
|
artifacts:
|
||||||
|
- image: homelaboperator
|
||||||
|
context: .
|
||||||
|
docker:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
manifests:
|
||||||
|
helm:
|
||||||
|
releases:
|
||||||
|
- name: homelab-operator
|
||||||
|
chartPath: charts/operator
|
||||||
|
setValueTemplates:
|
||||||
|
image.repository: '{{.IMAGE_REPO_homelaboperator}}'
|
||||||
|
image.tag: '{{.IMAGE_TAG_homelaboperator}}'
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
# Use kubectl to apply the manifests.
|
||||||
|
kubectl: {}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { CloudflareTunnel } from '#resources/homelab/cloudflare-tunnel/cloudflare-tunnel.ts';
|
||||||
|
import { ResourceService } from '#services/resources/resources.ts';
|
||||||
import type { Services } from '../utils/service.ts';
|
import type { Services } from '../utils/service.ts';
|
||||||
|
|
||||||
import { NamespaceService } from './namespaces/namespaces.ts';
|
import { NamespaceService } from './namespaces/namespaces.ts';
|
||||||
import { ReleaseService } from './releases/releases.ts';
|
import { ReleaseService } from './releases/releases.ts';
|
||||||
import { RepoService } from './repos/repos.ts';
|
import { RepoService } from './repos/repos.ts';
|
||||||
import { ClusterIssuerService } from './resources/issuer.ts';
|
|
||||||
|
|
||||||
class BootstrapService {
|
class BootstrapService {
|
||||||
#services: Services;
|
#services: Services;
|
||||||
@@ -23,15 +24,18 @@ class BootstrapService {
|
|||||||
return this.#services.get(ReleaseService);
|
return this.#services.get(ReleaseService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get clusterIssuer() {
|
public get cloudflareTunnel() {
|
||||||
return this.#services.get(ClusterIssuerService);
|
const resourceService = this.#services.get(ResourceService);
|
||||||
|
return resourceService.get(CloudflareTunnel, 'cloudflare-tunnel', this.namespaces.homelab.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ensure = async () => {
|
public ensure = async () => {
|
||||||
await this.namespaces.ensure();
|
await this.namespaces.ensure();
|
||||||
await this.repos.ensure();
|
await this.repos.ensure();
|
||||||
await this.releases.ensure();
|
await this.releases.ensure();
|
||||||
await this.clusterIssuer.ensure();
|
await this.cloudflareTunnel.ensure({
|
||||||
|
spec: {},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,19 @@
|
|||||||
import { NamespaceInstance } from '../../instances/namespace.ts';
|
|
||||||
import type { Services } from '../../utils/service.ts';
|
import type { Services } from '../../utils/service.ts';
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
import { ResourceService } from '../../services/resources/resources.ts';
|
||||||
|
|
||||||
|
import { Namespace } from '#resources/core/namespace/namespace.ts';
|
||||||
|
|
||||||
class NamespaceService {
|
class NamespaceService {
|
||||||
#homelab: NamespaceInstance;
|
#homelab: Namespace;
|
||||||
#istioSystem: NamespaceInstance;
|
#istioSystem: Namespace;
|
||||||
#certManager: NamespaceInstance;
|
#certManager: Namespace;
|
||||||
|
|
||||||
constructor(services: Services) {
|
constructor(services: Services) {
|
||||||
const resourceService = services.get(ResourceService);
|
const resourceService = services.get(ResourceService);
|
||||||
this.#homelab = resourceService.getInstance(
|
this.#homelab = resourceService.get(Namespace, 'homelab');
|
||||||
{
|
this.#istioSystem = resourceService.get(Namespace, 'istio-system');
|
||||||
apiVersion: 'v1',
|
this.#certManager = resourceService.get(Namespace, 'cert-manager');
|
||||||
kind: 'Namespace',
|
|
||||||
name: 'homelab',
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#istioSystem = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
name: 'istio-system',
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#certManager = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
name: 'cert-manager',
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#homelab.on('changed', this.ensure);
|
this.#homelab.on('changed', this.ensure);
|
||||||
this.#istioSystem.on('changed', this.ensure);
|
this.#istioSystem.on('changed', this.ensure);
|
||||||
this.#certManager.on('changed', this.ensure);
|
this.#certManager.on('changed', this.ensure);
|
||||||
|
|||||||
@@ -1,56 +1,26 @@
|
|||||||
import { HelmReleaseInstance } from '../../instances/helm-release.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
import { ResourceService } from '../../services/resources/resources.ts';
|
||||||
import { NAMESPACE } from '../../utils/consts.ts';
|
import { NAMESPACE } from '../../utils/consts.ts';
|
||||||
import { Services } from '../../utils/service.ts';
|
import { Services } from '../../utils/service.ts';
|
||||||
import { NamespaceService } from '../namespaces/namespaces.ts';
|
import { NamespaceService } from '../namespaces/namespaces.ts';
|
||||||
import { RepoService } from '../repos/repos.ts';
|
import { RepoService } from '../repos/repos.ts';
|
||||||
|
|
||||||
|
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
|
||||||
|
|
||||||
class ReleaseService {
|
class ReleaseService {
|
||||||
#services: Services;
|
#services: Services;
|
||||||
#certManager: HelmReleaseInstance;
|
#certManager: HelmRelease;
|
||||||
#istioBase: HelmReleaseInstance;
|
#istioBase: HelmRelease;
|
||||||
#istiod: HelmReleaseInstance;
|
#istiod: HelmRelease;
|
||||||
#istioGateway: HelmReleaseInstance;
|
#istioGateway: HelmRelease;
|
||||||
|
|
||||||
constructor(services: Services) {
|
constructor(services: Services) {
|
||||||
this.#services = services;
|
this.#services = services;
|
||||||
const resourceService = services.get(ResourceService);
|
const resourceService = services.get(ResourceService);
|
||||||
this.#certManager = resourceService.getInstance(
|
this.#certManager = resourceService.get(HelmRelease, 'cert-manager', NAMESPACE);
|
||||||
{
|
this.#istioBase = resourceService.get(HelmRelease, 'istio-base', NAMESPACE);
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
this.#istiod = resourceService.get(HelmRelease, 'istiod', NAMESPACE);
|
||||||
kind: 'HelmRelease',
|
this.#istioGateway = resourceService.get(HelmRelease, 'istio-gateway', NAMESPACE);
|
||||||
name: 'cert-manager',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#istioBase = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: 'istio-base',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#istiod = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: 'istiod',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#istioGateway = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: 'istio-gateway',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#certManager.on('changed', this.ensure);
|
this.#certManager.on('changed', this.ensure);
|
||||||
this.#istioBase.on('changed', this.ensure);
|
this.#istioBase.on('changed', this.ensure);
|
||||||
this.#istiod.on('changed', this.ensure);
|
this.#istiod.on('changed', this.ensure);
|
||||||
|
|||||||
@@ -1,110 +1,70 @@
|
|||||||
import type { Services } from '../../utils/service.ts';
|
import type { Services } from '../../utils/service.ts';
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
import { ResourceService } from '../../services/resources/resources.ts';
|
||||||
import { HelmRepoInstance } from '../../instances/helm-repo.ts';
|
|
||||||
import { NAMESPACE } from '../../utils/consts.ts';
|
import { NAMESPACE } from '../../utils/consts.ts';
|
||||||
|
|
||||||
|
import { HelmRepo } from '#resources/flux/helm-repo/helm-repo.ts';
|
||||||
|
|
||||||
class RepoService {
|
class RepoService {
|
||||||
#jetstack: HelmRepoInstance;
|
#jetstack: HelmRepo;
|
||||||
#istio: HelmRepoInstance;
|
#istio: HelmRepo;
|
||||||
#authentik: HelmRepoInstance;
|
#authentik: HelmRepo;
|
||||||
#containerro: HelmRepoInstance;
|
#cloudflare: HelmRepo;
|
||||||
|
#argo: HelmRepo;
|
||||||
|
|
||||||
constructor(services: Services) {
|
constructor(services: Services) {
|
||||||
const resourceService = services.get(ResourceService);
|
const resourceService = services.get(ResourceService);
|
||||||
this.#jetstack = resourceService.getInstance(
|
this.#jetstack = resourceService.get(HelmRepo, 'jetstack', NAMESPACE);
|
||||||
{
|
this.#istio = resourceService.get(HelmRepo, 'istio', NAMESPACE);
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
this.#authentik = resourceService.get(HelmRepo, 'authentik', NAMESPACE);
|
||||||
kind: 'HelmRepository',
|
this.#cloudflare = resourceService.get(HelmRepo, 'cloudflare', NAMESPACE);
|
||||||
name: 'jetstack',
|
this.#argo = resourceService.get(HelmRepo, 'argo', NAMESPACE);
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#istio = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: 'istio',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#authentik = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: 'authentik',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#containerro = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: 'containerro',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#jetstack.on('changed', this.ensure);
|
this.#jetstack.on('changed', this.ensure);
|
||||||
this.#istio.on('changed', this.ensure);
|
this.#istio.on('changed', this.ensure);
|
||||||
this.#authentik.on('changed', this.ensure);
|
this.#authentik.on('changed', this.ensure);
|
||||||
this.#containerro.on('changed', this.ensure);
|
this.#cloudflare.on('changed', this.ensure);
|
||||||
|
this.#argo.on('changed', this.ensure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get jetstack() {
|
public get jetstack() {
|
||||||
return this.#jetstack;
|
return this.#jetstack;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get istio() {
|
public get istio() {
|
||||||
return this.#istio;
|
return this.#istio;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get authentik() {
|
public get authentik() {
|
||||||
return this.#authentik;
|
return this.#authentik;
|
||||||
}
|
}
|
||||||
public get containerro() {
|
|
||||||
return this.#containerro;
|
public get cloudflare() {
|
||||||
|
return this.#cloudflare;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get argo() {
|
||||||
|
return this.#argo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ensure = async () => {
|
public ensure = async () => {
|
||||||
await this.#jetstack.ensure({
|
await this.#jetstack.set({
|
||||||
metadata: {
|
url: 'https://charts.jetstack.io',
|
||||||
name: 'jetstack',
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '1h',
|
|
||||||
url: 'https://charts.jetstack.io',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.#istio.ensure({
|
await this.#istio.set({
|
||||||
metadata: {
|
url: 'https://istio-release.storage.googleapis.com/charts',
|
||||||
name: 'istio',
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '1h',
|
|
||||||
url: 'https://istio-release.storage.googleapis.com/charts',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.#authentik.ensure({
|
await this.#authentik.set({
|
||||||
metadata: {
|
url: 'https://charts.goauthentik.io',
|
||||||
name: 'authentik',
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '1h',
|
|
||||||
url: 'https://charts.goauthentik.io',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.#containerro.ensure({
|
await this.#cloudflare.set({
|
||||||
metadata: {
|
url: 'https://cloudflare.github.io/helm-charts',
|
||||||
name: 'containerro',
|
});
|
||||||
},
|
|
||||||
spec: {
|
await this.#argo.set({
|
||||||
interval: '1h',
|
url: 'https://argoproj.github.io/argo-helm',
|
||||||
url: 'https://charts.containeroo.ch',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { ClusterIssuerInstance } from '../../instances/cluster-issuer.ts';
|
|
||||||
import { CustomDefinitionInstance } from '../../instances/custom-resource-definition.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import type { Services } from '../../utils/service.ts';
|
|
||||||
|
|
||||||
class ClusterIssuerService {
|
|
||||||
#clusterIssuerCrd: CustomDefinitionInstance;
|
|
||||||
#clusterIssuer: ClusterIssuerInstance;
|
|
||||||
|
|
||||||
constructor(services: Services) {
|
|
||||||
const resourceService = services.get(ResourceService);
|
|
||||||
this.#clusterIssuerCrd = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'CustomResourceDefinition',
|
|
||||||
name: 'clusterissuers.cert-manager.io',
|
|
||||||
},
|
|
||||||
CustomDefinitionInstance,
|
|
||||||
);
|
|
||||||
this.#clusterIssuer = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'ClusterIssuer',
|
|
||||||
name: 'cluster-issuer',
|
|
||||||
},
|
|
||||||
ClusterIssuerInstance,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#clusterIssuerCrd.on('changed', this.ensure);
|
|
||||||
this.#clusterIssuer.on('changed', this.ensure);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ensure = async () => {
|
|
||||||
if (!this.#clusterIssuerCrd.ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.#clusterIssuer.ensure({
|
|
||||||
spec: {
|
|
||||||
acme: {
|
|
||||||
server: 'https://acme-v02.api.letsencrypt.org/directory',
|
|
||||||
email: 'admin@example.com',
|
|
||||||
privateKeySecretRef: {
|
|
||||||
name: 'cluster-issuer-key',
|
|
||||||
},
|
|
||||||
solvers: [
|
|
||||||
{
|
|
||||||
dns01: {
|
|
||||||
cloudflare: {
|
|
||||||
email: 'admin@example.com',
|
|
||||||
apiKeySecretRef: {
|
|
||||||
name: 'cloudflare-api-key',
|
|
||||||
key: 'api-key',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ClusterIssuerService };
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import type { V1Secret } from '@kubernetes/client-node';
|
|
||||||
import type { z } from 'zod';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CustomResource,
|
|
||||||
type CustomResourceOptions,
|
|
||||||
type SubresourceResult,
|
|
||||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceReference } from '../../services/resources/resources.ref.ts';
|
|
||||||
import { ResourceService, type Resource } from '../../services/resources/resources.ts';
|
|
||||||
import { getWithNamespace } from '../../utils/naming.ts';
|
|
||||||
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
|
||||||
import { CONTROLLED_LABEL } from '../../utils/consts.ts';
|
|
||||||
import { isDeepSubset } from '../../utils/objects.ts';
|
|
||||||
import { AuthentikService } from '../../services/authentik/authentik.service.ts';
|
|
||||||
import { authentikServerSecretSchema } from '../authentik-server/authentik-server.schemas.ts';
|
|
||||||
|
|
||||||
import { authentikClientSecretSchema, type authentikClientSpecSchema } from './authentik-client.schemas.ts';
|
|
||||||
|
|
||||||
class AuthentikClientResource extends CustomResource<typeof authentikClientSpecSchema> {
|
|
||||||
#serverSecret: ResourceReference<V1Secret>;
|
|
||||||
#clientSecretResource: Resource<V1Secret>;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof authentikClientSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
|
|
||||||
this.#serverSecret = new ResourceReference();
|
|
||||||
this.#clientSecretResource = resourceService.get({
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Secret',
|
|
||||||
name: `authentik-client-${this.name}`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#updateResouces();
|
|
||||||
|
|
||||||
this.#serverSecret.on('changed', this.queueReconcile);
|
|
||||||
this.#clientSecretResource.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
#updateResouces = () => {
|
|
||||||
const serverSecretNames = getWithNamespace(`${this.spec.server}-server`, this.namespace);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
this.#serverSecret.current = resourceService.get({
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Secret',
|
|
||||||
name: serverSecretNames.name,
|
|
||||||
namespace: serverSecretNames.namespace,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
#reconcileClientSecret = async (): Promise<SubresourceResult> => {
|
|
||||||
const serverSecret = this.#serverSecret.current;
|
|
||||||
if (!serverSecret?.exists || !serverSecret.data) {
|
|
||||||
return {
|
|
||||||
ready: false,
|
|
||||||
failed: true,
|
|
||||||
message: 'Server or server secret not found',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const serverSecretData = authentikServerSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
|
||||||
if (!serverSecretData.success || !serverSecretData.data) {
|
|
||||||
return {
|
|
||||||
ready: false,
|
|
||||||
failed: true,
|
|
||||||
message: 'Server secret not found',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const url = serverSecretData.data.url;
|
|
||||||
const appName = this.name;
|
|
||||||
const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(this.#clientSecretResource.data));
|
|
||||||
|
|
||||||
const expectedValues: z.infer<typeof authentikClientSecretSchema> = {
|
|
||||||
clientId: this.name,
|
|
||||||
clientSecret: clientSecretData.data?.clientSecret || crypto.randomUUID(),
|
|
||||||
configuration: new URL(`/application/o/${appName}/.well-known/openid-configuration`, url).toString(),
|
|
||||||
configurationIssuer: new URL(`/application/o/${appName}/`, url).toString(),
|
|
||||||
authorization: new URL(`/application/o/${appName}/authorize/`, url).toString(),
|
|
||||||
token: new URL(`/application/o/${appName}/token/`, url).toString(),
|
|
||||||
userinfo: new URL(`/application/o/${appName}/userinfo/`, url).toString(),
|
|
||||||
endSession: new URL(`/application/o/${appName}/end-session/`, url).toString(),
|
|
||||||
jwks: new URL(`/application/o/${appName}/jwks/`, url).toString(),
|
|
||||||
};
|
|
||||||
if (!isDeepSubset(clientSecretData.data, expectedValues)) {
|
|
||||||
await this.#clientSecretResource.patch({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
labels: {
|
|
||||||
...CONTROLLED_LABEL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: encodeSecret(expectedValues),
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
ready: false,
|
|
||||||
syncing: true,
|
|
||||||
message: 'UpdatingManifest',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
ready: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
#reconcileServer = async (): Promise<SubresourceResult> => {
|
|
||||||
const serverSecret = this.#serverSecret.current;
|
|
||||||
const clientSecret = this.#clientSecretResource;
|
|
||||||
|
|
||||||
if (!serverSecret?.exists || !serverSecret.data) {
|
|
||||||
return {
|
|
||||||
ready: false,
|
|
||||||
failed: true,
|
|
||||||
message: 'Server secret not found',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverSecretData = authentikServerSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
|
||||||
if (!serverSecretData.success || !serverSecretData.data) {
|
|
||||||
return {
|
|
||||||
ready: false,
|
|
||||||
failed: true,
|
|
||||||
message: 'Server secret not found',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(clientSecret.data));
|
|
||||||
if (!clientSecretData.success || !clientSecretData.data) {
|
|
||||||
return {
|
|
||||||
ready: false,
|
|
||||||
failed: true,
|
|
||||||
message: 'Client secret not found',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const authentikService = this.services.get(AuthentikService);
|
|
||||||
const authentikServer = authentikService.get({
|
|
||||||
url: {
|
|
||||||
internal: `http://${serverSecretData.data.host}-server`,
|
|
||||||
external: serverSecretData.data.url,
|
|
||||||
},
|
|
||||||
token: serverSecretData.data.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
(await authentikServer).upsertClient({
|
|
||||||
...this.spec,
|
|
||||||
name: this.name,
|
|
||||||
secret: clientSecretData.data.clientSecret,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
ready: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
if (!this.exists || this.metadata?.deletionTimestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#updateResouces();
|
|
||||||
await Promise.all([
|
|
||||||
this.reconcileSubresource('Secret', this.#reconcileClientSecret),
|
|
||||||
this.reconcileSubresource('Server', this.#reconcileServer),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const secretReady = this.conditions.get('Secret')?.status === 'True';
|
|
||||||
const serverReady = this.conditions.get('Server')?.status === 'True';
|
|
||||||
|
|
||||||
await this.conditions.set('Ready', {
|
|
||||||
status: secretReady && serverReady ? 'True' : 'False',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AuthentikClientResource };
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { ClientTypeEnum, SubModeEnum } from '@goauthentik/api';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const authentikClientSpecSchema = z.object({
|
|
||||||
server: z.string(),
|
|
||||||
subMode: z.enum(SubModeEnum).optional(),
|
|
||||||
clientType: z.enum(ClientTypeEnum).optional(),
|
|
||||||
redirectUris: z.array(
|
|
||||||
z.object({
|
|
||||||
url: z.string(),
|
|
||||||
matchingMode: z.enum(['strict', 'regex']),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const authentikClientSecretSchema = z.object({
|
|
||||||
clientId: z.string(),
|
|
||||||
clientSecret: z.string().optional(),
|
|
||||||
configuration: z.string(),
|
|
||||||
configurationIssuer: z.string(),
|
|
||||||
authorization: z.string(),
|
|
||||||
token: z.string(),
|
|
||||||
userinfo: z.string(),
|
|
||||||
endSession: z.string(),
|
|
||||||
jwks: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { authentikClientSpecSchema, authentikClientSecretSchema };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { AuthentikClientResource } from './authentik-client.resource.ts';
|
|
||||||
import { authentikClientSpecSchema } from './authentik-client.schemas.ts';
|
|
||||||
|
|
||||||
const authentikClientDefinition = createCustomResourceDefinition({
|
|
||||||
group: GROUP,
|
|
||||||
version: 'v1',
|
|
||||||
kind: 'AuthentikClient',
|
|
||||||
names: {
|
|
||||||
plural: 'authentikclients',
|
|
||||||
singular: 'authentikclient',
|
|
||||||
},
|
|
||||||
create: (options) => new AuthentikClientResource(options),
|
|
||||||
spec: authentikClientSpecSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export { authentikClientDefinition };
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
import type { V1Secret } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { RepoService } from '../../bootstrap/repos/repos.ts';
|
|
||||||
import { HelmReleaseInstance } from '../../instances/helm-release.ts';
|
|
||||||
import { SecretInstance } from '../../instances/secret.ts';
|
|
||||||
import {
|
|
||||||
CustomResource,
|
|
||||||
type CustomResourceOptions,
|
|
||||||
type CustomResourceObject,
|
|
||||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceReference } from '../../services/resources/resources.ref.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts';
|
|
||||||
import { SecretService } from '../../services/secrets/secrets.ts';
|
|
||||||
import { API_VERSION } from '../../utils/consts.ts';
|
|
||||||
import { getWithNamespace } from '../../utils/naming.ts';
|
|
||||||
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
|
||||||
import type { environmentSpecSchema } from '../environment/environment.schemas.ts';
|
|
||||||
import { HttpServiceInstance } from '../../instances/http-service.ts';
|
|
||||||
import type { redisServerSpecSchema } from '../redis-server/redis-server.schemas.ts';
|
|
||||||
import { PostgresDatabaseInstance } from '../../instances/postgres-database.ts';
|
|
||||||
|
|
||||||
import {
|
|
||||||
authentikServerInitSecretSchema,
|
|
||||||
authentikServerSecretSchema,
|
|
||||||
type authentikServerSpecSchema,
|
|
||||||
} from './authentik-server.schemas.ts';
|
|
||||||
|
|
||||||
class AuthentikServerController extends CustomResource<typeof authentikServerSpecSchema> {
|
|
||||||
#environment: ResourceReference<CustomResourceObject<typeof environmentSpecSchema>>;
|
|
||||||
#authentikInitSecret: EnsuredSecret<typeof authentikServerInitSecretSchema>;
|
|
||||||
#authentikSecret: SecretInstance;
|
|
||||||
#authentikRelease: HelmReleaseInstance;
|
|
||||||
#postgresSecret: ResourceReference<V1Secret>;
|
|
||||||
#httpService: HttpServiceInstance;
|
|
||||||
#redisServer: ResourceReference<CustomResourceObject<typeof redisServerSpecSchema>>;
|
|
||||||
#postgresDatabase: PostgresDatabaseInstance;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof authentikServerSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const secretService = this.services.get(SecretService);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
|
|
||||||
this.#environment = new ResourceReference();
|
|
||||||
this.#authentikInitSecret = secretService.ensure({
|
|
||||||
owner: [this.ref],
|
|
||||||
name: `${this.name}-init`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
schema: authentikServerInitSecretSchema,
|
|
||||||
generator: () => ({
|
|
||||||
AUTHENTIK_BOOTSTRAP_TOKEN: crypto.randomUUID(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_PASSWORD: crypto.randomUUID(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_EMAIL: 'admin@example.com',
|
|
||||||
AUTHENTIK_SECRET_KEY: crypto.randomUUID(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
this.#authentikSecret = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Secret',
|
|
||||||
name: `${this.name}-server`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
SecretInstance<typeof authentikServerSecretSchema>,
|
|
||||||
);
|
|
||||||
this.#authentikRelease = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#httpService = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'HttpService',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
HttpServiceInstance,
|
|
||||||
);
|
|
||||||
this.#postgresDatabase = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'PostgresDatabase',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
PostgresDatabaseInstance,
|
|
||||||
);
|
|
||||||
this.#redisServer = new ResourceReference();
|
|
||||||
this.#postgresSecret = new ResourceReference();
|
|
||||||
this.#authentikSecret.on('changed', this.queueReconcile);
|
|
||||||
this.#authentikInitSecret.resource.on('deleted', this.queueReconcile);
|
|
||||||
this.#environment.on('changed', this.queueReconcile);
|
|
||||||
this.#authentikRelease.on('changed', this.queueReconcile);
|
|
||||||
this.#postgresSecret.on('changed', this.queueReconcile);
|
|
||||||
this.#httpService.on('changed', this.queueReconcile);
|
|
||||||
this.#redisServer.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
if (!this.exists || this.metadata?.deletionTimestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.#authentikInitSecret.isValid) {
|
|
||||||
await this.markNotReady('MissingAuthentikInitSecret', 'The authentik init secret is not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
const environmentNames = getWithNamespace(this.spec.environment, this.namespace);
|
|
||||||
|
|
||||||
this.#environment.current = resourceService.get({
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'Environment',
|
|
||||||
name: environmentNames.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#postgresDatabase.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
cluster: this.spec.postgresCluster,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const postgresSecret = this.#postgresDatabase.secret;
|
|
||||||
|
|
||||||
if (!postgresSecret.exists) {
|
|
||||||
await this.markNotReady('MissingPostgresSecret', 'The postgres secret is not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const postgresSecretData = decodeSecret(postgresSecret.data) || {};
|
|
||||||
|
|
||||||
if (!this.#environment.current?.exists) {
|
|
||||||
await this.markNotReady(
|
|
||||||
'MissingEnvironment',
|
|
||||||
`Environment ${this.#environment.current?.namespace}/${this.#environment.current?.name} not found`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const domain = this.#environment.current.spec?.domain;
|
|
||||||
if (!domain) {
|
|
||||||
await this.markNotReady('MissingDomain', 'The domain is not set');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretData = {
|
|
||||||
url: `https://${this.spec.subdomain}.${domain}`,
|
|
||||||
host: `${this.name}.${this.namespace}.svc.cluster.local`,
|
|
||||||
token: this.#authentikInitSecret.value?.AUTHENTIK_BOOTSTRAP_TOKEN ?? '',
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.#authentikSecret.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
data: encodeSecret(secretData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const repoService = this.services.get(RepoService);
|
|
||||||
|
|
||||||
const redisNames = getWithNamespace(this.spec.redisServer, this.namespace);
|
|
||||||
const redisHost = `${redisNames.name}.${redisNames.namespace}.svc.cluster.local`;
|
|
||||||
|
|
||||||
await this.#authentikRelease.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '60m',
|
|
||||||
chart: {
|
|
||||||
spec: {
|
|
||||||
chart: 'authentik',
|
|
||||||
version: '2025.6.4',
|
|
||||||
sourceRef: {
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: repoService.authentik.name,
|
|
||||||
namespace: repoService.authentik.namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
global: {
|
|
||||||
envFrom: [
|
|
||||||
{
|
|
||||||
secretRef: {
|
|
||||||
name: this.#authentikInitSecret.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
authentik: {
|
|
||||||
error_reporting: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
postgresql: {
|
|
||||||
host: postgresSecretData.host,
|
|
||||||
name: postgresSecretData.database,
|
|
||||||
user: postgresSecretData.username,
|
|
||||||
password: 'file:///postgres-creds/password',
|
|
||||||
},
|
|
||||||
redis: {
|
|
||||||
host: redisHost,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
secret: {
|
|
||||||
secretName: postgresSecret.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
volumeMounts: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
mountPath: '/postgres-creds',
|
|
||||||
readOnly: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
worker: {
|
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
secret: {
|
|
||||||
secretName: postgresSecret.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
volumeMounts: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
mountPath: '/postgres-creds',
|
|
||||||
readOnly: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#httpService.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
environment: this.spec.environment,
|
|
||||||
subdomain: this.spec.subdomain,
|
|
||||||
destination: {
|
|
||||||
host: `${this.name}-server.${this.namespace}.svc.cluster.local`,
|
|
||||||
port: {
|
|
||||||
number: 80,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.markReady();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AuthentikServerController };
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const authentikServerSpecSchema = z.object({
|
|
||||||
redisServer: z.string(),
|
|
||||||
postgresCluster: z.string(),
|
|
||||||
environment: z.string(),
|
|
||||||
subdomain: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const authentikServerInitSecretSchema = z.object({
|
|
||||||
AUTHENTIK_BOOTSTRAP_TOKEN: z.string(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_PASSWORD: z.string(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_EMAIL: z.string(),
|
|
||||||
AUTHENTIK_SECRET_KEY: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const authentikServerSecretSchema = z.object({
|
|
||||||
url: z.string(),
|
|
||||||
host: z.string(),
|
|
||||||
token: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { authentikServerSpecSchema, authentikServerInitSecretSchema, authentikServerSecretSchema };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { authentikServerSpecSchema } from './authentik-server.schemas.ts';
|
|
||||||
import { AuthentikServerController } from './authentik-server.controller.ts';
|
|
||||||
|
|
||||||
const authentikServerDefinition = createCustomResourceDefinition({
|
|
||||||
group: GROUP,
|
|
||||||
version: 'v1',
|
|
||||||
kind: 'AuthentikServer',
|
|
||||||
names: {
|
|
||||||
plural: 'authentikservers',
|
|
||||||
singular: 'authentikserver',
|
|
||||||
},
|
|
||||||
spec: authentikServerSpecSchema,
|
|
||||||
create: (options) => new AuthentikServerController(options),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { authentikServerDefinition };
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { authentikClientDefinition } from './authentik-client/authentik-client.ts';
|
|
||||||
import { authentikServerDefinition } from './authentik-server/authentik-server.ts';
|
|
||||||
import { environmentDefinition } from './environment/environment.ts';
|
|
||||||
import { generateSecretDefinition } from './generate-secret/generate-secret.ts';
|
|
||||||
import { httpServiceDefinition } from './http-service/http-service.ts';
|
|
||||||
import { postgresClusterDefinition } from './postgres-cluster/postgres-cluster.ts';
|
|
||||||
import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts';
|
|
||||||
import { redisServerDefinition } from './redis-server/redis-server.ts';
|
|
||||||
|
|
||||||
const customResources = [
|
|
||||||
postgresDatabaseDefinition,
|
|
||||||
authentikClientDefinition,
|
|
||||||
generateSecretDefinition,
|
|
||||||
environmentDefinition,
|
|
||||||
postgresClusterDefinition,
|
|
||||||
authentikServerDefinition,
|
|
||||||
httpServiceDefinition,
|
|
||||||
redisServerDefinition,
|
|
||||||
];
|
|
||||||
|
|
||||||
export { customResources };
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import { CertificateInstance } from '../../instances/certificate.ts';
|
|
||||||
import { CustomDefinitionInstance } from '../../instances/custom-resource-definition.ts';
|
|
||||||
import { NamespaceInstance } from '../../instances/namespace.ts';
|
|
||||||
import {
|
|
||||||
CustomResource,
|
|
||||||
type CustomResourceOptions,
|
|
||||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import { GatewayInstance } from '../../instances/gateway.ts';
|
|
||||||
import { PostgresClusterInstance } from '../../instances/postgres-cluster.ts';
|
|
||||||
import { API_VERSION } from '../../utils/consts.ts';
|
|
||||||
import { AuthentikServerInstance } from '../../instances/authentik-server.ts';
|
|
||||||
import { StorageClassInstance } from '../../instances/storageclass.ts';
|
|
||||||
import { PROVISIONER } from '../../storage-provider/storage-provider.ts';
|
|
||||||
import { RedisServerInstance } from '../../instances/redis-server.ts';
|
|
||||||
import { NamespaceService } from '../../bootstrap/namespaces/namespaces.ts';
|
|
||||||
|
|
||||||
import type { environmentSpecSchema } from './environment.schemas.ts';
|
|
||||||
|
|
||||||
class EnvironmentController extends CustomResource<typeof environmentSpecSchema> {
|
|
||||||
#namespace: NamespaceInstance;
|
|
||||||
#certificateCrd: CustomDefinitionInstance;
|
|
||||||
#certificate: CertificateInstance;
|
|
||||||
#gatewayCrd: CustomDefinitionInstance;
|
|
||||||
#gateway: GatewayInstance;
|
|
||||||
#storageClass: StorageClassInstance;
|
|
||||||
#postgresCluster: PostgresClusterInstance;
|
|
||||||
#authentikServer: AuthentikServerInstance;
|
|
||||||
#redisServer: RedisServerInstance;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof environmentSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
const namespaceService = this.services.get(NamespaceService);
|
|
||||||
this.#namespace = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
name: this.namespace,
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#certificateCrd = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'apiextensions.k8s.io/v1',
|
|
||||||
kind: 'CustomResourceDefinition',
|
|
||||||
name: 'certificates.cert-manager.io',
|
|
||||||
},
|
|
||||||
CustomDefinitionInstance,
|
|
||||||
);
|
|
||||||
this.#certificate = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'cert-manager.io/v1',
|
|
||||||
kind: 'Certificate',
|
|
||||||
name: `${this.name}-tls`,
|
|
||||||
namespace: namespaceService.homelab.name,
|
|
||||||
},
|
|
||||||
CertificateInstance,
|
|
||||||
);
|
|
||||||
this.#gatewayCrd = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'apiextensions.k8s.io/v1',
|
|
||||||
kind: 'CustomResourceDefinition',
|
|
||||||
name: 'gateways.networking.istio.io',
|
|
||||||
},
|
|
||||||
CustomDefinitionInstance,
|
|
||||||
);
|
|
||||||
this.#gateway = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'networking.istio.io/v1',
|
|
||||||
kind: 'Gateway',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
GatewayInstance,
|
|
||||||
);
|
|
||||||
this.#storageClass = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'storage.k8s.io/v1',
|
|
||||||
kind: 'StorageClass',
|
|
||||||
name: `${this.name}-retain`,
|
|
||||||
},
|
|
||||||
StorageClassInstance,
|
|
||||||
);
|
|
||||||
this.#postgresCluster = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'PostgresCluster',
|
|
||||||
name: `${this.name}-postgres-cluster`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
PostgresClusterInstance,
|
|
||||||
);
|
|
||||||
this.#authentikServer = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'AuthentikServer',
|
|
||||||
name: `${this.name}-authentik-server`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
AuthentikServerInstance,
|
|
||||||
);
|
|
||||||
this.#redisServer = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'RedisServer',
|
|
||||||
name: `${this.name}-redis-server`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
RedisServerInstance,
|
|
||||||
);
|
|
||||||
this.#gatewayCrd.on('changed', this.queueReconcile);
|
|
||||||
this.#gateway.on('changed', this.queueReconcile);
|
|
||||||
this.#certificateCrd.on('changed', this.queueReconcile);
|
|
||||||
this.#namespace.on('changed', this.queueReconcile);
|
|
||||||
this.#certificate.on('changed', this.queueReconcile);
|
|
||||||
this.#postgresCluster.on('changed', this.queueReconcile);
|
|
||||||
this.#authentikServer.on('changed', this.queueReconcile);
|
|
||||||
this.#storageClass.on('changed', this.queueReconcile);
|
|
||||||
this.#redisServer.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
if (!this.exists || this.metadata?.deletionTimestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.#namespace.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
labels: {
|
|
||||||
'istio-injection': 'enabled',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (this.#certificateCrd.ready) {
|
|
||||||
await this.#certificate.ensure({
|
|
||||||
spec: {
|
|
||||||
secretName: `${this.name}-tls`,
|
|
||||||
issuerRef: {
|
|
||||||
name: this.spec.tls.issuer,
|
|
||||||
kind: 'ClusterIssuer',
|
|
||||||
},
|
|
||||||
dnsNames: [`*.${this.spec.domain}`],
|
|
||||||
privateKey: {
|
|
||||||
rotationPolicy: 'Always',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.#gatewayCrd.ready) {
|
|
||||||
await this.#gateway.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
selector: {
|
|
||||||
istio: 'homelab-istio-gateway',
|
|
||||||
},
|
|
||||||
servers: [
|
|
||||||
{
|
|
||||||
hosts: [`*.${this.spec.domain}`],
|
|
||||||
port: {
|
|
||||||
name: 'http',
|
|
||||||
number: 80,
|
|
||||||
protocol: 'HTTP',
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
httpsRedirect: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hosts: [`*.${this.spec.domain}`],
|
|
||||||
port: {
|
|
||||||
name: 'https',
|
|
||||||
number: 443,
|
|
||||||
protocol: 'HTTPS',
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
mode: 'SIMPLE',
|
|
||||||
credentialName: `${this.name}-tls`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#storageClass.ensure({
|
|
||||||
provisioner: PROVISIONER,
|
|
||||||
parameters: {
|
|
||||||
storageLocation: this.spec.storage?.location || `/data/volumes/${this.name}`,
|
|
||||||
reclaimPolicy: 'Retain',
|
|
||||||
allowVolumeExpansion: 'true',
|
|
||||||
volumeBindingMode: 'Immediate',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#postgresCluster.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
environment: this.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#authentikServer.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
environment: `${this.namespace}/${this.name}`,
|
|
||||||
subdomain: 'authentik',
|
|
||||||
postgresCluster: `${this.name}-postgres-cluster`,
|
|
||||||
redisServer: `${this.name}-redis-server`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#redisServer.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { EnvironmentController };
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const environmentSpecSchema = z.object({
|
|
||||||
domain: z.string(),
|
|
||||||
tls: z.object({
|
|
||||||
issuer: z.string(),
|
|
||||||
}),
|
|
||||||
storage: z
|
|
||||||
.object({
|
|
||||||
location: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type EnvironmentSpec = z.infer<typeof environmentSpecSchema>;
|
|
||||||
|
|
||||||
export { environmentSpecSchema, type EnvironmentSpec };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { EnvironmentController } from './environment.controller.ts';
|
|
||||||
import { environmentSpecSchema } from './environment.schemas.ts';
|
|
||||||
|
|
||||||
const environmentDefinition = createCustomResourceDefinition({
|
|
||||||
group: GROUP,
|
|
||||||
version: 'v1',
|
|
||||||
kind: 'Environment',
|
|
||||||
names: {
|
|
||||||
plural: 'environments',
|
|
||||||
singular: 'environment',
|
|
||||||
},
|
|
||||||
spec: environmentSpecSchema,
|
|
||||||
create: (options) => new EnvironmentController(options),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { environmentDefinition };
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { V1Secret } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CustomResource,
|
|
||||||
type CustomResourceOptions,
|
|
||||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { Resource, ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
|
||||||
import { isDeepSubset } from '../../utils/objects.ts';
|
|
||||||
|
|
||||||
import { generateSecrets } from './generate-secret.utils.ts';
|
|
||||||
import { generateSecretSpecSchema } from './generate-secret.schemas.ts';
|
|
||||||
|
|
||||||
class GenerateSecretResource extends CustomResource<typeof generateSecretSpecSchema> {
|
|
||||||
#secretResource: Resource<V1Secret>;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof generateSecretSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
|
|
||||||
this.#secretResource = resourceService.get({
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Secret',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#secretResource.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
if (!this.exists || this.metadata?.deletionTimestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secrets = generateSecrets(this.spec);
|
|
||||||
const current = decodeSecret(this.#secretResource.data) || {};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
...secrets,
|
|
||||||
...current,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isDeepSubset(current, expected)) {
|
|
||||||
this.#secretResource.patch({
|
|
||||||
data: encodeSecret(expected),
|
|
||||||
});
|
|
||||||
this.conditions.set('SecretUpdated', {
|
|
||||||
status: 'False',
|
|
||||||
reason: 'SecretUpdated',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.conditions.set('Ready', {
|
|
||||||
status: 'True',
|
|
||||||
reason: 'Ready',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { GenerateSecretResource };
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const generateSecretFieldSchema = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
value: z.string().optional(),
|
|
||||||
encoding: z.enum(['base64', 'base64url', 'hex', 'utf8', 'numeric']).optional(),
|
|
||||||
length: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const generateSecretSpecSchema = z.object({
|
|
||||||
fields: z.array(generateSecretFieldSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
type GenerateSecretField = z.infer<typeof generateSecretFieldSchema>;
|
|
||||||
type GenerateSecretSpec = z.infer<typeof generateSecretSpecSchema>;
|
|
||||||
|
|
||||||
export { generateSecretSpecSchema, type GenerateSecretField, type GenerateSecretSpec };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { GenerateSecretResource } from './generate-secret.resource.ts';
|
|
||||||
import { generateSecretSpecSchema } from './generate-secret.schemas.ts';
|
|
||||||
|
|
||||||
const generateSecretDefinition = createCustomResourceDefinition({
|
|
||||||
group: GROUP,
|
|
||||||
version: 'v1',
|
|
||||||
kind: 'GenerateSecret',
|
|
||||||
names: {
|
|
||||||
plural: 'generate-secrets',
|
|
||||||
singular: 'generate-secret',
|
|
||||||
},
|
|
||||||
spec: generateSecretSpecSchema,
|
|
||||||
create: (options) => new GenerateSecretResource(options),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { generateSecretDefinition };
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user