mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1259e3e752 | ||
|
|
03e406322f | ||
|
|
5ee7a76443 |
@@ -5,5 +5,6 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
environment: '{{ .Values.environment }}'
|
environment: '{{ .Values.environment }}'
|
||||||
redirectUris:
|
redirectUris:
|
||||||
- url: https://localhost:3000/api/v1/authentik/oauth2/callback
|
- path: /api/auth/oidc/callback
|
||||||
|
subdomain: bytestash
|
||||||
matchingMode: strict
|
matchingMode: strict
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ metadata:
|
|||||||
name: '{{ .Release.Name }}'
|
name: '{{ .Release.Name }}'
|
||||||
spec:
|
spec:
|
||||||
environment: '{{ .Values.environment }}'
|
environment: '{{ .Values.environment }}'
|
||||||
subdomain: '{{ .Values.subdomain }}-external'
|
subdomain: '{{ .Values.subdomain }}'
|
||||||
destination:
|
destination:
|
||||||
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||||
port:
|
port:
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ spec:
|
|||||||
name: '{{ .Release.Name }}-client'
|
name: '{{ .Release.Name }}-client'
|
||||||
key: configuration
|
key: configuration
|
||||||
|
|
||||||
# !! IMPORTANT !!
|
|
||||||
# You MUST update this Redirect URI to match your external URL.
|
|
||||||
# This URI must also be configured in your Authentik provider settings for this client.
|
|
||||||
#- name: BS_OIDC_REDIRECT_URI
|
|
||||||
#value: 'https://bytestash.your-domain.com/login/oauth2/code/oidc'
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /data/snippets
|
- mountPath: /data/snippets
|
||||||
name: bytestash-data
|
name: bytestash-data
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
environment: dev
|
environment: prod
|
||||||
subdomain: bytestash
|
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.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.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 }}'
|
||||||
6
charts/apps/jellyfin/values.yaml
Normal file
6
charts/apps/jellyfin/values.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
image:
|
||||||
|
repository: docker.io/jellyfin/jellyfin
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
environment: prod
|
||||||
|
subdomain: jellyfin
|
||||||
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
|
||||||
52
charts/apps/ollama/templates/deployment.yaml
Normal file
52
charts/apps/ollama/templates/deployment.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}'
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ollama
|
||||||
|
image: ghcr.io/ollama/ollama:latest # Official image
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 11434
|
||||||
|
name: http
|
||||||
|
volumeMounts:
|
||||||
|
- name: ollama-data
|
||||||
|
mountPath: /root/.ollama
|
||||||
|
env:
|
||||||
|
# If you want to pre‑start a model, set this env var to the
|
||||||
|
# model name (e.g., "gpt-4o-mini"). The container will download
|
||||||
|
# it automatically at startup.
|
||||||
|
# - name: OLLAMA_MODEL
|
||||||
|
# value: "gpt-4o-mini"
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
scheme: HTTP
|
||||||
|
path: /api/status
|
||||||
|
port: 11434
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
successThreshold: 1
|
||||||
|
failureThreshold: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 1Gi
|
||||||
|
limits:
|
||||||
|
cpu: 2000m
|
||||||
|
memory: 4Gi
|
||||||
|
volumes:
|
||||||
|
- name: ollama-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 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: '{{ .Release.Name }}-data'
|
||||||
|
spec:
|
||||||
|
storageClassName: '{{ .Values.environment }}'
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 20Gi
|
||||||
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: LoadBalancer # Set to NodePort/ClusterIP if you prefer
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 11434
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
selector:
|
||||||
|
app: '{{ .Release.Name }}'
|
||||||
2
charts/apps/ollama/values.yaml
Normal file
2
charts/apps/ollama/values.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
environment: dev
|
||||||
|
subdomain: bytestash
|
||||||
0
charts/apps/values.yaml
Normal file
0
charts/apps/values.yaml
Normal file
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
|
||||||
32
charts/root/templates/apps.yaml
Normal file
32
charts/root/templates/apps.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: ApplicationSet
|
||||||
|
metadata:
|
||||||
|
name: homelab-apps
|
||||||
|
namespace: '{{ .Values.env }}-argo'
|
||||||
|
spec:
|
||||||
|
generators:
|
||||||
|
- directory:
|
||||||
|
path: charts/apps
|
||||||
|
recurse: false
|
||||||
|
include: '.*'
|
||||||
|
filter: |
|
||||||
|
.is_dir == true and (.path | endswith(".disabled") | not)
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: '{{path.basename}}'
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: '{{ .Values.repo }}'
|
||||||
|
targetRevision: '{{ .Values.ref }}'
|
||||||
|
path: charts/apps/{{path.basename}}
|
||||||
|
helm:
|
||||||
|
values: |
|
||||||
|
global: {{ .Values.global | 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
|
||||||
30
charts/volumes/templates/books-pvc.yaml
Normal file
30
charts/volumes/templates/books-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: books
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual
|
||||||
|
hostPath: null
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.books.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
readOnly: true
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: books
|
||||||
|
spec:
|
||||||
|
storageClassName: manual
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
30
charts/volumes/templates/movies-pvc.yaml
Normal file
30
charts/volumes/templates/movies-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: movies
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual
|
||||||
|
hostPath: null
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.movies.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
readOnly: true
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: movies
|
||||||
|
spec:
|
||||||
|
storageClassName: manual
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
30
charts/volumes/templates/music-pvc.yaml
Normal file
30
charts/volumes/templates/music-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: music
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual
|
||||||
|
hostPath: null
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.music.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
readOnly: true
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: music
|
||||||
|
spec:
|
||||||
|
storageClassName: manual
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
30
charts/volumes/templates/podcasts-pvc.yaml
Normal file
30
charts/volumes/templates/podcasts-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: podcasts
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual
|
||||||
|
hostPath: null
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.podcasts.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
readOnly: true
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: podcasts
|
||||||
|
spec:
|
||||||
|
storageClassName: manual
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
30
charts/volumes/templates/tv-pvc.yaml
Normal file
30
charts/volumes/templates/tv-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: tvshows
|
||||||
|
labels:
|
||||||
|
type: nfs
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 10Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: manual
|
||||||
|
hostPath: null
|
||||||
|
nfs:
|
||||||
|
path: '{{ .Values.tvshows.path }}'
|
||||||
|
server: '{{ .Values.host }}'
|
||||||
|
readOnly: true
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: tvshows
|
||||||
|
spec:
|
||||||
|
storageClassName: manual
|
||||||
|
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
|
||||||
@@ -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
|
||||||
spec:
|
spec:
|
||||||
domain: one.dev.olsen.cloud
|
domain: olsen.cloud
|
||||||
networkIp: 192.168.107.2
|
networkIp: 192.168.20.180
|
||||||
tls:
|
tls:
|
||||||
issuer: lets-encrypt-prod
|
issuer: lets-encrypt-prod
|
||||||
|
|||||||
@@ -4,16 +4,10 @@ metadata:
|
|||||||
name: homelab-operator
|
name: homelab-operator
|
||||||
|
|
||||||
build:
|
build:
|
||||||
# This tells Skaffold to build the image locally using your Docker daemon.
|
cluster: {}
|
||||||
local:
|
|
||||||
push: false
|
|
||||||
# This is the crucial part for your workflow. Instead of pushing to a
|
|
||||||
# registry, it loads the built image directly into your cluster's nodes.
|
|
||||||
# load: true
|
|
||||||
artifacts:
|
artifacts:
|
||||||
# Defines the image to build. It matches the placeholder in deployment.yaml.
|
|
||||||
- image: homelaboperator
|
- image: homelaboperator
|
||||||
context: . # The build context is the root directory
|
context: .
|
||||||
docker:
|
docker:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class RepoService {
|
|||||||
#istio: HelmRepo;
|
#istio: HelmRepo;
|
||||||
#authentik: HelmRepo;
|
#authentik: HelmRepo;
|
||||||
#cloudflare: HelmRepo;
|
#cloudflare: HelmRepo;
|
||||||
|
#argo: HelmRepo;
|
||||||
|
|
||||||
constructor(services: Services) {
|
constructor(services: Services) {
|
||||||
const resourceService = services.get(ResourceService);
|
const resourceService = services.get(ResourceService);
|
||||||
@@ -16,11 +17,13 @@ class RepoService {
|
|||||||
this.#istio = resourceService.get(HelmRepo, 'istio', NAMESPACE);
|
this.#istio = resourceService.get(HelmRepo, 'istio', NAMESPACE);
|
||||||
this.#authentik = resourceService.get(HelmRepo, 'authentik', NAMESPACE);
|
this.#authentik = resourceService.get(HelmRepo, 'authentik', NAMESPACE);
|
||||||
this.#cloudflare = resourceService.get(HelmRepo, 'cloudflare', NAMESPACE);
|
this.#cloudflare = resourceService.get(HelmRepo, 'cloudflare', NAMESPACE);
|
||||||
|
this.#argo = resourceService.get(HelmRepo, 'argo', NAMESPACE);
|
||||||
|
|
||||||
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.#cloudflare.on('changed', this.ensure);
|
this.#cloudflare.on('changed', this.ensure);
|
||||||
|
this.#argo.on('changed', this.ensure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get jetstack() {
|
public get jetstack() {
|
||||||
@@ -39,6 +42,10 @@ class RepoService {
|
|||||||
return this.#cloudflare;
|
return this.#cloudflare;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get argo() {
|
||||||
|
return this.#argo;
|
||||||
|
}
|
||||||
|
|
||||||
public ensure = async () => {
|
public ensure = async () => {
|
||||||
await this.#jetstack.set({
|
await this.#jetstack.set({
|
||||||
url: 'https://charts.jetstack.io',
|
url: 'https://charts.jetstack.io',
|
||||||
@@ -55,6 +62,10 @@ class RepoService {
|
|||||||
await this.#cloudflare.set({
|
await this.#cloudflare.set({
|
||||||
url: 'https://cloudflare.github.io/helm-charts',
|
url: 'https://cloudflare.github.io/helm-charts',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.#argo.set({
|
||||||
|
url: 'https://argoproj.github.io/argo-helm',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import { generateRandomHexPass } from '#utils/secrets.ts';
|
|||||||
import { Service } from '#resources/core/service/service.ts';
|
import { Service } from '#resources/core/service/service.ts';
|
||||||
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
|
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
|
||||||
import { RepoService } from '#bootstrap/repos/repos.ts';
|
import { RepoService } from '#bootstrap/repos/repos.ts';
|
||||||
import { VirtualService } from '#resources/istio/virtual-service/virtual-service.ts';
|
|
||||||
import { DestinationRule } from '#resources/istio/destination-rule/destination-rule.ts';
|
import { DestinationRule } from '#resources/istio/destination-rule/destination-rule.ts';
|
||||||
import { NotReadyError } from '#utils/errors.ts';
|
import { NotReadyError } from '#utils/errors.ts';
|
||||||
|
import { ExternalHttpService } from '../external-http-service.ts/external-http-service.ts';
|
||||||
|
|
||||||
const specSchema = z.object({
|
const specSchema = z.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@@ -44,7 +44,7 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
|
|||||||
#initSecret: Secret<InitSecretData>;
|
#initSecret: Secret<InitSecretData>;
|
||||||
#service: Service;
|
#service: Service;
|
||||||
#helmRelease: HelmRelease;
|
#helmRelease: HelmRelease;
|
||||||
#virtualService: VirtualService;
|
#externalHttpService: ExternalHttpService;
|
||||||
#destinationRule: DestinationRule;
|
#destinationRule: DestinationRule;
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
||||||
@@ -68,11 +68,10 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
|
|||||||
this.#helmRelease = resourceService.get(HelmRelease, this.name, this.namespace);
|
this.#helmRelease = resourceService.get(HelmRelease, this.name, this.namespace);
|
||||||
this.#helmRelease.on('changed', this.queueReconcile);
|
this.#helmRelease.on('changed', this.queueReconcile);
|
||||||
|
|
||||||
this.#virtualService = resourceService.get(VirtualService, this.name, this.namespace);
|
|
||||||
this.#virtualService.on('changed', this.queueReconcile);
|
|
||||||
|
|
||||||
this.#destinationRule = resourceService.get(DestinationRule, this.name, this.namespace);
|
this.#destinationRule = resourceService.get(DestinationRule, this.name, this.namespace);
|
||||||
this.#destinationRule.on('changed', this.queueReconcile);
|
this.#destinationRule.on('changed', this.queueReconcile);
|
||||||
|
|
||||||
|
this.#externalHttpService = resourceService.get(ExternalHttpService, this.name, this.namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get service() {
|
public get service() {
|
||||||
@@ -254,18 +253,13 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const gateway = this.#environment.current.gateway;
|
await this.#externalHttpService.ensure({
|
||||||
await this.#virtualService.set({
|
|
||||||
metadata: {
|
metadata: {
|
||||||
ownerReferences: [this.ref],
|
ownerReferences: [this.ref],
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
gateways: [`${gateway.namespace}/${gateway.name}`, 'mesh'],
|
environment: this.spec.environment,
|
||||||
hosts: [domain],
|
subdomain: this.spec.subdomain || 'authentik',
|
||||||
http: [
|
|
||||||
{
|
|
||||||
route: [
|
|
||||||
{
|
|
||||||
destination: {
|
destination: {
|
||||||
host: this.#service.hostname,
|
host: this.#service.hostname,
|
||||||
port: {
|
port: {
|
||||||
@@ -273,10 +267,6 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { RepoService } from '#bootstrap/repos/repos.ts';
|
|||||||
import { Secret } from '#resources/core/secret/secret.ts';
|
import { Secret } from '#resources/core/secret/secret.ts';
|
||||||
import { NotReadyError } from '#utils/errors.ts';
|
import { NotReadyError } from '#utils/errors.ts';
|
||||||
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
|
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
|
||||||
|
import { CloudflareService } from '#services/cloudflare/cloudflare.ts';
|
||||||
|
|
||||||
const specSchema = z.object({});
|
const specSchema = z.object({});
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
|
|||||||
|
|
||||||
#helmRelease: HelmRelease;
|
#helmRelease: HelmRelease;
|
||||||
#secret: Secret<SecretData>;
|
#secret: Secret<SecretData>;
|
||||||
|
#cloudflareService;
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -40,6 +42,9 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
|
|||||||
this.#helmRelease = resourceService.get(HelmRelease, this.name, namespace);
|
this.#helmRelease = resourceService.get(HelmRelease, this.name, namespace);
|
||||||
this.#secret = resourceService.get(Secret<SecretData>, 'cloudflare', namespace);
|
this.#secret = resourceService.get(Secret<SecretData>, 'cloudflare', namespace);
|
||||||
this.#secret.on('changed', this.queueReconcile);
|
this.#secret.on('changed', this.queueReconcile);
|
||||||
|
|
||||||
|
this.#cloudflareService = this.services.get(CloudflareService);
|
||||||
|
this.#cloudflareService.on('changed', this.queueReconcile);
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleResourceChanged = (resource: Resource<ExpectedAny>) => {
|
#handleResourceChanged = (resource: Resource<ExpectedAny>) => {
|
||||||
@@ -60,6 +65,18 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
|
|||||||
hostname: rule?.hostname,
|
hostname: rule?.hostname,
|
||||||
service: `http://${rule?.destination.host}:${rule?.destination.port.number}`,
|
service: `http://${rule?.destination.host}:${rule?.destination.port.number}`,
|
||||||
}));
|
}));
|
||||||
|
if (this.#cloudflareService.ready) {
|
||||||
|
for (const route of ingress) {
|
||||||
|
if (!route.hostname) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.#cloudflareService.ensureTunnel(route.hostname);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
await this.#helmRelease.ensure({
|
await this.#helmRelease.ensure({
|
||||||
metadata: {
|
metadata: {
|
||||||
ownerReferences: [this.ref],
|
ownerReferences: [this.ref],
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { Gateway } from '#resources/istio/gateway/gateway.ts';
|
|||||||
import { NotReadyError } from '#utils/errors.ts';
|
import { NotReadyError } from '#utils/errors.ts';
|
||||||
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
|
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
|
||||||
import { CloudflareService } from '#services/cloudflare/cloudflare.ts';
|
import { CloudflareService } from '#services/cloudflare/cloudflare.ts';
|
||||||
|
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
|
||||||
|
import { RepoService } from '#bootstrap/repos/repos.ts';
|
||||||
|
|
||||||
const specSchema = z.object({
|
const specSchema = z.object({
|
||||||
domain: z.string(),
|
domain: z.string(),
|
||||||
@@ -37,6 +39,8 @@ class Environment extends CustomResource<typeof specSchema> {
|
|||||||
#redisServer: RedisServer;
|
#redisServer: RedisServer;
|
||||||
#authentikServer: AuthentikServer;
|
#authentikServer: AuthentikServer;
|
||||||
#cloudflareService: CloudflareService;
|
#cloudflareService: CloudflareService;
|
||||||
|
#argoRelease: HelmRelease;
|
||||||
|
#argoNamespace: Namespace;
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -67,6 +71,11 @@ class Environment extends CustomResource<typeof specSchema> {
|
|||||||
|
|
||||||
this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, homelabNamespace);
|
this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, homelabNamespace);
|
||||||
this.#authentikServer.on('changed', this.queueReconcile);
|
this.#authentikServer.on('changed', this.queueReconcile);
|
||||||
|
|
||||||
|
this.#argoNamespace = resourceService.get(Namespace, `${this.name}-argo`);
|
||||||
|
|
||||||
|
this.#argoRelease = resourceService.get(HelmRelease, `${this.name}-argo`, homelabNamespace);
|
||||||
|
this.#argoRelease.on('changed', this.queueReconcile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get certificate() {
|
public get certificate() {
|
||||||
@@ -99,27 +108,6 @@ class Environment extends CustomResource<typeof specSchema> {
|
|||||||
throw new NotReadyError('InvalidSpec');
|
throw new NotReadyError('InvalidSpec');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#cloudflareService.ready && spec.networkIp) {
|
|
||||||
const client = this.#cloudflareService.client;
|
|
||||||
const zones = await client.zones.list({
|
|
||||||
name: spec.domain,
|
|
||||||
});
|
|
||||||
const [zone] = zones.result;
|
|
||||||
if (!zone) {
|
|
||||||
throw new NotReadyError('NoZoneFound');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingRecords = await client.dns.records.list({
|
|
||||||
zone_id: zone.id,
|
|
||||||
name: {
|
|
||||||
exact: `*.${spec.domain}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Cloudflare records', existingRecords);
|
|
||||||
|
|
||||||
// zones.result[0].
|
|
||||||
}
|
|
||||||
await this.#namespace.ensure({
|
await this.#namespace.ensure({
|
||||||
metadata: {
|
metadata: {
|
||||||
labels: {
|
labels: {
|
||||||
@@ -208,6 +196,29 @@ class Environment extends CustomResource<typeof specSchema> {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.#argoNamespace.ensure({});
|
||||||
|
|
||||||
|
const repoService = this.services.get(RepoService);
|
||||||
|
await this.#argoRelease.ensure({
|
||||||
|
spec: {
|
||||||
|
targetNamespace: this.#argoNamespace.name,
|
||||||
|
interval: '1h',
|
||||||
|
values: {},
|
||||||
|
chart: {
|
||||||
|
spec: {
|
||||||
|
chart: 'argo-cd',
|
||||||
|
version: '3.9.0',
|
||||||
|
sourceRef: {
|
||||||
|
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
||||||
|
kind: 'HelmRepository',
|
||||||
|
name: repoService.argo.name,
|
||||||
|
namespace: repoService.argo.namespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const specSchema = z.object({
|
|||||||
clientType: z.enum(ClientTypeEnum).optional(),
|
clientType: z.enum(ClientTypeEnum).optional(),
|
||||||
redirectUris: z.array(
|
redirectUris: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
url: z.string(),
|
subdomain: z.string(),
|
||||||
|
path: z.string(),
|
||||||
matchingMode: z.enum(['strict', 'regex']),
|
matchingMode: z.enum(['strict', 'regex']),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -98,8 +99,14 @@ class OIDCClient extends CustomResource<typeof specSchema> {
|
|||||||
token: authentikSecret.token,
|
token: authentikSecret.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const redirectUris = this.spec.redirectUris.map((uri) => ({
|
||||||
|
matchingMode: uri.matchingMode,
|
||||||
|
url: new URL(uri.path, `https://${uri.subdomain}.${this.#environment.current?.spec?.domain}`).toString(),
|
||||||
|
}));
|
||||||
|
|
||||||
await authentikServer.upsertClient({
|
await authentikServer.upsertClient({
|
||||||
...this.spec,
|
...this.spec,
|
||||||
|
redirectUris,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
secret: secret.clientSecret,
|
secret: secret.clientSecret,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,55 @@ class CloudflareService extends EventEmitter<CloudflareServiceEvents> {
|
|||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ensureTunnel = async (route: string) => {
|
||||||
|
const secret = this.#secret.value;
|
||||||
|
if (!secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = this.client;
|
||||||
|
const domainParts = route.split('.');
|
||||||
|
const cname = `${secret.tunnelId}.cfargotunnel.com`;
|
||||||
|
const tld = domainParts.pop();
|
||||||
|
const root = domainParts.pop();
|
||||||
|
const zoneName = `${root}.${tld}`;
|
||||||
|
const name = domainParts.join('.');
|
||||||
|
|
||||||
|
const zones = await client.zones.list({
|
||||||
|
name: zoneName,
|
||||||
|
});
|
||||||
|
const [zone] = zones.result;
|
||||||
|
if (!zone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const records = await client.dns.records.list({
|
||||||
|
zone_id: zone.id,
|
||||||
|
name: {
|
||||||
|
exact: route,
|
||||||
|
},
|
||||||
|
type: 'CNAME',
|
||||||
|
});
|
||||||
|
const [record] = records.result;
|
||||||
|
if (record) {
|
||||||
|
await client.dns.records.edit(record.id, {
|
||||||
|
zone_id: zone.id,
|
||||||
|
type: 'CNAME',
|
||||||
|
content: cname,
|
||||||
|
name: name,
|
||||||
|
ttl: 1,
|
||||||
|
proxied: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await client.dns.records.create({
|
||||||
|
zone_id: zone.id,
|
||||||
|
type: 'CNAME',
|
||||||
|
content: cname,
|
||||||
|
name: name,
|
||||||
|
ttl: 1,
|
||||||
|
proxied: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { CloudflareService };
|
export { CloudflareService };
|
||||||
|
|||||||
Reference in New Issue
Block a user