mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12c74dadb8 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -33,6 +33,4 @@ 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 +0,0 @@
|
|||||||
3.13
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM node:23-slim
|
FROM node:23-alpine
|
||||||
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 --docker" # --mount ${PWD}/data:/data:w
|
colima start --network-address --kubernetes -m 8 --k3s-arg="--disable=helm-controller,local-storage,traefik" # --mount ${PWD}/data:/data:w
|
||||||
flux install --components="source-controller,helm-controller"
|
flux install --components="source-controller,helm-controller"
|
||||||
|
|
||||||
setup-flux:
|
setup-flux:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## Bootstrap repo
|
||||||
|
|
||||||
|
```
|
||||||
|
brew install fluxcd/tap/flux
|
||||||
|
make setup-server
|
||||||
|
```
|
||||||
|
|||||||
1
TODO.md
Normal file
1
TODO.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- Fix issue with incompatible spec breaking the server
|
||||||
19
cert-issuer.yaml
Normal file
19
cert-issuer.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: audiobookshelf
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/advplyr/audiobookshelf
|
|
||||||
tag: 2.26.1
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
subdomain: audiobookshelf
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: ByteStash
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-data'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
subdomain: bytestash
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: gitea
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
|
||||||
kind: OidcClient
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}'
|
|
||||||
spec:
|
|
||||||
environment: '{{ .Values.globals.environment }}'
|
|
||||||
redirectUris:
|
|
||||||
- path: /user/oauth2/Authentik/callback
|
|
||||||
subdomain: '{{ .Values.subdomain }}'
|
|
||||||
matchingMode: strict
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
|
||||||
kind: PostgresDatabase
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}'
|
|
||||||
spec:
|
|
||||||
environment: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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: 3000
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /data
|
|
||||||
name: data
|
|
||||||
env:
|
|
||||||
- name: TZ
|
|
||||||
value: '{{ .Values.globals.timezone }}'
|
|
||||||
- name: USER_UID
|
|
||||||
value: '1000'
|
|
||||||
- name: USER_GID
|
|
||||||
value: '1000'
|
|
||||||
- name: GITEA__service__REQUIRE_EXTERNAL_REGISTRATION_PASSWORD
|
|
||||||
value: 'true'
|
|
||||||
- name: GITEA__service__ENABLE_BASIC_AUTHENTICATION
|
|
||||||
value: 'true'
|
|
||||||
- name: GITEA__service__ENABLE_PASSWORD_SIGNIN_FORM
|
|
||||||
value: 'false'
|
|
||||||
- name: GITEA__service__DEFAULT_KEEP_EMAIL_PRIVATE
|
|
||||||
value: 'true'
|
|
||||||
- name: GITEA__service__DEFAULT_USER_IS_RESTRICTED
|
|
||||||
value: 'true'
|
|
||||||
- name: GITEA__service__DEFAULT_USER_VISIBILITY
|
|
||||||
value: 'private'
|
|
||||||
- name: GITEA__service__DEFAULT_ORG_VISIBILITY
|
|
||||||
value: 'private'
|
|
||||||
- name: GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION
|
|
||||||
value: 'true'
|
|
||||||
- name: GITEA__other__SHOW_FOOTER_POWERED_BY
|
|
||||||
value: 'false'
|
|
||||||
- name: GITEA__other__SHOW_FOOTER_TEMPLATE_LOAD_TIME
|
|
||||||
value: 'false'
|
|
||||||
- name: GITEA__other__SHOW_FOOTER_VERSION
|
|
||||||
value: 'false'
|
|
||||||
- name: GITEA__repository__ENABLE_PUSH_CREATE_USER
|
|
||||||
value: 'true'
|
|
||||||
- name: GITEA__repository__ENABLE_PUSH_CREATE_ORG
|
|
||||||
value: 'true'
|
|
||||||
- name: GITEA__openid__ENABLE_OPENID_SIGNIN
|
|
||||||
value: 'false'
|
|
||||||
- name: GITEA__openid__ENABLE_OPENID_SIGNUP
|
|
||||||
value: 'false'
|
|
||||||
- name: GITEA__database__DB_TYPE
|
|
||||||
value: postgres
|
|
||||||
- name: GITEA__database__NAME
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: database
|
|
||||||
- name: GITEA__database__HOST
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: host
|
|
||||||
- name: GITEA__database__USER
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: user
|
|
||||||
- name: GITEA__database__PASSWD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: password
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: '{{ .Release.Name }}-data'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-data'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}'
|
|
||||||
labels:
|
|
||||||
app: '{{ .Release.Name }}'
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 3000
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
app: '{{ .Release.Name }}'
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
timezone: Europe/Amsterdam
|
|
||||||
image:
|
|
||||||
repository: docker.gitea.com/gitea
|
|
||||||
tag: latest
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
subdomain: gitea
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: Jellyfin
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
https://www.authelia.com/integration/openid-connect/clients/jellyfin/
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-config'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.environment }}'
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
image:
|
|
||||||
repository: docker.io/jellyfin/jellyfin
|
|
||||||
tag: latest
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
subdomain: jellyfin
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: metamcp
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
|
||||||
kind: PostgresDatabase
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}'
|
|
||||||
spec:
|
|
||||||
environment: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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: 12008
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /data
|
|
||||||
name: data
|
|
||||||
env:
|
|
||||||
- name: TZ
|
|
||||||
value: '{{ .Values.globals.timezone }}'
|
|
||||||
- name: APP_URL
|
|
||||||
value: https://metamcp.olsen.cloud # TODO: Change
|
|
||||||
- name: NEXT_PUBLIC_APP_URL
|
|
||||||
value: https://metamcp.olsen.cloud # TODO: Change
|
|
||||||
- name: BETTER_AUTH_SECRET
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-secrets'
|
|
||||||
key: betterauth
|
|
||||||
- name: DATABASE_URL
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: url
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: database
|
|
||||||
- name: POSTGRES_HOST
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: host
|
|
||||||
- name: POSTGRES_PORT
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: port
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: user
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: '{{ .Release.Name }}-pg-connection'
|
|
||||||
key: password
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: '{{ .Release.Name }}-data'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-data'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
|
||||||
kind: GenerateSecret
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-secrets'
|
|
||||||
spec:
|
|
||||||
fields:
|
|
||||||
- name: betterauth
|
|
||||||
encoding: base64
|
|
||||||
length: 64
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}'
|
|
||||||
labels:
|
|
||||||
app: '{{ .Release.Name }}'
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 12008
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
app: '{{ .Release.Name }}'
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
timezone: Europe/Amsterdam
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/metatool-ai/metamcp
|
|
||||||
tag: latest
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
subdomain: metamcp
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: ByteStash
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-data'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
subdomain: miniflux
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: Jellyfin
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
|
||||||
kind: PostgresDatabase
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}'
|
|
||||||
spec:
|
|
||||||
environment: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-data'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
timezone: Europe/Amsterdam
|
|
||||||
image:
|
|
||||||
repository: docker.n8n.io/n8nio/n8n
|
|
||||||
tag: latest
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
subdomain: n8n
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: ollama
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-data'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
image:
|
|
||||||
repository: ollama/ollama
|
|
||||||
tag: 0.11.8
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
subdomain: openwebui
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: openwebui
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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'
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: '{{ .Release.Name }}-data'
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- 'ReadWriteOnce'
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: '1Gi'
|
|
||||||
storageClassName: '{{ .Values.globals.environment }}'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
globals:
|
|
||||||
environment: prod
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/open-webui/open-webui
|
|
||||||
tag: main
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
subdomain: openwebui
|
|
||||||
@@ -6,9 +6,6 @@ rules:
|
|||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["secrets"]
|
resources: ["secrets"]
|
||||||
verbs: ["create", "get", "watch", "list"]
|
verbs: ["create", "get", "watch", "list"]
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["namespaces"]
|
|
||||||
verbs: ["get", "list", "watch", "create", "update", "patch"]
|
|
||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["persistentvolumes"]
|
resources: ["persistentvolumes"]
|
||||||
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
|
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
|
||||||
@@ -26,7 +23,7 @@ rules:
|
|||||||
verbs: ["get", "list", "watch"]
|
verbs: ["get", "list", "watch"]
|
||||||
- apiGroups: ["*"]
|
- apiGroups: ["*"]
|
||||||
resources: ["*"]
|
resources: ["*"]
|
||||||
verbs: ["get", "watch", "list", "patch", "create", "update", "replace"]
|
verbs: ["get", "watch", "list", "patch"]
|
||||||
- apiGroups: ["apiextensions.k8s.io"]
|
- apiGroups: ["apiextensions.k8s.io"]
|
||||||
resources: ["customresourcedefinitions"]
|
resources: ["customresourcedefinitions"]
|
||||||
verbs: ["get", "create", "update", "replace", "patch"]
|
verbs: ["get", "create", "replace"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
image:
|
image:
|
||||||
repository: ghcr.io/morten-olsen/homelab-operator
|
repository: ghcr.io/morten-olsen/homelab-operator
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: Always
|
||||||
# Overrides the image tag whose default is the chart appVersion.
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
tag: main
|
tag: main
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: root
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
globals:
|
|
||||||
env: prod
|
|
||||||
repo: https://github.com/morten-olsen/homelab-operator.git
|
|
||||||
ref: HEAD
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
version: 1.0.0
|
|
||||||
name: Resources
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
12
docker-compose.dev.yaml
Normal file
12
docker-compose.dev.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: homelab
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: $POSTGRES_USER
|
||||||
|
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-postgres}
|
||||||
|
volumes:
|
||||||
|
- $PWD/.data/local/postgres:/var/lib/postgresql/data
|
||||||
901
docs/writing-custom-resources.md
Normal file
901
docs/writing-custom-resources.md
Normal file
@@ -0,0 +1,901 @@
|
|||||||
|
# Writing Custom Resources
|
||||||
|
|
||||||
|
This guide explains how to create and implement custom resources in the
|
||||||
|
homelab-operator.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Custom resources in this operator follow a structured pattern that includes:
|
||||||
|
|
||||||
|
- **Specification schemas** using Zod for runtime validation
|
||||||
|
- **Resource implementations** that extend the base `CustomResource` class
|
||||||
|
- **Manifest creation** helpers for generating Kubernetes resources
|
||||||
|
- **Reconciliation logic** to manage the desired state
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
Each custom resource should be organized in its own directory under
|
||||||
|
`src/custom-resouces/` with the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/custom-resouces/{resource-name}/
|
||||||
|
├── {resource-name}.ts # Main definition file
|
||||||
|
├── {resource-name}.schemas.ts # Zod validation schemas
|
||||||
|
├── {resource-name}.resource.ts # Resource implementation
|
||||||
|
└── {resource-name}.create-manifests.ts # Manifest generation helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
This section walks through creating a complete custom resource from scratch.
|
||||||
|
We'll build a `MyResource` that manages a web application with a deployment and
|
||||||
|
service.
|
||||||
|
|
||||||
|
### 1. Define Your Resource
|
||||||
|
|
||||||
|
The main definition file registers your custom resource with the operator
|
||||||
|
framework. This file serves as the entry point that ties together your schemas,
|
||||||
|
implementation, and Kubernetes CRD definition.
|
||||||
|
|
||||||
|
Create the main definition file (`{resource-name}.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createCustomResourceDefinition } from "../../services/custom-resources/custom-resources.ts";
|
||||||
|
import { GROUP } from "../../utils/consts.ts";
|
||||||
|
|
||||||
|
import { MyResourceResource } from "./my-resource.resource.ts";
|
||||||
|
import { myResourceSpecSchema } from "./my-resource.schemas.ts";
|
||||||
|
|
||||||
|
const myResourceDefinition = createCustomResourceDefinition({
|
||||||
|
group: GROUP, // Uses your operator's API group (homelab.mortenolsen.pro)
|
||||||
|
version: "v1", // API version for this resource
|
||||||
|
kind: "MyResource", // The Kubernetes kind name (PascalCase)
|
||||||
|
names: {
|
||||||
|
plural: "myresources", // Plural name for kubectl (lowercase)
|
||||||
|
singular: "myresource", // Singular name for kubectl (lowercase)
|
||||||
|
},
|
||||||
|
spec: myResourceSpecSchema, // Zod schema for validation
|
||||||
|
create: (options) => new MyResourceResource(options), // Factory function
|
||||||
|
});
|
||||||
|
|
||||||
|
export { myResourceDefinition };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Points:**
|
||||||
|
|
||||||
|
- The `group` should always use the `GROUP` constant to maintain consistency
|
||||||
|
- `kind` should be descriptive and follow Kubernetes naming conventions
|
||||||
|
(PascalCase)
|
||||||
|
- `names.plural` is used in kubectl commands (`kubectl get myresources`)
|
||||||
|
- The `create` function instantiates your resource implementation when a CR is
|
||||||
|
detected
|
||||||
|
|
||||||
|
### 2. Create Validation Schemas
|
||||||
|
|
||||||
|
Schemas define the structure and validation rules for your custom resource's
|
||||||
|
specification. Using Zod provides runtime type safety and automatic validation
|
||||||
|
of user input.
|
||||||
|
|
||||||
|
Define your spec schema (`{resource-name}.schemas.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const myResourceSpecSchema = z.object({
|
||||||
|
// Required fields - these must be provided by users
|
||||||
|
hostname: z.string(), // Base hostname for the application
|
||||||
|
port: z.number().min(1).max(65535), // Container port (validated range)
|
||||||
|
|
||||||
|
// Optional fields with defaults - provide sensible fallbacks
|
||||||
|
replicas: z.number().min(1).default(1), // Number of pod replicas
|
||||||
|
|
||||||
|
// Enums - restrict to specific values with defaults
|
||||||
|
protocol: z.enum(["http", "https"]).default("https"),
|
||||||
|
|
||||||
|
// Nested objects - for complex configuration
|
||||||
|
database: z.object({
|
||||||
|
host: z.string(), // Database hostname
|
||||||
|
port: z.number(), // Database port
|
||||||
|
name: z.string(), // Database name
|
||||||
|
}).optional(), // Entire database config is optional
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additional schemas for secrets, status, etc.
|
||||||
|
// Separate schemas help organize different data types
|
||||||
|
const myResourceSecretSchema = z.object({
|
||||||
|
apiKey: z.string(), // API key for external services
|
||||||
|
password: z.string(), // Database or service password
|
||||||
|
});
|
||||||
|
|
||||||
|
export { myResourceSecretSchema, myResourceSpecSchema };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schema Design Best Practices:**
|
||||||
|
|
||||||
|
- **Required vs Optional**: Make fields required only when absolutely necessary
|
||||||
|
- **Defaults**: Provide sensible defaults to reduce user configuration burden
|
||||||
|
- **Validation**: Use Zod's built-in validators (`.min()`, `.max()`, `.email()`,
|
||||||
|
etc.)
|
||||||
|
- **Enums**: Restrict values to prevent invalid configurations
|
||||||
|
- **Nested Objects**: Group related configuration together
|
||||||
|
- **Separate Schemas**: Create different schemas for different purposes (spec,
|
||||||
|
secrets, status)
|
||||||
|
|
||||||
|
### 3. Implement the Resource
|
||||||
|
|
||||||
|
The resource implementation is the core of your custom resource. It contains the
|
||||||
|
business logic for managing Kubernetes resources and maintains the desired
|
||||||
|
state. This class extends `CustomResource` and implements the reconciliation
|
||||||
|
logic.
|
||||||
|
|
||||||
|
Create the resource implementation (`{resource-name}.resource.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { KubernetesObject } from "@kubernetes/client-node";
|
||||||
|
import deepEqual from "deep-equal";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomResource,
|
||||||
|
type CustomResourceOptions,
|
||||||
|
type SubresourceResult,
|
||||||
|
} from "../../services/custom-resources/custom-resources.custom-resource.ts";
|
||||||
|
import {
|
||||||
|
ResourceReference,
|
||||||
|
ResourceService,
|
||||||
|
} from "../../services/resources/resources.ts";
|
||||||
|
|
||||||
|
import type { myResourceSpecSchema } from "./my-resource.schemas.ts";
|
||||||
|
import {
|
||||||
|
createDeploymentManifest,
|
||||||
|
createServiceManifest,
|
||||||
|
} from "./my-resource.create-manifests.ts";
|
||||||
|
|
||||||
|
class MyResourceResource extends CustomResource<typeof myResourceSpecSchema> {
|
||||||
|
#deploymentResource = new ResourceReference();
|
||||||
|
#serviceResource = new ResourceReference();
|
||||||
|
|
||||||
|
constructor(options: CustomResourceOptions<typeof myResourceSpecSchema>) {
|
||||||
|
super(options);
|
||||||
|
const resourceService = this.services.get(ResourceService);
|
||||||
|
|
||||||
|
// Initialize resource references
|
||||||
|
this.#deploymentResource.current = resourceService.get({
|
||||||
|
apiVersion: "apps/v1",
|
||||||
|
kind: "Deployment",
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#serviceResource.current = resourceService.get({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Service",
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up event handlers for reconciliation
|
||||||
|
this.#deploymentResource.on("changed", this.queueReconcile);
|
||||||
|
this.#serviceResource.on("changed", this.queueReconcile);
|
||||||
|
}
|
||||||
|
|
||||||
|
#reconcileDeployment = async (): Promise<SubresourceResult> => {
|
||||||
|
const manifest = createDeploymentManifest({
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
ref: this.ref,
|
||||||
|
spec: this.spec,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.#deploymentResource.current?.exists) {
|
||||||
|
await this.#deploymentResource.current?.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: "Creating",
|
||||||
|
message: "Creating deployment",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deepEqual(this.#deploymentResource.current.spec, manifest.spec)) {
|
||||||
|
await this.#deploymentResource.current.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: "Updating",
|
||||||
|
message: "Deployment needs updates",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if deployment is ready
|
||||||
|
const deployment = this.#deploymentResource.current;
|
||||||
|
const isReady =
|
||||||
|
deployment.status?.readyReplicas === deployment.status?.replicas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: isReady,
|
||||||
|
reason: isReady ? "Ready" : "Pending",
|
||||||
|
message: isReady ? "Deployment is ready" : "Waiting for pods to be ready",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#reconcileService = async (): Promise<SubresourceResult> => {
|
||||||
|
const manifest = createServiceManifest({
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
ref: this.ref,
|
||||||
|
spec: this.spec,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deepEqual(this.#serviceResource.current?.spec, manifest.spec)) {
|
||||||
|
await this.#serviceResource.current?.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: "Updating",
|
||||||
|
message: "Service needs updates",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ready: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
public reconcile = async () => {
|
||||||
|
if (!this.exists || this.metadata.deletionTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile subresources
|
||||||
|
await this.reconcileSubresource("Deployment", this.#reconcileDeployment);
|
||||||
|
await this.reconcileSubresource("Service", this.#reconcileService);
|
||||||
|
|
||||||
|
// Update overall ready condition
|
||||||
|
const deploymentReady =
|
||||||
|
this.conditions.get("Deployment")?.status === "True";
|
||||||
|
const serviceReady = this.conditions.get("Service")?.status === "True";
|
||||||
|
|
||||||
|
await this.conditions.set("Ready", {
|
||||||
|
status: deploymentReady && serviceReady ? "True" : "False",
|
||||||
|
reason: deploymentReady && serviceReady ? "Ready" : "Pending",
|
||||||
|
message: deploymentReady && serviceReady
|
||||||
|
? "All resources are ready"
|
||||||
|
: "Waiting for resources to be ready",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MyResourceResource };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resource Implementation Breakdown:**
|
||||||
|
|
||||||
|
**Constructor Setup:**
|
||||||
|
|
||||||
|
- **Resource References**: Create `ResourceReference` objects to track managed
|
||||||
|
Kubernetes resources
|
||||||
|
- **Service Access**: Use dependency injection to access operator services
|
||||||
|
(`ResourceService`)
|
||||||
|
- **Event Handlers**: Listen for changes in managed resources to trigger
|
||||||
|
reconciliation
|
||||||
|
- **Resource Registration**: Register references for Deployment and Service that
|
||||||
|
will be managed
|
||||||
|
|
||||||
|
**Reconciliation Methods:**
|
||||||
|
|
||||||
|
- **`#reconcileDeployment`**: Manages the application's Deployment resource
|
||||||
|
- Creates manifests using helper functions
|
||||||
|
- Checks if resource exists and creates/updates as needed
|
||||||
|
- Uses `deepEqual` to avoid unnecessary updates
|
||||||
|
- Returns status indicating readiness state
|
||||||
|
- **`#reconcileService`**: Manages the Service resource for network access
|
||||||
|
- Similar pattern to deployment but typically simpler
|
||||||
|
- Services are usually ready immediately after creation
|
||||||
|
|
||||||
|
**Main Reconcile Loop:**
|
||||||
|
|
||||||
|
- **Deletion Check**: Early return if resource is being deleted
|
||||||
|
- **Subresource Management**: Calls individual reconciliation methods
|
||||||
|
- **Condition Updates**: Aggregates status from all subresources
|
||||||
|
- **Status Reporting**: Updates the overall "Ready" condition
|
||||||
|
|
||||||
|
**Key Design Patterns:**
|
||||||
|
|
||||||
|
- **Private Methods**: Use `#` for private reconciliation methods
|
||||||
|
- **Async/Await**: All reconciliation is asynchronous
|
||||||
|
- **Resource References**: Track external resources with type safety
|
||||||
|
- **Condition Management**: Provide clear status through Kubernetes conditions
|
||||||
|
- **Event-Driven**: React to changes in managed resources automatically
|
||||||
|
|
||||||
|
### 4. Create Manifest Helpers
|
||||||
|
|
||||||
|
Manifest helpers are pure functions that generate Kubernetes resource
|
||||||
|
definitions. They transform your custom resource's specification into standard
|
||||||
|
Kubernetes objects. This separation keeps your reconciliation logic clean and
|
||||||
|
makes manifests easy to test and modify.
|
||||||
|
|
||||||
|
Define manifest creation functions (`{resource-name}.create-manifests.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type CreateDeploymentManifestOptions = {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
ref: any; // Owner reference
|
||||||
|
spec: {
|
||||||
|
hostname: string;
|
||||||
|
port: number;
|
||||||
|
replicas: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDeploymentManifest = (
|
||||||
|
options: CreateDeploymentManifestOptions,
|
||||||
|
) => ({
|
||||||
|
apiVersion: "apps/v1",
|
||||||
|
kind: "Deployment",
|
||||||
|
metadata: {
|
||||||
|
name: options.name,
|
||||||
|
namespace: options.namespace,
|
||||||
|
ownerReferences: [options.ref],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
replicas: options.spec.replicas,
|
||||||
|
selector: {
|
||||||
|
matchLabels: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
metadata: {
|
||||||
|
labels: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
name: options.name,
|
||||||
|
image: "nginx:latest",
|
||||||
|
ports: [
|
||||||
|
{
|
||||||
|
containerPort: options.spec.port,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
env: [
|
||||||
|
{
|
||||||
|
name: "HOSTNAME",
|
||||||
|
value: options.spec.hostname,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type CreateServiceManifestOptions = {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
ref: any;
|
||||||
|
spec: {
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createServiceManifest = (options: CreateServiceManifestOptions) => ({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Service",
|
||||||
|
metadata: {
|
||||||
|
name: options.name,
|
||||||
|
namespace: options.namespace,
|
||||||
|
ownerReferences: [options.ref],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
selector: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
ports: [
|
||||||
|
{
|
||||||
|
port: 80,
|
||||||
|
targetPort: options.spec.port,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { createDeploymentManifest, createServiceManifest };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manifest Helper Patterns:**
|
||||||
|
|
||||||
|
**Type Definitions:**
|
||||||
|
|
||||||
|
- **Options Types**: Define clear interfaces for function parameters
|
||||||
|
- **Structured Input**: Group related parameters in nested objects
|
||||||
|
- **Type Safety**: Leverage TypeScript to catch configuration errors at compile
|
||||||
|
time
|
||||||
|
|
||||||
|
**Deployment Manifest:**
|
||||||
|
|
||||||
|
- **Owner References**: Ensures garbage collection when parent resource is
|
||||||
|
deleted
|
||||||
|
- **Labels & Selectors**: Consistent labeling for pod selection and organization
|
||||||
|
- **Container Configuration**: Maps custom resource spec to container settings
|
||||||
|
- **Environment Variables**: Passes configuration from spec to running
|
||||||
|
containers
|
||||||
|
- **Port Configuration**: Exposes application ports based on spec
|
||||||
|
|
||||||
|
**Service Manifest:**
|
||||||
|
|
||||||
|
- **Service Discovery**: Creates stable network endpoint for the deployment
|
||||||
|
- **Port Mapping**: Routes external traffic to container ports
|
||||||
|
- **Selector Matching**: Uses same labels as deployment for proper routing
|
||||||
|
- **Owner References**: Links service lifecycle to custom resource
|
||||||
|
|
||||||
|
**Best Practices for Manifest Helpers:**
|
||||||
|
|
||||||
|
- **Pure Functions**: No side effects, same input always produces same output
|
||||||
|
- **Immutable Objects**: Return new objects rather than modifying inputs
|
||||||
|
- **Validation**: Let TypeScript catch type mismatches
|
||||||
|
- **Consistent Naming**: Use predictable patterns for resource names
|
||||||
|
- **Owner References**: Always set for proper cleanup
|
||||||
|
- **Documentation**: Comment non-obvious configuration choices
|
||||||
|
|
||||||
|
### 5. Register Your Resource
|
||||||
|
|
||||||
|
Add your resource to `src/custom-resouces/custom-resources.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { myResourceDefinition } from "./my-resource/my-resource.ts";
|
||||||
|
|
||||||
|
const customResources = [
|
||||||
|
// ... existing resources
|
||||||
|
myResourceDefinition,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
These fundamental patterns are used throughout the operator framework.
|
||||||
|
Understanding them is essential for building robust custom resources.
|
||||||
|
|
||||||
|
### Resource References
|
||||||
|
|
||||||
|
`ResourceReference` objects provide a strongly-typed way to track and manage
|
||||||
|
Kubernetes resources that your custom resource creates or depends on. They
|
||||||
|
automatically handle resource watching, caching, and change notifications.
|
||||||
|
|
||||||
|
Use `ResourceReference` to manage related Kubernetes resources:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
ResourceReference,
|
||||||
|
ResourceService,
|
||||||
|
} from "../../services/resources/resources.ts";
|
||||||
|
|
||||||
|
class MyResource extends CustomResource<typeof myResourceSpecSchema> {
|
||||||
|
#deploymentResource = new ResourceReference();
|
||||||
|
|
||||||
|
constructor(options: CustomResourceOptions<typeof myResourceSpecSchema>) {
|
||||||
|
super(options);
|
||||||
|
const resourceService = this.services.get(ResourceService);
|
||||||
|
|
||||||
|
this.#deploymentResource.current = resourceService.get({
|
||||||
|
apiVersion: "apps/v1",
|
||||||
|
kind: "Deployment",
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
this.#deploymentResource.on("changed", this.queueReconcile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why Resource References Matter:**
|
||||||
|
|
||||||
|
- **Automatic Watching**: Changes to referenced resources trigger reconciliation
|
||||||
|
- **Type Safety**: Get compile-time checking for resource properties
|
||||||
|
- **Lifecycle Management**: Easily check if resources exist and their current
|
||||||
|
state
|
||||||
|
- **Event Handling**: React to external changes without polling
|
||||||
|
- **Caching**: Avoid repeated API calls for the same resource data
|
||||||
|
|
||||||
|
### Conditions
|
||||||
|
|
||||||
|
Kubernetes conditions provide a standardized way to communicate resource status.
|
||||||
|
They follow the Kubernetes convention of expressing current state, reasons for
|
||||||
|
that state, and human-readable messages. Conditions are crucial for operators
|
||||||
|
and users to understand what's happening with resources.
|
||||||
|
|
||||||
|
Use conditions to track the status of your resource:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Set a condition
|
||||||
|
await this.conditions.set("Ready", {
|
||||||
|
status: "True",
|
||||||
|
reason: "AllResourcesReady",
|
||||||
|
message: "All subresources are ready",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get a condition
|
||||||
|
const isReady = this.conditions.get("Ready")?.status === "True";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Condition Best Practices:**
|
||||||
|
|
||||||
|
- **Standard Names**: Use common condition types like "Ready", "Available",
|
||||||
|
"Progressing"
|
||||||
|
- **Clear Status**: Use "True", "False", or "Unknown" following Kubernetes
|
||||||
|
conventions
|
||||||
|
- **Descriptive Reasons**: Provide specific reason codes for troubleshooting
|
||||||
|
- **Helpful Messages**: Include actionable information for users
|
||||||
|
- **Consistent Updates**: Always update conditions during reconciliation
|
||||||
|
|
||||||
|
### Subresource Reconciliation
|
||||||
|
|
||||||
|
The `reconcileSubresource` method provides a standardized way to manage
|
||||||
|
individual components of your custom resource. It automatically handles
|
||||||
|
condition updates, error management, and status aggregation. This pattern keeps
|
||||||
|
your main reconciliation loop clean and ensures consistent error handling.
|
||||||
|
|
||||||
|
Use `reconcileSubresource` to manage individual components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
public reconcile = async () => {
|
||||||
|
// This automatically manages conditions and error handling
|
||||||
|
await this.reconcileSubresource("Deployment", this.#reconcileDeployment);
|
||||||
|
await this.reconcileSubresource("Service", this.#reconcileService);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Subresource Reconciliation Benefits:**
|
||||||
|
|
||||||
|
- **Automatic Condition Management**: Sets conditions based on reconciliation
|
||||||
|
results
|
||||||
|
- **Error Isolation**: Failures in one subresource don't stop others
|
||||||
|
- **Status Aggregation**: Combines individual component status into overall
|
||||||
|
status
|
||||||
|
- **Consistent Patterns**: Same error handling and retry logic across all
|
||||||
|
components
|
||||||
|
- **Observability**: Clear visibility into which components are having issues
|
||||||
|
|
||||||
|
### Deep Equality Checks
|
||||||
|
|
||||||
|
Deep equality checks prevent unnecessary API calls and resource churn.
|
||||||
|
Kubernetes resources should only be updated when their desired state actually
|
||||||
|
differs from their current state. This improves performance and reduces cluster
|
||||||
|
load.
|
||||||
|
|
||||||
|
Use `deepEqual` to avoid unnecessary updates:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import deepEqual from "deep-equal";
|
||||||
|
|
||||||
|
if (!deepEqual(currentResource.spec, desiredManifest.spec)) {
|
||||||
|
await currentResource.patch(desiredManifest);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deep Equality Benefits:**
|
||||||
|
|
||||||
|
- **Performance**: Avoids unnecessary API calls to Kubernetes
|
||||||
|
- **Reduced Churn**: Prevents resource version conflicts and unnecessary events
|
||||||
|
- **Stability**: Reduces reconciliation loops and system noise
|
||||||
|
- **Efficiency**: Lets you focus compute on actual changes
|
||||||
|
- **Observability**: Cleaner audit logs with only meaningful changes
|
||||||
|
|
||||||
|
**When to Use Deep Equality:**
|
||||||
|
|
||||||
|
- **Spec Comparisons**: Before updating any Kubernetes resource
|
||||||
|
- **Status Updates**: Only update status when values actually change
|
||||||
|
- **Metadata Updates**: Check labels and annotations before patching
|
||||||
|
- **Complex Objects**: Especially useful for nested configuration objects
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
These patterns handle more complex scenarios like secret management, resource
|
||||||
|
dependencies, and sophisticated error handling. Use these when building
|
||||||
|
production-ready operators that need to handle real-world complexity.
|
||||||
|
|
||||||
|
### Working with Secrets
|
||||||
|
|
||||||
|
Many resources need to manage secrets. Here's a pattern for secret management:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SecretService } from "../../services/secrets/secrets.ts";
|
||||||
|
|
||||||
|
class MyResource extends CustomResource<typeof myResourceSpecSchema> {
|
||||||
|
constructor(options: CustomResourceOptions<typeof myResourceSpecSchema>) {
|
||||||
|
super(options);
|
||||||
|
const secretService = this.services.get(SecretService);
|
||||||
|
|
||||||
|
// Get or create a secret
|
||||||
|
this.secretRef = secretService.get({
|
||||||
|
name: `${this.name}-secret`,
|
||||||
|
namespace: this.namespace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#ensureSecret = async () => {
|
||||||
|
const secretData = {
|
||||||
|
apiKey: generateApiKey(),
|
||||||
|
password: generatePassword(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.secretRef.current?.exists) {
|
||||||
|
await this.secretRef.current?.patch({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Secret",
|
||||||
|
metadata: {
|
||||||
|
name: this.secretRef.current.name,
|
||||||
|
namespace: this.secretRef.current.namespace,
|
||||||
|
ownerReferences: [this.ref],
|
||||||
|
},
|
||||||
|
data: secretData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-Resource Dependencies
|
||||||
|
|
||||||
|
When your resource depends on other custom resources:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class MyResource extends CustomResource<typeof myResourceSpecSchema> {
|
||||||
|
#dependentResource = new ResourceReference();
|
||||||
|
|
||||||
|
constructor(options: CustomResourceOptions<typeof myResourceSpecSchema>) {
|
||||||
|
super(options);
|
||||||
|
const resourceService = this.services.get(ResourceService);
|
||||||
|
|
||||||
|
// Reference another custom resource
|
||||||
|
this.#dependentResource.current = resourceService.get({
|
||||||
|
apiVersion: "homelab.mortenolsen.pro/v1",
|
||||||
|
kind: "PostgresDatabase",
|
||||||
|
name: this.spec.database,
|
||||||
|
namespace: this.namespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#dependentResource.on("changed", this.queueReconcile);
|
||||||
|
}
|
||||||
|
|
||||||
|
#reconcileApp = async (): Promise<SubresourceResult> => {
|
||||||
|
// Check if dependency is ready
|
||||||
|
const dependency = this.#dependentResource.current;
|
||||||
|
if (!dependency?.exists) {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
failed: true,
|
||||||
|
reason: "MissingDependency",
|
||||||
|
message: `PostgresDatabase ${this.spec.database} not found`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencyReady = dependency.status?.conditions?.find(
|
||||||
|
(c) => c.type === "Ready" && c.status === "True",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dependencyReady) {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
reason: "WaitingForDependency",
|
||||||
|
message:
|
||||||
|
`Waiting for PostgresDatabase ${this.spec.database} to be ready`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with reconciliation...
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
Proper error handling in reconciliation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
#reconcileDeployment = async (): Promise<SubresourceResult> => {
|
||||||
|
try {
|
||||||
|
// Reconciliation logic...
|
||||||
|
return { ready: true };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
failed: true,
|
||||||
|
reason: 'ReconciliationError',
|
||||||
|
message: `Failed to reconcile deployment: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
Once your custom resource is implemented and registered, users can create
|
||||||
|
instances using standard Kubernetes manifests. The operator will automatically
|
||||||
|
detect new resources and begin reconciliation based on your implementation
|
||||||
|
logic.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: MyResource
|
||||||
|
metadata:
|
||||||
|
name: my-app
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
hostname: my-app.example.com
|
||||||
|
port: 8080
|
||||||
|
replicas: 3
|
||||||
|
protocol: https
|
||||||
|
database:
|
||||||
|
host: postgres.default.svc.cluster.local
|
||||||
|
port: 5432
|
||||||
|
name: myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens when this resource is created:**
|
||||||
|
|
||||||
|
1. **Validation**: The operator validates the spec against your Zod schema
|
||||||
|
2. **Resource Creation**: Your `MyResourceResource` class is instantiated
|
||||||
|
3. **Reconciliation**: The operator creates a Deployment with 3 replicas and a
|
||||||
|
Service
|
||||||
|
4. **Status Updates**: Conditions are set to track deployment and service
|
||||||
|
readiness
|
||||||
|
5. **Event Handling**: The operator watches for changes and re-reconciles as
|
||||||
|
needed
|
||||||
|
|
||||||
|
Users can then monitor the resource status with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get myresources my-app -o yaml
|
||||||
|
kubectl describe myresource my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real Examples
|
||||||
|
|
||||||
|
These examples show how the patterns described above are used in practice within
|
||||||
|
the homelab-operator.
|
||||||
|
|
||||||
|
### Simple Resource: Domain
|
||||||
|
|
||||||
|
The `Domain` resource demonstrates a straightforward custom resource that
|
||||||
|
manages external dependencies. It creates and manages TLS certificates through
|
||||||
|
cert-manager and configures Istio gateways for HTTPS traffic routing.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
- Creates a cert-manager Certificate for TLS termination
|
||||||
|
- Configures an Istio Gateway for traffic routing
|
||||||
|
- Manages the lifecycle of both resources through owner references
|
||||||
|
- Provides wildcard certificate support for subdomains
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: Domain
|
||||||
|
metadata:
|
||||||
|
name: homelab
|
||||||
|
namespace: homelab
|
||||||
|
spec:
|
||||||
|
hostname: local.olsen.cloud # Domain for certificate and gateway
|
||||||
|
issuer: letsencrypt-prod # cert-manager ClusterIssuer to use
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Implementation Features:**
|
||||||
|
|
||||||
|
- **CRD Dependency Checking**: Validates that cert-manager and Istio CRDs exist
|
||||||
|
- **Cross-Namespace Resources**: Certificate is created in the istio-ingress
|
||||||
|
namespace
|
||||||
|
- **Status Aggregation**: Combines certificate and gateway readiness into
|
||||||
|
overall status
|
||||||
|
- **Wildcard Support**: Automatically configures `*.hostname` for subdomains
|
||||||
|
|
||||||
|
### Complex Resource: AuthentikServer
|
||||||
|
|
||||||
|
The `AuthentikServer` resource showcases a complex custom resource with multiple
|
||||||
|
dependencies and sophisticated reconciliation logic. It deploys a complete
|
||||||
|
identity provider solution with database and Redis dependencies.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
- Deploys Authentik identity provider with proper configuration
|
||||||
|
- Manages database schema and user creation
|
||||||
|
- Configures Redis connection for session storage
|
||||||
|
- Sets up domain integration for SSO endpoints
|
||||||
|
- Handles secret generation and rotation
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: AuthentikServer
|
||||||
|
metadata:
|
||||||
|
name: homelab
|
||||||
|
namespace: homelab
|
||||||
|
spec:
|
||||||
|
domain: homelab # References a Domain resource
|
||||||
|
database: test2 # References a PostgresDatabase resource
|
||||||
|
redis: redis # References a Redis connection
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Implementation Features:**
|
||||||
|
|
||||||
|
- **Resource Dependencies**: Waits for Domain, PostgresDatabase, and Redis
|
||||||
|
resources
|
||||||
|
- **Secret Management**: Generates and manages API keys, passwords, and tokens
|
||||||
|
- **Service Configuration**: Creates comprehensive Kubernetes manifests
|
||||||
|
(Deployment, Service, Ingress)
|
||||||
|
- **Health Checking**: Monitors application readiness and database connectivity
|
||||||
|
- **Cross-Resource Communication**: Uses other custom resources' status and
|
||||||
|
outputs
|
||||||
|
|
||||||
|
### Database Resource: PostgresDatabase
|
||||||
|
|
||||||
|
The `PostgresDatabase` resource illustrates how to manage stateful resources and
|
||||||
|
external system integration. It creates databases within an existing PostgreSQL
|
||||||
|
instance and manages user permissions.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
- Creates a new database in an existing PostgreSQL server
|
||||||
|
- Generates dedicated database user with appropriate permissions
|
||||||
|
- Manages connection secrets for applications
|
||||||
|
- Handles database cleanup and user removal
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
|
kind: PostgresDatabase
|
||||||
|
metadata:
|
||||||
|
name: test2
|
||||||
|
namespace: homelab
|
||||||
|
spec:
|
||||||
|
connection: homelab/db # References PostgreSQL connection (namespace/name)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Implementation Features:**
|
||||||
|
|
||||||
|
- **External System Integration**: Connects to existing PostgreSQL instances
|
||||||
|
- **User Management**: Creates database-specific users with minimal required
|
||||||
|
permissions
|
||||||
|
- **Secret Generation**: Provides connection details to consuming applications
|
||||||
|
- **Cleanup Handling**: Safely removes databases and users when resource is
|
||||||
|
deleted
|
||||||
|
- **Connection Validation**: Verifies connectivity before marking as ready
|
||||||
|
|
||||||
|
**Common Patterns Across Examples:**
|
||||||
|
|
||||||
|
- **Owner References**: All managed resources have proper ownership for garbage
|
||||||
|
collection
|
||||||
|
- **Condition Management**: Consistent status reporting through Kubernetes
|
||||||
|
conditions
|
||||||
|
- **Resource Dependencies**: Graceful handling of missing or unready
|
||||||
|
dependencies
|
||||||
|
- **Secret Management**: Secure generation and storage of credentials
|
||||||
|
- **Cross-Resource Integration**: Resources reference and depend on each other
|
||||||
|
appropriately
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Validation**: Always use Zod schemas for comprehensive spec validation
|
||||||
|
2. **Idempotency**: Use `deepEqual` checks to avoid unnecessary updates
|
||||||
|
3. **Conditions**: Provide clear status information through conditions
|
||||||
|
4. **Owner References**: Always set owner references for created resources
|
||||||
|
5. **Error Handling**: Provide meaningful error messages and failure reasons
|
||||||
|
6. **Dependencies**: Handle missing dependencies gracefully
|
||||||
|
7. **Cleanup**: Leverage Kubernetes garbage collection through owner references
|
||||||
|
8. **Testing**: Create test manifests in `test-manifests/` for your resources
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Resource not reconciling**: Check if the resource is properly registered in
|
||||||
|
`custom-resources.ts`
|
||||||
|
- **Validation errors**: Ensure your Zod schema matches the expected spec
|
||||||
|
structure
|
||||||
|
- **Missing dependencies**: Verify that referenced resources exist and are ready
|
||||||
|
- **Owner reference issues**: Make sure `ownerReferences` are set correctly for
|
||||||
|
garbage collection
|
||||||
|
- **Condition not updating**: Ensure you're calling `this.conditions.set()` with
|
||||||
|
proper status values
|
||||||
|
|
||||||
|
For more examples, refer to the existing custom resources in
|
||||||
|
`src/custom-resouces/`.
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
|
||||||
kind: OidcClient
|
|
||||||
metadata:
|
|
||||||
name: test-client
|
|
||||||
spec:
|
|
||||||
environment: dev
|
|
||||||
redirectUris:
|
|
||||||
- url: https://localhost:3000/api/v1/authentik/oauth2/callback
|
|
||||||
matchingMode: strict
|
|
||||||
@@ -6,9 +6,9 @@ metadata:
|
|||||||
apiVersion: homelab.mortenolsen.pro/v1
|
apiVersion: homelab.mortenolsen.pro/v1
|
||||||
kind: Environment
|
kind: Environment
|
||||||
metadata:
|
metadata:
|
||||||
name: prod
|
name: dev
|
||||||
|
namespace: dev
|
||||||
spec:
|
spec:
|
||||||
domain: olsen.cloud
|
domain: one.dev.olsen.cloud
|
||||||
networkIp: 192.168.20.180
|
|
||||||
tls:
|
tls:
|
||||||
issuer: lets-encrypt-prod
|
issuer: letsencrypt-prod
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -25,10 +25,9 @@ metadata:
|
|||||||
namespace: homelab
|
namespace: homelab
|
||||||
spec:
|
spec:
|
||||||
releaseName: operator
|
releaseName: operator
|
||||||
interval: 60m
|
|
||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: charts/operator
|
chart: chart
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: GitRepository
|
kind: GitRepository
|
||||||
name: homelab
|
name: homelab
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
"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",
|
||||||
@@ -37,12 +35,6 @@
|
|||||||
"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,12 +14,6 @@ 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
|
||||||
@@ -235,15 +229,9 @@ 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==}
|
||||||
|
|
||||||
@@ -315,10 +303,6 @@ 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:
|
||||||
@@ -495,9 +479,6 @@ 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'}
|
||||||
@@ -526,10 +507,6 @@ 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'}
|
||||||
@@ -777,10 +754,6 @@ 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==}
|
||||||
|
|
||||||
@@ -852,17 +825,10 @@ 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==}
|
||||||
|
|
||||||
@@ -1272,10 +1238,6 @@ 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'}
|
||||||
@@ -1377,11 +1339,6 @@ 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}
|
||||||
@@ -1929,9 +1886,6 @@ 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==}
|
||||||
|
|
||||||
@@ -1951,10 +1905,6 @@ 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==}
|
||||||
|
|
||||||
@@ -2179,17 +2129,11 @@ 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
|
||||||
@@ -2296,10 +2240,6 @@ 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
|
||||||
@@ -2318,6 +2258,7 @@ 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:
|
||||||
@@ -2522,18 +2463,6 @@ 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
|
||||||
@@ -2556,11 +2485,6 @@ 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
|
||||||
@@ -2904,8 +2828,6 @@ 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:
|
||||||
@@ -2981,8 +2903,6 @@ 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
|
||||||
@@ -2991,11 +2911,6 @@ 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:
|
||||||
@@ -3149,6 +3064,7 @@ 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:
|
||||||
@@ -3413,8 +3329,6 @@ 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
|
||||||
@@ -3526,8 +3440,6 @@ 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
|
||||||
@@ -4186,8 +4098,6 @@ 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: {}
|
||||||
@@ -4208,8 +4118,6 @@ 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:
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
[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",
|
|
||||||
]
|
|
||||||
4
scripts/apply-test.sh
Executable file
4
scripts/apply-test.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
for f in "./test-manifests/"*; do
|
||||||
|
echo "Applying $f"
|
||||||
|
kubectl apply -f "$f"
|
||||||
|
done
|
||||||
20
scripts/create-secrets.sh
Executable file
20
scripts/create-secrets.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/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}"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user