From e8e939ad1959833f834bc9371b6008292c4ddf6e Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Fri, 22 Aug 2025 11:44:53 +0200 Subject: [PATCH] fixes --- Makefile | 2 +- cert-issuer.yaml | 19 ----- docs/resources/environment.md | 9 --- docs/resources/http-service.md | 0 docs/resources/oidc-client.md | 8 -- docs/resources/postgres-database.md | 8 -- manifests/environment.yaml | 1 + package.json | 1 + pnpm-lock.yaml | 73 ++++++++++++++++++- scripts/apply-test.sh | 4 - scripts/create-secrets.sh | 20 ----- scripts/list-manifests.ts | 15 ---- scripts/setup-server.sh | 3 - skaffold.yaml | 2 +- .../homelab/environment/environment.ts | 39 ++++++++-- src/services/cloudflare/cloudflare.ts | 57 +++++++++++++++ 16 files changed, 166 insertions(+), 95 deletions(-) delete mode 100644 cert-issuer.yaml delete mode 100644 docs/resources/environment.md delete mode 100644 docs/resources/http-service.md delete mode 100644 docs/resources/oidc-client.md delete mode 100644 docs/resources/postgres-database.md delete mode 100755 scripts/apply-test.sh delete mode 100755 scripts/create-secrets.sh delete mode 100755 scripts/list-manifests.ts delete mode 100755 scripts/setup-server.sh create mode 100644 src/services/cloudflare/cloudflare.ts diff --git a/Makefile b/Makefile index 6801232..4577849 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ dev-destroy: colima delete -f 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" setup-flux: diff --git a/cert-issuer.yaml b/cert-issuer.yaml deleted file mode 100644 index bcdc7c3..0000000 --- a/cert-issuer.yaml +++ /dev/null @@ -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 diff --git a/docs/resources/environment.md b/docs/resources/environment.md deleted file mode 100644 index 1a302ee..0000000 --- a/docs/resources/environment.md +++ /dev/null @@ -1,9 +0,0 @@ -```yaml -kind: Environment -metadata: - name: dev -spec: - domain: one.dev.olsen.cloud - tls: - issuer: lets-encrypt-prod -``` diff --git a/docs/resources/http-service.md b/docs/resources/http-service.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/oidc-client.md b/docs/resources/oidc-client.md deleted file mode 100644 index 9eed270..0000000 --- a/docs/resources/oidc-client.md +++ /dev/null @@ -1,8 +0,0 @@ -``` -kind: OidcClient -metadata: - name: demo - namespace: dev-demo -spec: - env: dev -``` diff --git a/docs/resources/postgres-database.md b/docs/resources/postgres-database.md deleted file mode 100644 index e9e8ae6..0000000 --- a/docs/resources/postgres-database.md +++ /dev/null @@ -1,8 +0,0 @@ -```yaml -kind: PostgresDatabase -metadata: - name: demo - namespace: dev-demo -spec: - env: dev -``` diff --git a/manifests/environment.yaml b/manifests/environment.yaml index 40a4180..38ed2d7 100644 --- a/manifests/environment.yaml +++ b/manifests/environment.yaml @@ -9,5 +9,6 @@ metadata: name: dev spec: domain: one.dev.olsen.cloud + networkIp: 192.168.107.2 tls: issuer: lets-encrypt-prod diff --git a/package.json b/package.json index 54760a3..20cf473 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@goauthentik/api": "2025.6.3-1751754396", "@kubernetes/client-node": "^1.3.0", + "cloudflare": "^4.5.0", "cron": "^4.3.3", "debounce": "^2.2.0", "deep-equal": "^2.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ab0aaa..868027c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@kubernetes/client-node': specifier: ^1.3.0 version: 1.3.0(encoding@0.1.13) + cloudflare: + specifier: ^4.5.0 + version: 4.5.0(encoding@0.1.13) cron: specifier: ^4.3.3 version: 4.3.3 @@ -238,6 +241,9 @@ packages: '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node@18.19.123': + resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==} + '@types/node@22.16.5': resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} @@ -309,6 +315,10 @@ packages: abbrev@1.1.1: 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: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -485,6 +495,9 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + cloudflare@4.5.0: + resolution: {integrity: sha512-fPcbPKx4zF45jBvQ0z7PCdgejVAPBBCZxwqk1k7krQNfpM07Cfj97/Q6wBzvYqlWXx/zt1S9+m8vnfCe06umbQ==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -764,6 +777,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 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: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -835,10 +852,17 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -1353,6 +1377,11 @@ packages: node-addon-api@7.1.1: 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: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -1900,6 +1929,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -1919,6 +1951,10 @@ packages: util-deprecate@1.0.2: 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: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -2150,6 +2186,10 @@ snapshots: '@types/node': 22.16.5 form-data: 4.0.4 + '@types/node@18.19.123': + dependencies: + undici-types: 5.26.5 + '@types/node@22.16.5': dependencies: undici-types: 6.21.0 @@ -2256,6 +2296,10 @@ snapshots: abbrev@1.1.1: optional: true + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -2274,7 +2318,6 @@ snapshots: agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 - optional: true aggregate-error@3.1.0: dependencies: @@ -2479,6 +2522,18 @@ snapshots: clean-stack@2.2.0: 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: dependencies: color-name: 1.1.4 @@ -2849,6 +2904,8 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + eventemitter3@5.0.1: {} execa@9.6.0: @@ -2924,6 +2981,8 @@ snapshots: dependencies: is-callable: 1.2.7 + form-data-encoder@1.7.2: {} + form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -2932,6 +2991,11 @@ snapshots: hasown: 2.0.2 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-minipass@2.1.0: @@ -3085,7 +3149,6 @@ snapshots: humanize-ms@1.2.1: dependencies: ms: 2.1.3 - optional: true iconv-lite@0.6.3: dependencies: @@ -3463,6 +3526,8 @@ snapshots: node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -4121,6 +4186,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@5.26.5: {} + undici-types@6.21.0: {} unicorn-magic@0.3.0: {} @@ -4141,6 +4208,8 @@ snapshots: util-deprecate@1.0.2: {} + web-streams-polyfill@4.0.0-beta.3: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: diff --git a/scripts/apply-test.sh b/scripts/apply-test.sh deleted file mode 100755 index cfbfe3d..0000000 --- a/scripts/apply-test.sh +++ /dev/null @@ -1,4 +0,0 @@ -for f in "./test-manifests/"*; do - echo "Applying $f" - kubectl apply -f "$f" -done diff --git a/scripts/create-secrets.sh b/scripts/create-secrets.sh deleted file mode 100755 index 6644134..0000000 --- a/scripts/create-secrets.sh +++ /dev/null @@ -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}" diff --git a/scripts/list-manifests.ts b/scripts/list-manifests.ts deleted file mode 100755 index b19dcf6..0000000 --- a/scripts/list-manifests.ts +++ /dev/null @@ -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}`); - } -} diff --git a/scripts/setup-server.sh b/scripts/setup-server.sh deleted file mode 100755 index 8bc4246..0000000 --- a/scripts/setup-server.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -flux install --components="source-controller,helm-controller" -kubectl create namespace homelab \ No newline at end of file diff --git a/skaffold.yaml b/skaffold.yaml index 2ffa7fb..1a09589 100644 --- a/skaffold.yaml +++ b/skaffold.yaml @@ -1,7 +1,7 @@ apiVersion: skaffold/v4beta7 kind: Config metadata: - name: my-utility-service + name: homelab-operator build: # This tells Skaffold to build the image locally using your Docker daemon. diff --git a/src/resources/homelab/environment/environment.ts b/src/resources/homelab/environment/environment.ts index 5d2e938..453be8c 100644 --- a/src/resources/homelab/environment/environment.ts +++ b/src/resources/homelab/environment/environment.ts @@ -13,9 +13,11 @@ import { PROVISIONER } from '#resources/core/pvc/pvc.ts'; 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'; const specSchema = z.object({ domain: z.string(), + networkIp: z.string().optional(), tls: z.object({ issuer: z.string(), }), @@ -34,31 +36,36 @@ class Environment extends CustomResource { #postgresCluster: PostgresCluster; #redisServer: RedisServer; #authentikServer: AuthentikServer; + #cloudflareService: CloudflareService; constructor(options: CustomResourceOptions) { super(options); const resourceService = this.services.get(ResourceService); 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.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.#storageClass = resourceService.get(StorageClass, this.name); 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.#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.#gateway = resourceService.get(Gateway, this.name, this.name); + this.#gateway = resourceService.get(Gateway, this.name, homelabNamespace); 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); } @@ -91,6 +98,28 @@ class Environment extends CustomResource { if (!success || !spec) { 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: { diff --git a/src/services/cloudflare/cloudflare.ts b/src/services/cloudflare/cloudflare.ts new file mode 100644 index 0000000..cc1e97b --- /dev/null +++ b/src/services/cloudflare/cloudflare.ts @@ -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 { + #services: Services; + #secret: Secret; + + constructor(services: Services) { + super(); + this.#services = services; + const resourceService = this.#services.get(ResourceService); + const namespaceService = this.#services.get(NamespaceService); + this.#secret = resourceService.get(Secret, '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 };