Compare commits

...

3 Commits

Author SHA1 Message Date
Morten Olsen
00d90bfa21 more-stuff 2025-09-03 17:24:27 +02:00
Morten Olsen
03e406322f more stuff 2025-09-03 15:16:50 +02:00
Morten Olsen
5ee7a76443 more stuff 2025-09-03 14:33:48 +02:00
39 changed files with 581 additions and 63 deletions

View File

@@ -5,5 +5,6 @@ metadata:
spec: spec:
environment: '{{ .Values.environment }}' environment: '{{ .Values.environment }}'
redirectUris: redirectUris:
- url: https://localhost:3000/api/v1/authentik/oauth2/callback - path: /api/auth/oidc/callback
subdomain: bytestash
matchingMode: strict matchingMode: strict

View File

@@ -4,7 +4,7 @@ metadata:
name: '{{ .Release.Name }}' name: '{{ .Release.Name }}'
spec: spec:
environment: '{{ .Values.environment }}' environment: '{{ .Values.environment }}'
subdomain: '{{ .Values.subdomain }}-external' subdomain: '{{ .Values.subdomain }}'
destination: destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local' host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port: port:

View File

@@ -42,11 +42,6 @@ spec:
name: '{{ .Release.Name }}-client' name: '{{ .Release.Name }}-client'
key: configuration key: configuration
# !! IMPORTANT !!
# You MUST update this Redirect URI to match your external URL.
# This URI must also be configured in your Authentik provider settings for this client.
#- name: BS_OIDC_REDIRECT_URI
#value: 'https://bytestash.your-domain.com/login/oauth2/code/oidc'
volumeMounts: volumeMounts:
- mountPath: /data/snippets - mountPath: /data/snippets
name: bytestash-data name: bytestash-data

View File

@@ -1,2 +1,2 @@
environment: dev environment: prod
subdomain: bytestash subdomain: bytestash

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: Jellyfin

View File

@@ -0,0 +1 @@
https://www.authelia.com/integration/openid-connect/clients/jellyfin/

View File

@@ -0,0 +1,10 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.environment }}'
redirectUris:
- path: /sso/OID/redirect/Authentik
subdomain: '{{ .Values.subdomain }}'
matchingMode: strict

View File

@@ -0,0 +1,11 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-config'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.environment }}'

View File

@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
ports:
- name: http
containerPort: 8096
protocol: TCP
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumeMounts:
- mountPath: /config
name: config
- mountPath: /media/movies
name: movies
- mountPath: /media/tv-shows
name: tvshows
- mountPath: /media/music
name: music
volumes:
- name: config
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-config'
- name: movies
persistentVolumeClaim:
claimName: movies
- name: tvshows
persistentVolumeClaim:
claimName: tvshows
- name: music
persistentVolumeClaim:
claimName: music

View 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

View 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 }}'

View File

@@ -0,0 +1,6 @@
image:
repository: docker.io/jellyfin/jellyfin
tag: latest
pullPolicy: IfNotPresent
environment: prod
subdomain: jellyfin

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: Ollama

View 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 prestart 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'

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: '{{ .Release.Name }}-data'
spec:
storageClassName: '{{ .Values.environment }}'
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi

View 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 }}'

View File

@@ -0,0 +1,2 @@
environment: dev
subdomain: bytestash

0
charts/apps/values.yaml Normal file
View File

3
charts/root/Chart.yaml Normal file
View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: root

View File

