This commit is contained in:
Morten Olsen
2025-08-22 11:44:53 +02:00
parent 1b5b5145b0
commit e8e939ad19
16 changed files with 166 additions and 95 deletions

View File

@@ -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" # --mount ${PWD}/data:/data:w colima start --network-address --kubernetes -m 8 --k3s-arg="--disable helm-controller,local-storage,traefik --docker" # --mount ${PWD}/data:/data:w
flux install --components="source-controller,helm-controller" flux install --components="source-controller,helm-controller"
setup-flux: setup-flux:

View File

@@ -1,19 +0,0 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: lets-encrypt-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
key: token

View File

@@ -1,9 +0,0 @@
```yaml
kind: Environment
metadata:
name: dev
spec:
domain: one.dev.olsen.cloud
tls:
issuer: lets-encrypt-prod
```

View File

@@ -1,8 +0,0 @@
```
kind: OidcClient
metadata:
name: demo
namespace: dev-demo
spec:
env: dev
```

View File

@@ -1,8 +0,0 @@
```yaml
kind: PostgresDatabase
metadata:
name: demo
namespace: dev-demo
spec:
env: dev
```

View File

@@ -9,5 +9,6 @@ metadata:
name: dev name: dev
spec: spec:
domain: one.dev.olsen.cloud domain: one.dev.olsen.cloud
networkIp: 192.168.107.2
tls: tls:
issuer: lets-encrypt-prod issuer: lets-encrypt-prod

View File

@@ -22,6 +22,7 @@
"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", "cron": "^4.3.3",
"debounce": "^2.2.0", "debounce": "^2.2.0",
"deep-equal": "^2.2.3", "deep-equal": "^2.2.3",

73
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ 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: cron:
specifier: ^4.3.3 specifier: ^4.3.3
version: 4.3.3 version: 4.3.3
@@ -238,6 +241,9 @@ packages:
'@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==}
@@ -309,6 +315,10 @@ 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:
@@ -485,6 +495,9 @@ 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'}
@@ -764,6 +777,10 @@ 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==}
@@ -835,10 +852,17 @@ 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==}
@@ -1353,6 +1377,11 @@ 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}
@@ -1900,6 +1929,9 @@ 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==}
@@ -1919,6 +1951,10 @@ 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==}
@@ -2150,6 +2186,10 @@ snapshots:
'@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
@@ -2256,6 +2296,10 @@ 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
@@ -2274,7 +2318,6 @@ 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:
@@ -2479,6 +2522,18 @@ 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
@@ -2849,6 +2904,8 @@ 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:
@@ -2924,6 +2981,8 @@ 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
@@ -2932,6 +2991,11 @@ 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:
@@ -3085,7 +3149,6 @@ 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:
@@ -3463,6 +3526,8 @@ 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
@@ -4121,6 +4186,8 @@ 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: {}
@@ -4141,6 +4208,8 @@ 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:

View File

@@ -1,4 +0,0 @@
for f in "./test-manifests/"*; do
echo "Applying $f"
kubectl apply -f "$f"
done

View File

@@ -1,20 +0,0 @@
#!/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}"

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env node
import { K8sService } from '../src/services/k8s/k8s.ts';
import { Services } from '../src/utils/service.ts';
const services = new Services();
const k8s = services.get(K8sService);
const manifests = await k8s.extensionsApi.listCustomResourceDefinition();
for (const manifest of manifests.items) {
for (const version of manifest.spec.versions) {
console.log(`group: ${manifest.spec.group}, plural: ${manifest.spec.names.plural}, version: ${version.name}`);
}
}

View File

@@ -1,3 +0,0 @@
#!/bin/bash
flux install --components="source-controller,helm-controller"
kubectl create namespace homelab

View File

@@ -1,7 +1,7 @@
apiVersion: skaffold/v4beta7 apiVersion: skaffold/v4beta7
kind: Config kind: Config
metadata: metadata:
name: my-utility-service name: homelab-operator
build: build:
# This tells Skaffold to build the image locally using your Docker daemon. # This tells Skaffold to build the image locally using your Docker daemon.

View File

