mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
more stuff
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
3
charts/apps/jellyfin/Chart.yaml
Normal file
3
charts/apps/jellyfin/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
apiVersion: v2
|
||||
version: 1.0.0
|
||||
name: Jellyfin
|
||||
10
charts/apps/jellyfin/templates/client.yaml
Normal file
10
charts/apps/jellyfin/templates/client.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: homelab.mortenolsen.pro/v1
|
||||
kind: OidcClient
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}'
|
||||
spec:
|
||||
environment: '{{ .Values.environment }}'
|
||||
redirectUris:
|
||||
- path: /api/auth/oidc/callback
|
||||
subdomain: '{{ .Values.subdomain }}'
|
||||
matchingMode: strict
|
||||
11
charts/apps/jellyfin/templates/config-pvc.yaml
Normal file
11
charts/apps/jellyfin/templates/config-pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-config'
|
||||
spec:
|
||||
accessModes:
|
||||
- 'ReadWriteOnce'
|
||||
resources:
|
||||
requests:
|
||||
storage: '1Gi'
|
||||
storageClassName: '{{ .Values.environment }}'
|
||||
42
charts/apps/jellyfin/templates/deployment.yaml
Normal file
42
charts/apps/jellyfin/templates/deployment.yaml
Normal file
@@ -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'
|
||||
11
charts/apps/jellyfin/templates/external-http-service.yaml
Normal file
11
charts/apps/jellyfin/templates/external-http-service.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: homelab.mortenolsen.pro/v1
|
||||
kind: ExternalHttpService
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}'
|
||||
spec:
|
||||
environment: '{{ .Values.environment }}'
|
||||
subdomain: '{{ .Values.subdomain }}'
|
||||
destination:
|
||||
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
|
||||
port:
|
||||
number: 80
|
||||
15
charts/apps/jellyfin/templates/service.yaml
Normal file
15
charts/apps/jellyfin/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}'
|
||||
labels:
|
||||
app: '{{ .Release.Name }}'
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8096
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: '{{ .Release.Name }}'
|
||||
6
charts/apps/jellyfin/values.yaml
Normal file
6
charts/apps/jellyfin/values.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
image:
|
||||
repository: docker.io/jellyfin/jellyfin
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
environment: dev
|
||||
subdomain: jellyfin
|
||||
3
charts/apps/ollama/Chart.yaml
Normal file
3
charts/apps/ollama/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
apiVersion: v2
|
||||
version: 1.0.0
|
||||
name: Ollama
|
||||
52
charts/apps/ollama/templates/deployment.yaml
Normal file
52
charts/apps/ollama/templates/deployment.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}'
|
||||
labels:
|
||||
app: '{{ .Release.Name }}'
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: '{{ .Release.Name }}'
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: '{{ .Release.Name }}'
|
||||
spec:
|
||||
containers:
|
||||
- name: ollama
|
||||
image: ghcr.io/ollama/ollama:latest # Official image
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 11434
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: ollama-data
|
||||
mountPath: /root/.ollama
|
||||
env:
|
||||
# If you want to pre‑start a model, set this env var to the
|
||||
# model name (e.g., "gpt-4o-mini"). The container will download
|
||||
# it automatically at startup.
|
||||
# - name: OLLAMA_MODEL
|
||||
# value: "gpt-4o-mini"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
scheme: HTTP
|
||||
path: /api/status
|
||||
port: 11434
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 4Gi
|
||||
volumes:
|
||||
- name: ollama-data
|
||||
persistentVolumeClaim:
|
||||
claimName: '{{ .Release.Name }}-data'
|
||||
11
charts/apps/ollama/templates/pvc.yaml
Normal file
11
charts/apps/ollama/templates/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}-data'
|
||||
spec:
|
||||
storageClassName: '{{ .Values.environment }}'
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
15
charts/apps/ollama/templates/service.yaml
Normal file
15
charts/apps/ollama/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: '{{ .Release.Name }}'
|
||||
labels:
|
||||
app: '{{ .Release.Name }}'
|
||||
spec:
|
||||
type: LoadBalancer # Set to NodePort/ClusterIP if you prefer
|
||||
ports:
|
||||
- name: http
|
||||
port: 11434
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: '{{ .Release.Name }}'
|
||||
2
charts/apps/ollama/values.yaml
Normal file
2
charts/apps/ollama/values.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
environment: dev
|
||||
subdomain: bytestash
|
||||
3
charts/volumes/Chart.yaml
Normal file
3
charts/volumes/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
apiVersion: v2
|
||||
version: 1.0.0
|
||||
name: Resources
|
||||
30
charts/volumes/templates/books-pvc.yaml
Normal file
30
charts/volumes/templates/books-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: books
|
||||
labels:
|
||||
type: nfs
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: manual
|
||||
hostPath: null
|
||||
nfs:
|
||||
path: '{{ .Values.books.path }}'
|
||||
server: '{{ .Values.host }}'
|
||||
readOnly: true
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: books
|
||||
spec:
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
30
charts/volumes/templates/movies-pvc.yaml
Normal file
30
charts/volumes/templates/movies-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: movies
|
||||
labels:
|
||||
type: nfs
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: manual
|
||||
hostPath: null
|
||||
nfs:
|
||||
path: '{{ .Values.movies.path }}'
|
||||
server: '{{ .Values.host }}'
|
||||
readOnly: true
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: movies
|
||||
spec:
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
30
charts/volumes/templates/music-pvc.yaml
Normal file
30
charts/volumes/templates/music-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: music
|
||||
labels:
|
||||
type: nfs
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: manual
|
||||
hostPath: null
|
||||
nfs:
|
||||
path: '{{ .Values.music.path }}'
|
||||
server: '{{ .Values.host }}'
|
||||
readOnly: true
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: music
|
||||
spec:
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
30
charts/volumes/templates/podcasts-pvc.yaml
Normal file
30
charts/volumes/templates/podcasts-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: podcasts
|
||||
labels:
|
||||
type: nfs
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: manual
|
||||
hostPath: null
|
||||
nfs:
|
||||
path: '{{ .Values.podcasts.path }}'
|
||||
server: '{{ .Values.host }}'
|
||||
readOnly: true
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: podcasts
|
||||
spec:
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
30
charts/volumes/templates/tv-pvc.yaml
Normal file
30
charts/volumes/templates/tv-pvc.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: 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
|
||||
11
charts/volumes/values.yaml
Normal file
11
charts/volumes/values.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
host: 192.168.20.106
|
||||
movies:
|
||||
path: /mnt/HDD/Movies
|
||||
tv-shows:
|
||||
path: /mnt/HDD/TV-Shows
|
||||
music:
|
||||
path: /mnt/HDD/Music2
|
||||
books:
|
||||
path: /mnt/HDD/Books
|
||||
podcats:
|
||||
path: /mnt/HDD/Podcasts
|
||||
22
istio-test.yaml
Normal file
22
istio-test.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: networking.istio.io/v1beta1
|
||||
kind: ServiceEntry
|
||||
metadata:
|
||||
name: dev-authentik-override
|
||||
namespace: dev
|
||||
spec:
|
||||
hosts:
|
||||
- authentik.mortenolsen.nett
|
||||
ports:
|
||||
- number: 443
|
||||
name: https
|
||||
protocol: HTTPS
|
||||
- number: 80
|
||||
name: http
|
||||
protocol: HTTP
|
||||
location: MESH_EXTERNAL
|
||||
resolution: STATIC
|
||||
endpoints:
|
||||
- address: 1.1.1.1
|
||||
ports:
|
||||
https: 443
|
||||
http: 80
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<typeof specSchema> {
|
||||
#initSecret: Secret<InitSecretData>;
|
||||
#service: Service;
|
||||
#helmRelease: HelmRelease;
|
||||
#virtualService: VirtualService;
|
||||
#externalHttpService: ExternalHttpService;
|
||||
#destinationRule: DestinationRule;
|
||||
|
||||
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
||||
@@ -68,11 +68,10 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
|
||||
this.#helmRelease = resourceService.get(HelmRelease, this.name, this.namespace);
|
||||
this.#helmRelease.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<typeof specSchema> {
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<typeof specSchema> {
|
||||
|
||||
#helmRelease: HelmRelease;
|
||||
#secret: Secret<SecretData>;
|
||||
#cloudflareService;
|
||||
|
||||
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
||||
super(options);
|
||||
@@ -40,6 +42,9 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
|
||||
this.#helmRelease = resourceService.get(HelmRelease, this.name, namespace);
|
||||
this.#secret = resourceService.get(Secret<SecretData>, 'cloudflare', namespace);
|
||||
this.#secret.on('changed', this.queueReconcile);
|
||||
|
||||
this.#cloudflareService = this.services.get(CloudflareService);
|
||||
this.#cloudflareService.on('changed', this.queueReconcile);
|
||||
}
|
||||
|
||||
#handleResourceChanged = (resource: Resource<ExpectedAny>) => {
|
||||
@@ -60,6 +65,18 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
|
||||
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],
|
||||
|
||||
@@ -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<typeof specSchema> {
|
||||
#redisServer: RedisServer;
|
||||
#authentikServer: AuthentikServer;
|
||||
#cloudflareService: CloudflareService;
|
||||
#argoRelease: HelmRelease;
|
||||
#argoNamespace: Namespace;
|
||||
|
||||
constructor(options: CustomResourceOptions<typeof specSchema>) {
|
||||
super(options);
|
||||
@@ -67,6 +71,11 @@ class Environment extends CustomResource<typeof specSchema> {
|
||||
|
||||
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<typeof specSchema> {
|
||||
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<typeof specSchema> {
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await this.#argoNamespace.ensure({});
|
||||
|
||||
const repoService = this.services.get(RepoService);
|
||||
await this.#argoRelease.ensure({
|
||||
spec: {
|
||||
targetNamespace: this.#argoNamespace.name,
|
||||
interval: '1h',
|
||||
values: {},
|
||||
chart: {
|
||||
spec: {
|
||||
chart: 'argo-cd',
|
||||
version: '3.9.0',
|
||||
sourceRef: {
|
||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
||||
kind: 'HelmRepository',
|
||||
name: repoService.argo.name,
|
||||
namespace: repoService.argo.namespace,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ const specSchema = z.object({
|
||||
clientType: z.enum(ClientTypeEnum).optional(),
|
||||
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<typeof specSchema> {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -52,6 +52,55 @@ class CloudflareService extends EventEmitter<CloudflareServiceEvents> {
|
||||
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user