@@ -0,0 +1,33 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: homelab-apps
namespace: '{{ .Values.env }}-argo'
spec:
generators:
- git:
repoURL: '{{ .Values.repo }}'
revision: '{{ .Values.ref }}'
directories:
- path: charts/apps/*
include: '.*'
exclude: '.*.disabled'
template:
metadata:
name: '{{`{{path.basename}}`}}'
spec:
project: default
source:
repoURL: '{{ .Values.repo }}'
targetRevision: '{{ .Values.ref }}'
path: charts/apps/{{`{{path.basename}}`}}
helm:
values: |
globals: {{ .Values.globals | toYaml | nindent 14 }}
destination:
server: https://kubernetes.default.svc
namespace: '{{ .Values.globals.env }}'
syncPolicy:
automated:
prune: true
selfHeal: true

View File

@@ -0,0 +1,21 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: homelab-root
namespace: '{{ .Values.globals.env }}-argo'
spec:
project: default
source:
repoURL: '{{ .Values.repo }}'
targetRevision: '{{ .Values.ref }}'
path: charts/root
helm:
valueFiles:
- values.yaml
destination:
server: https://kubernetes.default.svc
namespace: '{{ .Values.globals.env }}-argo'
syncPolicy:
automated:
prune: true
selfHeal: true

4
charts/root/values.yaml Normal file
View File

@@ -0,0 +1,4 @@
globals:
env: prod
repo: https://github.com/morten-olsen/homelab-operator.git
ref: HEAD

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: Resources

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,30 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: tvshows
labels:
type: nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath: null
nfs:
path: '{{ .Values.tvshows.path }}'
server: '{{ .Values.host }}'
readOnly: true
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: tvshows
spec:
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi

View File

@@ -0,0 +1,11 @@
host: 192.168.20.106
movies:
path: /mnt/HDD/Movies
tvshows:
path: /mnt/HDD/TV-Shows
music:
path: /mnt/HDD/Music2
books:
path: /mnt/HDD/Books
podcasts:
path: /mnt/HDD/Podcasts

22
istio-test.yaml Normal file
View 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

View File

@@ -6,9 +6,9 @@ metadata:
apiVersion: homelab.mortenolsen.pro/v1 apiVersion: homelab.mortenolsen.pro/v1
kind: Environment kind: Environment
metadata: metadata:
name: dev name: prod
spec: spec:
domain: one.dev.olsen.cloud domain: olsen.cloud
networkIp: 192.168.107.2 networkIp: 192.168.20.180
tls: tls:
issuer: lets-encrypt-prod issuer: lets-encrypt-prod

View File

@@ -4,16 +4,10 @@ metadata:
name: homelab-operator name: homelab-operator
build: build:
# This tells Skaffold to build the image locally using your Docker daemon. cluster: {}
local:
push: false
# This is the crucial part for your workflow. Instead of pushing to a
# registry, it loads the built image directly into your cluster's nodes.
# load: true
artifacts: artifacts:
# Defines the image to build. It matches the placeholder in deployment.yaml.
- image: homelaboperator - image: homelaboperator
context: . # The build context is the root directory context: .
docker: docker:
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@@ -9,6 +9,7 @@ class RepoService {
#istio: HelmRepo; #istio: HelmRepo;
#authentik: HelmRepo; #authentik: HelmRepo;
#cloudflare: HelmRepo; #cloudflare: HelmRepo;
#argo: HelmRepo;
constructor(services: Services) { constructor(services: Services) {
const resourceService = services.get(ResourceService); const resourceService = services.get(ResourceService);
@@ -16,11 +17,13 @@ class RepoService {
this.#istio = resourceService.get(HelmRepo, 'istio', NAMESPACE); this.#istio = resourceService.get(HelmRepo, 'istio', NAMESPACE);
this.#authentik = resourceService.get(HelmRepo, 'authentik', NAMESPACE); this.#authentik = resourceService.get(HelmRepo, 'authentik', NAMESPACE);
this.#cloudflare = resourceService.get(HelmRepo, 'cloudflare', NAMESPACE); this.#cloudflare = resourceService.get(HelmRepo, 'cloudflare', NAMESPACE);
this.#argo = resourceService.get(HelmRepo, 'argo', NAMESPACE);
this.#jetstack.on('changed', this.ensure); this.#jetstack.on('changed', this.ensure);
this.#istio.on('changed', this.ensure); this.#istio.on('changed', this.ensure);
this.#authentik.on('changed', this.ensure); this.#authentik.on('changed', this.ensure);
this.#cloudflare.on('changed', this.ensure); this.#cloudflare.on('changed', this.ensure);
this.#argo.on('changed', this.ensure);
} }
public get jetstack() { public get jetstack() {
@@ -39,6 +42,10 @@ class RepoService {
return this.#cloudflare; return this.#cloudflare;
} }
public get argo() {
return this.#argo;
}
public ensure = async () => { public ensure = async () => {
await this.#jetstack.set({ await this.#jetstack.set({
url: 'https://charts.jetstack.io', url: 'https://charts.jetstack.io',
@@ -55,6 +62,10 @@ class RepoService {
await this.#cloudflare.set({ await this.#cloudflare.set({
url: 'https://cloudflare.github.io/helm-charts', url: 'https://cloudflare.github.io/helm-charts',
}); });
await this.#argo.set({
url: 'https://argoproj.github.io/argo-helm',
});
}; };
} }

View File

@@ -15,9 +15,9 @@ import { generateRandomHexPass } from '#utils/secrets.ts';
import { Service } from '#resources/core/service/service.ts'; import { Service } from '#resources/core/service/service.ts';
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts'; import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
import { RepoService } from '#bootstrap/repos/repos.ts'; import { RepoService } from '#bootstrap/repos/repos.ts';
import { VirtualService } from '#resources/istio/virtual-service/virtual-service.ts';
import { DestinationRule } from '#resources/istio/destination-rule/destination-rule.ts'; import { DestinationRule } from '#resources/istio/destination-rule/destination-rule.ts';
import { NotReadyError } from '#utils/errors.ts'; import { NotReadyError } from '#utils/errors.ts';
import { ExternalHttpService } from '../external-http-service.ts/external-http-service.ts';
const specSchema = z.object({ const specSchema = z.object({
environment: z.string(), environment: z.string(),
@@ -44,7 +44,7 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
#initSecret: Secret<InitSecretData>; #initSecret: Secret<InitSecretData>;
#service: Service; #service: Service;
#helmRelease: HelmRelease; #helmRelease: HelmRelease;
#virtualService: VirtualService; #externalHttpService: ExternalHttpService;
#destinationRule: DestinationRule; #destinationRule: DestinationRule;
constructor(options: CustomResourceOptions<typeof specSchema>) { constructor(options: CustomResourceOptions<typeof specSchema>) {
@@ -68,11 +68,10 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
this.#helmRelease = resourceService.get(HelmRelease, this.name, this.namespace); this.#helmRelease = resourceService.get(HelmRelease, this.name, this.namespace);
this.#helmRelease.on('changed', this.queueReconcile); this.#helmRelease.on('changed', this.queueReconcile);
this.#virtualService = resourceService.get(VirtualService, this.name, this.namespace);
this.#virtualService.on('changed', this.queueReconcile);
this.#destinationRule = resourceService.get(DestinationRule, this.name, this.namespace); this.#destinationRule = resourceService.get(DestinationRule, this.name, this.namespace);
this.#destinationRule.on('changed', this.queueReconcile); this.#destinationRule.on('changed', this.queueReconcile);
this.#externalHttpService = resourceService.get(ExternalHttpService, this.name, this.namespace);
} }
public get service() { public get service() {
@@ -254,18 +253,13 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
}, },
}); });
const gateway = this.#environment.current.gateway; await this.#externalHttpService.ensure({
await this.#virtualService.set({
metadata: { metadata: {
ownerReferences: [this.ref], ownerReferences: [this.ref],
}, },
spec: { spec: {
gateways: [`${gateway.namespace}/${gateway.name}`, 'mesh'], environment: this.spec.environment,
hosts: [domain], subdomain: this.spec.subdomain || 'authentik',
http: [
{
route: [
{
destination: { destination: {
host: this.#service.hostname, host: this.#service.hostname,
port: { port: {
@@ -273,10 +267,6 @@ class AuthentikServer extends CustomResource<typeof specSchema> {
}, },
}, },
}, },
],
},
],
},
}); });
}; };
} }

View File

@@ -12,6 +12,7 @@ import { RepoService } from '#bootstrap/repos/repos.ts';
import { Secret } from '#resources/core/secret/secret.ts'; import { Secret } from '#resources/core/secret/secret.ts';
import { NotReadyError } from '#utils/errors.ts'; import { NotReadyError } from '#utils/errors.ts';
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts'; import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
import { CloudflareService } from '#services/cloudflare/cloudflare.ts';
const specSchema = z.object({}); const specSchema = z.object({});
@@ -29,6 +30,7 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
#helmRelease: HelmRelease; #helmRelease: HelmRelease;
#secret: Secret<SecretData>; #secret: Secret<SecretData>;
#cloudflareService;
constructor(options: CustomResourceOptions<typeof specSchema>) { constructor(options: CustomResourceOptions<typeof specSchema>) {
super(options); super(options);
@@ -40,6 +42,9 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
this.#helmRelease = resourceService.get(HelmRelease, this.name, namespace); this.#helmRelease = resourceService.get(HelmRelease, this.name, namespace);
this.#secret = resourceService.get(Secret<SecretData>, 'cloudflare', namespace); this.#secret = resourceService.get(Secret<SecretData>, 'cloudflare', namespace);
this.#secret.on('changed', this.queueReconcile); this.#secret.on('changed', this.queueReconcile);
this.#cloudflareService = this.services.get(CloudflareService);
this.#cloudflareService.on('changed', this.queueReconcile);
} }
#handleResourceChanged = (resource: Resource<ExpectedAny>) => { #handleResourceChanged = (resource: Resource<ExpectedAny>) => {
@@ -60,6 +65,18 @@ class CloudflareTunnel extends CustomResource<typeof specSchema> {
hostname: rule?.hostname, hostname: rule?.hostname,
service: `http://${rule?.destination.host}:${rule?.destination.port.number}`, service: `http://${rule?.destination.host}:${rule?.destination.port.number}`,
})); }));
if (this.#cloudflareService.ready) {
for (const route of ingress) {
if (!route.hostname) {
continue;
}
try {
await this.#cloudflareService.ensureTunnel(route.hostname);
} catch (err) {
console.error(err);
}
}
}
await this.#helmRelease.ensure({ await this.#helmRelease.ensure({
metadata: { metadata: {
ownerReferences: [this.ref], ownerReferences: [this.ref],

View File

@@ -14,6 +14,8 @@ import { Gateway } from '#resources/istio/gateway/gateway.ts';
import { NotReadyError } from '#utils/errors.ts'; import { NotReadyError } from '#utils/errors.ts';
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts'; import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
import { CloudflareService } from '#services/cloudflare/cloudflare.ts'; import { CloudflareService } from '#services/cloudflare/cloudflare.ts';
import { HelmRelease } from '#resources/flux/helm-release/helm-release.ts';
import { RepoService } from '#bootstrap/repos/repos.ts';
const specSchema = z.object({ const specSchema = z.object({
domain: z.string(), domain: z.string(),
@@ -37,6 +39,8 @@ class Environment extends CustomResource<typeof specSchema> {
#redisServer: RedisServer; #redisServer: RedisServer;
#authentikServer: AuthentikServer; #authentikServer: AuthentikServer;
#cloudflareService: CloudflareService; #cloudflareService: CloudflareService;
#argoRelease: HelmRelease;
#argoNamespace: Namespace;
constructor(options: CustomResourceOptions<typeof specSchema>) { constructor(options: CustomResourceOptions<typeof specSchema>) {
super(options); super(options);
@@ -67,6 +71,11 @@ class Environment extends CustomResource<typeof specSchema> {
this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, homelabNamespace); this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, homelabNamespace);
this.#authentikServer.on('changed', this.queueReconcile); this.#authentikServer.on('changed', this.queueReconcile);
this.#argoNamespace = resourceService.get(Namespace, `${this.name}-argo`);
this.#argoRelease = resourceService.get(HelmRelease, `${this.name}-argo`, homelabNamespace);
this.#argoRelease.on('changed', this.queueReconcile);
} }
public get certificate() { public get certificate() {
@@ -99,27 +108,6 @@ class Environment extends CustomResource<typeof specSchema> {
throw new NotReadyError('InvalidSpec'); throw new NotReadyError('InvalidSpec');
} }
if (this.#cloudflareService.ready && spec.networkIp) {
const client = this.#cloudflareService.client;
const zones = await client.zones.list({
name: spec.domain,
});
const [zone] = zones.result;
if (!zone) {
throw new NotReadyError('NoZoneFound');
}
const existingRecords = await client.dns.records.list({
zone_id: zone.id,
name: {
exact: `*.${spec.domain}`,
},
});
console.log('Cloudflare records', existingRecords);
// zones.result[0].
}
await this.#namespace.ensure({ await this.#namespace.ensure({
metadata: { metadata: {
labels: { labels: {
@@ -208,6 +196,33 @@ 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: {
applicationset: {
enabled: true,
},
},
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,
},
},
},
},
});
}; };
} }

View File

@@ -19,7 +19,8 @@ const specSchema = z.object({
clientType: z.enum(ClientTypeEnum).optional(), clientType: z.enum(ClientTypeEnum).optional(),
redirectUris: z.array( redirectUris: z.array(
z.object({ z.object({
url: z.string(), subdomain: z.string(),
path: z.string(),
matchingMode: z.enum(['strict', 'regex']), matchingMode: z.enum(['strict', 'regex']),
}), }),
), ),
@@ -98,8 +99,14 @@ class OIDCClient extends CustomResource<typeof specSchema> {
token: authentikSecret.token, token: authentikSecret.token,
}); });
const redirectUris = this.spec.redirectUris.map((uri) => ({
matchingMode: uri.matchingMode,
url: new URL(uri.path, `https://${uri.subdomain}.${this.#environment.current?.spec?.domain}`).toString(),
}));
await authentikServer.upsertClient({ await authentikServer.upsertClient({
...this.spec, ...this.spec,
redirectUris,
name: this.name, name: this.name,
secret: secret.clientSecret, secret: secret.clientSecret,
}); });

View File

@@ -52,6 +52,55 @@ class CloudflareService extends EventEmitter<CloudflareServiceEvents> {
return client; return client;
} }
public ensureTunnel = async (route: string) => {
const secret = this.#secret.value;
if (!secret) {
return;
}
const client = this.client;
const domainParts = route.split('.');
const cname = `${secret.tunnelId}.cfargotunnel.com`;
const tld = domainParts.pop();
const root = domainParts.pop();
const zoneName = `${root}.${tld}`;
const name = domainParts.join('.');
const zones = await client.zones.list({
name: zoneName,
});
const [zone] = zones.result;
if (!zone) {
return;
}
const records = await client.dns.records.list({
zone_id: zone.id,
name: {
exact: route,
},
type: 'CNAME',
});
const [record] = records.result;
if (record) {
await client.dns.records.edit(record.id, {
zone_id: zone.id,
type: 'CNAME',
content: cname,
name: name,
ttl: 1,
proxied: true,
});
} else {
await client.dns.records.create({
zone_id: zone.id,
type: 'CNAME',
content: cname,
name: name,
ttl: 1,
proxied: true,
});
}
};
} }
export { CloudflareService }; export { CloudflareService };