From 5ee7a76443fd80aae399710934f31f620386d300 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Wed, 3 Sep 2025 14:33:48 +0200 Subject: [PATCH] more stuff --- .../{http-service.yaml => _http-service.yaml} | 0 charts/apps/bytestash/templates/client.yaml | 3 +- .../templates/external-http-service.yaml | 2 +- .../bytestash/templates/stateful-set.yaml | 5 -- charts/apps/jellyfin/Chart.yaml | 3 ++ charts/apps/jellyfin/templates/client.yaml | 10 ++++ .../apps/jellyfin/templates/config-pvc.yaml | 11 ++++ .../apps/jellyfin/templates/deployment.yaml | 42 +++++++++++++++ .../templates/external-http-service.yaml | 11 ++++ charts/apps/jellyfin/templates/service.yaml | 15 ++++++ charts/apps/jellyfin/values.yaml | 6 +++ charts/apps/ollama/Chart.yaml | 3 ++ charts/apps/ollama/templates/deployment.yaml | 52 ++++++++++++++++++ charts/apps/ollama/templates/pvc.yaml | 11 ++++ charts/apps/ollama/templates/service.yaml | 15 ++++++ charts/apps/ollama/values.yaml | 2 + charts/volumes/Chart.yaml | 3 ++ charts/volumes/templates/books-pvc.yaml | 30 +++++++++++ charts/volumes/templates/movies-pvc.yaml | 30 +++++++++++ charts/volumes/templates/music-pvc.yaml | 30 +++++++++++ charts/volumes/templates/podcasts-pvc.yaml | 30 +++++++++++ charts/volumes/templates/tv-pvc.yaml | 30 +++++++++++ charts/volumes/values.yaml | 11 ++++ istio-test.yaml | 22 ++++++++ manifests/environment.yaml | 4 +- src/bootstrap/repos/repos.ts | 11 ++++ .../authentik-server/authentik-server.ts | 34 +++++------- .../cloudflare-tunnel/cloudflare-tunnel.ts | 17 ++++++ .../homelab/environment/environment.ts | 53 +++++++++++-------- .../homelab/oidc-client/oidc-client.ts | 9 +++- src/services/cloudflare/cloudflare.ts | 49 +++++++++++++++++ 31 files changed, 501 insertions(+), 53 deletions(-) rename charts/apps/bytestash/templates/{http-service.yaml => _http-service.yaml} (100%) create mode 100644 charts/apps/jellyfin/Chart.yaml create mode 100644 charts/apps/jellyfin/templates/client.yaml create mode 100644 charts/apps/jellyfin/templates/config-pvc.yaml create mode 100644 charts/apps/jellyfin/templates/deployment.yaml create mode 100644 charts/apps/jellyfin/templates/external-http-service.yaml create mode 100644 charts/apps/jellyfin/templates/service.yaml create mode 100644 charts/apps/jellyfin/values.yaml create mode 100644 charts/apps/ollama/Chart.yaml create mode 100644 charts/apps/ollama/templates/deployment.yaml create mode 100644 charts/apps/ollama/templates/pvc.yaml create mode 100644 charts/apps/ollama/templates/service.yaml create mode 100644 charts/apps/ollama/values.yaml create mode 100644 charts/volumes/Chart.yaml create mode 100644 charts/volumes/templates/books-pvc.yaml create mode 100644 charts/volumes/templates/movies-pvc.yaml create mode 100644 charts/volumes/templates/music-pvc.yaml create mode 100644 charts/volumes/templates/podcasts-pvc.yaml create mode 100644 charts/volumes/templates/tv-pvc.yaml create mode 100644 charts/volumes/values.yaml create mode 100644 istio-test.yaml diff --git a/charts/apps/bytestash/templates/http-service.yaml b/charts/apps/bytestash/templates/_http-service.yaml similarity index 100% rename from charts/apps/bytestash/templates/http-service.yaml rename to charts/apps/bytestash/templates/_http-service.yaml diff --git a/charts/apps/bytestash/templates/client.yaml b/charts/apps/bytestash/templates/client.yaml index a21656c..2d5b3a6 100644 --- a/charts/apps/bytestash/templates/client.yaml +++ b/charts/apps/bytestash/templates/client.yaml @@ -5,5 +5,6 @@ metadata: spec: environment: '{{ .Values.environment }}' redirectUris: - - url: https://localhost:3000/api/v1/authentik/oauth2/callback + - path: /api/auth/oidc/callback + subdomain: bytestash matchingMode: strict diff --git a/charts/apps/bytestash/templates/external-http-service.yaml b/charts/apps/bytestash/templates/external-http-service.yaml index ff86e79..f944629 100644 --- a/charts/apps/bytestash/templates/external-http-service.yaml +++ b/charts/apps/bytestash/templates/external-http-service.yaml @@ -4,7 +4,7 @@ metadata: name: '{{ .Release.Name }}' spec: environment: '{{ .Values.environment }}' - subdomain: '{{ .Values.subdomain }}-external' + subdomain: '{{ .Values.subdomain }}' destination: host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local' port: diff --git a/charts/apps/bytestash/templates/stateful-set.yaml b/charts/apps/bytestash/templates/stateful-set.yaml index 46d5798..c1878fb 100644 --- a/charts/apps/bytestash/templates/stateful-set.yaml +++ b/charts/apps/bytestash/templates/stateful-set.yaml @@ -42,11 +42,6 @@ spec: name: '{{ .Release.Name }}-client' key: configuration - # !! IMPORTANT !! - # You MUST update this Redirect URI to match your external URL. - # This URI must also be configured in your Authentik provider settings for this client. - #- name: BS_OIDC_REDIRECT_URI - #value: 'https://bytestash.your-domain.com/login/oauth2/code/oidc' volumeMounts: - mountPath: /data/snippets name: bytestash-data diff --git a/charts/apps/jellyfin/Chart.yaml b/charts/apps/jellyfin/Chart.yaml new file mode 100644 index 0000000..333b29f --- /dev/null +++ b/charts/apps/jellyfin/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +version: 1.0.0 +name: Jellyfin diff --git a/charts/apps/jellyfin/templates/client.yaml b/charts/apps/jellyfin/templates/client.yaml new file mode 100644 index 0000000..4b53326 --- /dev/null +++ b/charts/apps/jellyfin/templates/client.yaml @@ -0,0 +1,10 @@ +apiVersion: homelab.mortenolsen.pro/v1 +kind: OidcClient +metadata: + name: '{{ .Release.Name }}' +spec: + environment: '{{ .Values.environment }}' + redirectUris: + - path: /api/auth/oidc/callback + subdomain: '{{ .Values.subdomain }}' + matchingMode: strict diff --git a/charts/apps/jellyfin/templates/config-pvc.yaml b/charts/apps/jellyfin/templates/config-pvc.yaml new file mode 100644 index 0000000..d52ab25 --- /dev/null +++ b/charts/apps/jellyfin/templates/config-pvc.yaml @@ -0,0 +1,11 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: '{{ .Release.Name }}-config' +spec: + accessModes: + - 'ReadWriteOnce' + resources: + requests: + storage: '1Gi' + storageClassName: '{{ .Values.environment }}' diff --git a/charts/apps/jellyfin/templates/deployment.yaml b/charts/apps/jellyfin/templates/deployment.yaml new file mode 100644 index 0000000..0659c39 --- /dev/null +++ b/charts/apps/jellyfin/templates/deployment.yaml @@ -0,0 +1,42 @@ +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 + volumes: + - name: config + persistentVolumeClaim: + claimName: '{{ .Release.Name }}-config' + - name: movies + persistentVolumeClaim: + claimName: 'movies' diff --git a/charts/apps/jellyfin/templates/external-http-service.yaml b/charts/apps/jellyfin/templates/external-http-service.yaml new file mode 100644 index 0000000..f944629 --- /dev/null +++ b/charts/apps/jellyfin/templates/external-http-service.yaml @@ -0,0 +1,11 @@ +apiVersion: homelab.mortenolsen.pro/v1 +kind: ExternalHttpService +metadata: + name: '{{ .Release.Name }}' +spec: + environment: '{{ .Values.environment }}' + subdomain: '{{ .Values.subdomain }}' + destination: + host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local' + port: + number: 80 diff --git a/charts/apps/jellyfin/templates/service.yaml b/charts/apps/jellyfin/templates/service.yaml new file mode 100644 index 0000000..99905f3 --- /dev/null +++ b/charts/apps/jellyfin/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: '{{ .Release.Name }}' + labels: + app: '{{ .Release.Name }}' +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8096 + protocol: TCP + name: http + selector: + app: '{{ .Release.Name }}' diff --git a/charts/apps/jellyfin/values.yaml b/charts/apps/jellyfin/values.yaml new file mode 100644 index 0000000..d01bf0a --- /dev/null +++ b/charts/apps/jellyfin/values.yaml @@ -0,0 +1,6 @@ +image: + repository: docker.io/jellyfin/jellyfin + tag: latest + pullPolicy: IfNotPresent +environment: dev +subdomain: jellyfin diff --git a/charts/apps/ollama/Chart.yaml b/charts/apps/ollama/Chart.yaml new file mode 100644 index 0000000..5726120 --- /dev/null +++ b/charts/apps/ollama/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +version: 1.0.0 +name: Ollama diff --git a/charts/apps/ollama/templates/deployment.yaml b/charts/apps/ollama/templates/deployment.yaml new file mode 100644 index 0000000..1cadfe6 --- /dev/null +++ b/charts/apps/ollama/templates/deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: '{{ .Release.Name }}' + labels: + app: '{{ .Release.Name }}' +spec: + replicas: 1 + selector: + matchLabels: + app: '{{ .Release.Name }}' + template: + metadata: + labels: + app: '{{ .Release.Name }}' + spec: + containers: + - name: ollama + image: ghcr.io/ollama/ollama:latest # Official image + imagePullPolicy: IfNotPresent + ports: + - containerPort: 11434 + name: http + volumeMounts: + - name: ollama-data + mountPath: /root/.ollama + env: + # If you want to pre‑start a model, set this env var to the + # model name (e.g., "gpt-4o-mini"). The container will download + # it automatically at startup. + # - name: OLLAMA_MODEL + # value: "gpt-4o-mini" + readinessProbe: + httpGet: + scheme: HTTP + path: /api/status + port: 11434 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 2000m + memory: 4Gi + volumes: + - name: ollama-data + persistentVolumeClaim: + claimName: '{{ .Release.Name }}-data' diff --git a/charts/apps/ollama/templates/pvc.yaml b/charts/apps/ollama/templates/pvc.yaml new file mode 100644 index 0000000..8530cae --- /dev/null +++ b/charts/apps/ollama/templates/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: '{{ .Release.Name }}-data' +spec: + storageClassName: '{{ .Values.environment }}' + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi diff --git a/charts/apps/ollama/templates/service.yaml b/charts/apps/ollama/templates/service.yaml new file mode 100644 index 0000000..4fcd7f4 --- /dev/null +++ b/charts/apps/ollama/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: '{{ .Release.Name }}' + labels: + app: '{{ .Release.Name }}' +spec: + type: LoadBalancer # Set to NodePort/ClusterIP if you prefer + ports: + - name: http + port: 11434 + targetPort: http + protocol: TCP + selector: + app: '{{ .Release.Name }}' diff --git a/charts/apps/ollama/values.yaml b/charts/apps/ollama/values.yaml new file mode 100644 index 0000000..3067066 --- /dev/null +++ b/charts/apps/ollama/values.yaml @@ -0,0 +1,2 @@ +environment: dev +subdomain: bytestash diff --git a/charts/volumes/Chart.yaml b/charts/volumes/Chart.yaml new file mode 100644 index 0000000..2e26fac --- /dev/null +++ b/charts/volumes/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +version: 1.0.0 +name: Resources diff --git a/charts/volumes/templates/books-pvc.yaml b/charts/volumes/templates/books-pvc.yaml new file mode 100644 index 0000000..2f0693c --- /dev/null +++ b/charts/volumes/templates/books-pvc.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: books + labels: + type: nfs +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: null + nfs: + path: '{{ .Values.books.path }}' + server: '{{ .Values.host }}' + readOnly: true +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: books +spec: + storageClassName: manual + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi diff --git a/charts/volumes/templates/movies-pvc.yaml b/charts/volumes/templates/movies-pvc.yaml new file mode 100644 index 0000000..693aa9c --- /dev/null +++ b/charts/volumes/templates/movies-pvc.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: movies + labels: + type: nfs +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: null + nfs: + path: '{{ .Values.movies.path }}' + server: '{{ .Values.host }}' + readOnly: true +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: movies +spec: + storageClassName: manual + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi diff --git a/charts/volumes/templates/music-pvc.yaml b/charts/volumes/templates/music-pvc.yaml new file mode 100644 index 0000000..8f648cc --- /dev/null +++ b/charts/volumes/templates/music-pvc.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: music + labels: + type: nfs +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: null + nfs: + path: '{{ .Values.music.path }}' + server: '{{ .Values.host }}' + readOnly: true +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: music +spec: + storageClassName: manual + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi diff --git a/charts/volumes/templates/podcasts-pvc.yaml b/charts/volumes/templates/podcasts-pvc.yaml new file mode 100644 index 0000000..9e093fe --- /dev/null +++ b/charts/volumes/templates/podcasts-pvc.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: podcasts + labels: + type: nfs +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: null + nfs: + path: '{{ .Values.podcasts.path }}' + server: '{{ .Values.host }}' + readOnly: true +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: podcasts +spec: + storageClassName: manual + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi diff --git a/charts/volumes/templates/tv-pvc.yaml b/charts/volumes/templates/tv-pvc.yaml new file mode 100644 index 0000000..06386c9 --- /dev/null +++ b/charts/volumes/templates/tv-pvc.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: tv-shows + labels: + type: nfs +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: null + nfs: + path: '{{ .Values.tv-shows.path }}' + server: '{{ .Values.host }}' + readOnly: true +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: tv-shows +spec: + storageClassName: manual + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi diff --git a/charts/volumes/values.yaml b/charts/volumes/values.yaml new file mode 100644 index 0000000..983c5e5 --- /dev/null +++ b/charts/volumes/values.yaml @@ -0,0 +1,11 @@ +host: 192.168.20.106 +movies: + path: /mnt/HDD/Movies +tv-shows: + path: /mnt/HDD/TV-Shows +music: + path: /mnt/HDD/Music2 +books: + path: /mnt/HDD/Books +podcats: + path: /mnt/HDD/Podcasts diff --git a/istio-test.yaml b/istio-test.yaml new file mode 100644 index 0000000..f6b0617 --- /dev/null +++ b/istio-test.yaml @@ -0,0 +1,22 @@ +apiVersion: networking.istio.io/v1beta1 +kind: ServiceEntry +metadata: + name: dev-authentik-override + namespace: dev +spec: + hosts: + - authentik.mortenolsen.nett + ports: + - number: 443 + name: https + protocol: HTTPS + - number: 80 + name: http + protocol: HTTP + location: MESH_EXTERNAL + resolution: STATIC + endpoints: + - address: 1.1.1.1 + ports: + https: 443 + http: 80 diff --git a/manifests/environment.yaml b/manifests/environment.yaml index 38ed2d7..00222b8 100644 --- a/manifests/environment.yaml +++ b/manifests/environment.yaml @@ -8,7 +8,7 @@ kind: Environment metadata: name: dev spec: - domain: one.dev.olsen.cloud - networkIp: 192.168.107.2 + domain: mortenolsen.net + networkIp: 192.168.64.2 tls: issuer: lets-encrypt-prod diff --git a/src/bootstrap/repos/repos.ts b/src/bootstrap/repos/repos.ts index 648d7df..3f3760b 100644 --- a/src/bootstrap/repos/repos.ts +++ b/src/bootstrap/repos/repos.ts @@ -9,6 +9,7 @@ class RepoService { #istio: HelmRepo; #authentik: HelmRepo; #cloudflare: HelmRepo; + #argo: HelmRepo; constructor(services: Services) { const resourceService = services.get(ResourceService); @@ -16,11 +17,13 @@ class RepoService { this.#istio = resourceService.get(HelmRepo, 'istio', NAMESPACE); this.#authentik = resourceService.get(HelmRepo, 'authentik', NAMESPACE); this.#cloudflare = resourceService.get(HelmRepo, 'cloudflare', NAMESPACE); + this.#argo = resourceService.get(HelmRepo, 'argo', NAMESPACE); this.#jetstack.on('changed', this.ensure); this.#istio.on('changed', this.ensure); this.#authentik.on('changed', this.ensure); this.#cloudflare.on('changed', this.ensure); + this.#argo.on('changed', this.ensure); } public get jetstack() { @@ -39,6 +42,10 @@ class RepoService { return this.#cloudflare; } + public get argo() { + return this.#argo; + } + public ensure = async () => { await this.#jetstack.set({ url: 'https://charts.jetstack.io', @@ -55,6 +62,10 @@ class RepoService { await this.#cloudflare.set({ url: 'https://cloudflare.github.io/helm-charts', }); + + await this.#argo.set({ + url: 'https://argoproj.github.io/argo-helm', + }); }; } diff --git a/src/resources/homelab/authentik-server/authentik-server.ts b/src/resources/homelab/authentik-server/authentik-server.ts index 6354b74..c85d597 100644 --- a/src/resources/homelab/authentik-server/authentik-server.ts +++ b/src/resources/homelab/authentik-server/authentik-server.ts @@ -15,9 +15,9 @@ import { generateRandomHexPass } from '#utils/secrets.ts'; import { Service } from '#resources/core/service/service.ts'; import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts'; import { RepoService } from '#bootstrap/repos/repos.ts'; -import { VirtualService } from '#resources/istio/virtual-service/virtual-service.ts'; import { DestinationRule } from '#resources/istio/destination-rule/destination-rule.ts'; import { NotReadyError } from '#utils/errors.ts'; +import { ExternalHttpService } from '../external-http-service.ts/external-http-service.ts'; const specSchema = z.object({ environment: z.string(), @@ -44,7 +44,7 @@ class AuthentikServer extends CustomResource { #initSecret: Secret; #service: Service; #helmRelease: HelmRelease; - #virtualService: VirtualService; + #externalHttpService: ExternalHttpService; #destinationRule: DestinationRule; constructor(options: CustomResourceOptions) { @@ -68,11 +68,10 @@ class AuthentikServer extends CustomResource { this.#helmRelease = resourceService.get(HelmRelease, this.name, this.namespace); this.#helmRelease.on('changed', this.queueReconcile); - this.#virtualService = resourceService.get(VirtualService, this.name, this.namespace); - this.#virtualService.on('changed', this.queueReconcile); - this.#destinationRule = resourceService.get(DestinationRule, this.name, this.namespace); this.#destinationRule.on('changed', this.queueReconcile); + + this.#externalHttpService = resourceService.get(ExternalHttpService, this.name, this.namespace); } public get service() { @@ -254,28 +253,19 @@ class AuthentikServer extends CustomResource { }, }); - const gateway = this.#environment.current.gateway; - await this.#virtualService.set({ + await this.#externalHttpService.ensure({ metadata: { ownerReferences: [this.ref], }, spec: { - gateways: [`${gateway.namespace}/${gateway.name}`, 'mesh'], - hosts: [domain], - http: [ - { - route: [ - { - destination: { - host: this.#service.hostname, - port: { - number: 80, - }, - }, - }, - ], + environment: this.spec.environment, + subdomain: this.spec.subdomain || 'authentik', + destination: { + host: this.#service.hostname, + port: { + number: 80, }, - ], + }, }, }); }; diff --git a/src/resources/homelab/cloudflare-tunnel/cloudflare-tunnel.ts b/src/resources/homelab/cloudflare-tunnel/cloudflare-tunnel.ts index 084fd93..9612de4 100644 --- a/src/resources/homelab/cloudflare-tunnel/cloudflare-tunnel.ts +++ b/src/resources/homelab/cloudflare-tunnel/cloudflare-tunnel.ts @@ -12,6 +12,7 @@ import { RepoService } from '#bootstrap/repos/repos.ts'; import { Secret } from '#resources/core/secret/secret.ts'; import { NotReadyError } from '#utils/errors.ts'; import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts'; +import { CloudflareService } from '#services/cloudflare/cloudflare.ts'; const specSchema = z.object({}); @@ -29,6 +30,7 @@ class CloudflareTunnel extends CustomResource { #helmRelease: HelmRelease; #secret: Secret; + #cloudflareService; constructor(options: CustomResourceOptions) { super(options); @@ -40,6 +42,9 @@ class CloudflareTunnel extends CustomResource { this.#helmRelease = resourceService.get(HelmRelease, this.name, namespace); this.#secret = resourceService.get(Secret, 'cloudflare', namespace); this.#secret.on('changed', this.queueReconcile); + + this.#cloudflareService = this.services.get(CloudflareService); + this.#cloudflareService.on('changed', this.queueReconcile); } #handleResourceChanged = (resource: Resource) => { @@ -60,6 +65,18 @@ class CloudflareTunnel extends CustomResource { hostname: rule?.hostname, service: `http://${rule?.destination.host}:${rule?.destination.port.number}`, })); + if (this.#cloudflareService.ready) { + for (const route of ingress) { + if (!route.hostname) { + continue; + } + try { + await this.#cloudflareService.ensureTunnel(route.hostname); + } catch (err) { + console.error(err); + } + } + } await this.#helmRelease.ensure({ metadata: { ownerReferences: [this.ref], diff --git a/src/resources/homelab/environment/environment.ts b/src/resources/homelab/environment/environment.ts index 453be8c..a315bac 100644 --- a/src/resources/homelab/environment/environment.ts +++ b/src/resources/homelab/environment/environment.ts @@ -14,6 +14,8 @@ import { Gateway } from '#resources/istio/gateway/gateway.ts'; import { NotReadyError } from '#utils/errors.ts'; import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts'; import { CloudflareService } from '#services/cloudflare/cloudflare.ts'; +import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts'; +import { RepoService } from '#bootstrap/repos/repos.ts'; const specSchema = z.object({ domain: z.string(), @@ -37,6 +39,8 @@ class Environment extends CustomResource { #redisServer: RedisServer; #authentikServer: AuthentikServer; #cloudflareService: CloudflareService; + #argoRelease: HelmRelease; + #argoNamespace: Namespace; constructor(options: CustomResourceOptions) { super(options); @@ -67,6 +71,11 @@ class Environment extends CustomResource { this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, homelabNamespace); this.#authentikServer.on('changed', this.queueReconcile); + + this.#argoNamespace = resourceService.get(Namespace, `${this.name}-argo`); + + this.#argoRelease = resourceService.get(HelmRelease, `${this.name}-argo`, homelabNamespace); + this.#argoRelease.on('changed', this.queueReconcile); } public get certificate() { @@ -99,27 +108,6 @@ class Environment extends CustomResource { throw new NotReadyError('InvalidSpec'); } - if (this.#cloudflareService.ready && spec.networkIp) { - const client = this.#cloudflareService.client; - const zones = await client.zones.list({ - name: spec.domain, - }); - const [zone] = zones.result; - if (!zone) { - throw new NotReadyError('NoZoneFound'); - } - - const existingRecords = await client.dns.records.list({ - zone_id: zone.id, - name: { - exact: `*.${spec.domain}`, - }, - }); - - console.log('Cloudflare records', existingRecords); - - // zones.result[0]. - } await this.#namespace.ensure({ metadata: { labels: { @@ -208,6 +196,29 @@ class Environment extends CustomResource { ], }, }); + + await this.#argoNamespace.ensure({}); + + const repoService = this.services.get(RepoService); + await this.#argoRelease.ensure({ + spec: { + targetNamespace: this.#argoNamespace.name, + interval: '1h', + values: {}, + chart: { + spec: { + chart: 'argo-cd', + version: '3.9.0', + sourceRef: { + apiVersion: 'source.toolkit.fluxcd.io/v1', + kind: 'HelmRepository', + name: repoService.argo.name, + namespace: repoService.argo.namespace, + }, + }, + }, + }, + }); }; } diff --git a/src/resources/homelab/oidc-client/oidc-client.ts b/src/resources/homelab/oidc-client/oidc-client.ts index ee9df5c..c0477c9 100644 --- a/src/resources/homelab/oidc-client/oidc-client.ts +++ b/src/resources/homelab/oidc-client/oidc-client.ts @@ -19,7 +19,8 @@ const specSchema = z.object({ clientType: z.enum(ClientTypeEnum).optional(), redirectUris: z.array( z.object({ - url: z.string(), + subdomain: z.string(), + path: z.string(), matchingMode: z.enum(['strict', 'regex']), }), ), @@ -98,8 +99,14 @@ class OIDCClient extends CustomResource { token: authentikSecret.token, }); + const redirectUris = this.spec.redirectUris.map((uri) => ({ + matchingMode: uri.matchingMode, + url: new URL(uri.path, `https://${uri.subdomain}.${this.#environment.current?.spec?.domain}`).toString(), + })); + await authentikServer.upsertClient({ ...this.spec, + redirectUris, name: this.name, secret: secret.clientSecret, }); diff --git a/src/services/cloudflare/cloudflare.ts b/src/services/cloudflare/cloudflare.ts index cc1e97b..f543b96 100644 --- a/src/services/cloudflare/cloudflare.ts +++ b/src/services/cloudflare/cloudflare.ts @@ -52,6 +52,55 @@ class CloudflareService extends EventEmitter { return client; } + + public ensureTunnel = async (route: string) => { + const secret = this.#secret.value; + if (!secret) { + return; + } + const client = this.client; + const domainParts = route.split('.'); + const cname = `${secret.tunnelId}.cfargotunnel.com`; + const tld = domainParts.pop(); + const root = domainParts.pop(); + const zoneName = `${root}.${tld}`; + const name = domainParts.join('.'); + + const zones = await client.zones.list({ + name: zoneName, + }); + const [zone] = zones.result; + if (!zone) { + return; + } + const records = await client.dns.records.list({ + zone_id: zone.id, + name: { + exact: route, + }, + type: 'CNAME', + }); + const [record] = records.result; + if (record) { + await client.dns.records.edit(record.id, { + zone_id: zone.id, + type: 'CNAME', + content: cname, + name: name, + ttl: 1, + proxied: true, + }); + } else { + await client.dns.records.create({ + zone_id: zone.id, + type: 'CNAME', + content: cname, + name: name, + ttl: 1, + proxied: true, + }); + } + }; } export { CloudflareService };