@@ -13,9 +13,11 @@ import { PROVISIONER } from '#resources/core/pvc/pvc.ts';
import { Gateway } from '#resources/istio/gateway/gateway.ts'; 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';
const specSchema = z.object({ const specSchema = z.object({
domain: z.string(), domain: z.string(),
networkIp: z.string().optional(),
tls: z.object({ tls: z.object({
issuer: z.string(), issuer: z.string(),
}), }),
@@ -34,31 +36,36 @@ class Environment extends CustomResource<typeof specSchema> {
#postgresCluster: PostgresCluster; #postgresCluster: PostgresCluster;
#redisServer: RedisServer; #redisServer: RedisServer;
#authentikServer: AuthentikServer; #authentikServer: AuthentikServer;
#cloudflareService: CloudflareService;
constructor(options: CustomResourceOptions<typeof specSchema>) { constructor(options: CustomResourceOptions<typeof specSchema>) {
super(options); super(options);
const resourceService = this.services.get(ResourceService); const resourceService = this.services.get(ResourceService);
const namespaceService = this.services.get(NamespaceService); const namespaceService = this.services.get(NamespaceService);
const homelabNamespace = namespaceService.homelab.name;
this.#cloudflareService = this.services.get(CloudflareService);
this.#cloudflareService.on('changed', this.queueReconcile);
this.#namespace = resourceService.get(Namespace, this.name); this.#namespace = resourceService.get(Namespace, this.name);
this.#namespace.on('changed', this.queueReconcile); this.#namespace.on('changed', this.queueReconcile);
this.#certificate = resourceService.get(Certificate, this.name, namespaceService.homelab.name); this.#certificate = resourceService.get(Certificate, this.name, homelabNamespace);
this.#certificate.on('changed', this.queueReconcile); this.#certificate.on('changed', this.queueReconcile);
this.#storageClass = resourceService.get(StorageClass, this.name); this.#storageClass = resourceService.get(StorageClass, this.name);
this.#storageClass.on('changed', this.queueReconcile); this.#storageClass.on('changed', this.queueReconcile);
this.#postgresCluster = resourceService.get(PostgresCluster, `${this.name}-postgres-cluster`, this.name); this.#postgresCluster = resourceService.get(PostgresCluster, `${this.name}-postgres-cluster`, homelabNamespace);
this.#postgresCluster.on('changed', this.queueReconcile); this.#postgresCluster.on('changed', this.queueReconcile);
this.#redisServer = resourceService.get(RedisServer, `${this.name}-redis-server`, this.name); this.#redisServer = resourceService.get(RedisServer, `${this.name}-redis-server`, homelabNamespace);
this.#redisServer.on('changed', this.queueReconcile); this.#redisServer.on('changed', this.queueReconcile);
this.#gateway = resourceService.get(Gateway, this.name, this.name); this.#gateway = resourceService.get(Gateway, this.name, homelabNamespace);
this.#gateway.on('changed', this.queueReconcile); this.#gateway.on('changed', this.queueReconcile);
this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, this.name); this.#authentikServer = resourceService.get(AuthentikServer, `${this.name}-authentik`, homelabNamespace);
this.#authentikServer.on('changed', this.queueReconcile); this.#authentikServer.on('changed', this.queueReconcile);
} }
@@ -91,6 +98,28 @@ class Environment extends CustomResource<typeof specSchema> {
if (!success || !spec) { if (!success || !spec) {
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: {

View File

@@ -0,0 +1,57 @@
import { Cloudflare } from 'cloudflare';
import { EventEmitter } from 'eventemitter3';
import { NamespaceService } from '#bootstrap/namespaces/namespaces.ts';
import { Secret } from '#resources/core/secret/secret.ts';
import { ResourceService } from '#services/resources/resources.ts';
import type { Services } from '#utils/service.ts';
type SecretData = {
account: string;
tunnelName: string;
tunnelId: string;
secret: string;
token: string;
};
type CloudflareServiceEvents = {
changed: () => void;
};
class CloudflareService extends EventEmitter<CloudflareServiceEvents> {
#services: Services;
#secret: Secret<SecretData>;
constructor(services: Services) {
super();
this.#services = services;
const resourceService = this.#services.get(ResourceService);
const namespaceService = this.#services.get(NamespaceService);
this.#secret = resourceService.get(Secret<SecretData>, 'cloudflare', namespaceService.homelab.name);
this.#secret.on('changed', this.emit.bind(this, 'changed'));
}
public get secret() {
return this.#secret.value;
}
public get ready() {
return !!this.secret;
}
public get client() {
const token = this.#secret.value?.token;
if (!token) {
throw new Error('Cloudflare API token is not set');
}
const client = new Cloudflare({
apiToken: token,
});
return client;
}
}
export { CloudflareService };