From aa6d14738aa54ffc51661d6a3cde2367d09e8cfe Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Thu, 7 Aug 2025 23:10:29 +0200 Subject: [PATCH] simplify --- chart/templates/deployment.yaml | 61 --- chart/values.yaml | 52 +-- .../authentik-client.create-manifest.ts | 0 .../authentik-client.resource.ts | 192 ++++----- .../authentik-client.schemas.ts | 14 +- .../authentik-server.create-manifests.ts | 176 -------- .../authentik-server.resource.ts | 382 ------------------ .../authentik-server.scemas.ts | 16 - .../authentik-server/authentik-server.ts | 19 - src/custom-resouces/custom-resources.ts | 22 +- .../domain-service.create-manifests.ts | 86 ---- .../domain-service/domain-service.resource.ts | 169 -------- .../domain-service/domain-service.schemas.ts | 15 - .../domain-service/domain-service.ts | 19 - .../domain/domain.create-manifests.ts | 73 ---- src/custom-resouces/domain/domain.resource.ts | 174 -------- src/custom-resouces/domain/domain.schemas.ts | 8 - src/custom-resouces/domain/domain.ts | 19 - .../generate-secret.resource.ts | 61 +++ .../generate-secret.schemas.ts | 17 + .../generate-secret/generate-secret.ts | 19 + .../generate-secret/generate-secret.utils.ts | 69 ++++ .../homelab/homelab.manifests.ts | 297 -------------- .../homelab/homelab.resource.ts | 263 ------------ .../homelab/homelab.schemas.ts | 17 - src/custom-resouces/homelab/homelab.ts | 19 - .../postgres-cluster.manifests.ts | 124 ------ .../postgres-cluster.resource.ts | 170 -------- .../postgres-cluster.schemas.ts | 5 - .../postgres-cluster/postgres-cluster.ts | 19 - .../posgtres-connection.schemas.ts | 14 - .../postgres-connection.resource.ts | 94 ----- .../postgres-connection.ts | 19 - .../portgres-database.schemas.ts | 19 +- .../postgres-database.resource.ts | 119 +++--- .../redis-connection.resource.ts | 61 --- .../redis-connection.schemas.ts | 14 - .../redis-connection/redis-connection.ts | 19 - .../redis-server/redis-server.manifests.ts | 82 ---- .../redis-server/redis-server.resource.ts | 138 ------- .../redis-server/redis-server.schemas.ts | 5 - .../redis-server/redis-server.ts | 19 - src/index.ts | 9 - src/services/authentik/authentik.types.ts | 2 +- src/services/istio/istio.ts | 48 --- test-manifests/authentik-client.yaml | 10 - test-manifests/authentik-server.yaml | 10 - test-manifests/domain-service.yaml | 12 - test-manifests/domain.yaml | 8 - test-manifests/homelab.yaml | 9 - test-manifests/postgres-cluster.yaml | 6 - test-manifests/postgres-database.yaml | 7 - test-manifests/redis-database.yaml | 6 - 53 files changed, 333 insertions(+), 2974 deletions(-) delete mode 100644 src/custom-resouces/authentik-client/authentik-client.create-manifest.ts delete mode 100644 src/custom-resouces/authentik-server/authentik-server.create-manifests.ts delete mode 100644 src/custom-resouces/authentik-server/authentik-server.resource.ts delete mode 100644 src/custom-resouces/authentik-server/authentik-server.scemas.ts delete mode 100644 src/custom-resouces/authentik-server/authentik-server.ts delete mode 100644 src/custom-resouces/domain-service/domain-service.create-manifests.ts delete mode 100644 src/custom-resouces/domain-service/domain-service.resource.ts delete mode 100644 src/custom-resouces/domain-service/domain-service.schemas.ts delete mode 100644 src/custom-resouces/domain-service/domain-service.ts delete mode 100644 src/custom-resouces/domain/domain.create-manifests.ts delete mode 100644 src/custom-resouces/domain/domain.resource.ts delete mode 100644 src/custom-resouces/domain/domain.schemas.ts delete mode 100644 src/custom-resouces/domain/domain.ts create mode 100644 src/custom-resouces/generate-secret/generate-secret.resource.ts create mode 100644 src/custom-resouces/generate-secret/generate-secret.schemas.ts create mode 100644 src/custom-resouces/generate-secret/generate-secret.ts create mode 100644 src/custom-resouces/generate-secret/generate-secret.utils.ts delete mode 100644 src/custom-resouces/homelab/homelab.manifests.ts delete mode 100644 src/custom-resouces/homelab/homelab.resource.ts delete mode 100644 src/custom-resouces/homelab/homelab.schemas.ts delete mode 100644 src/custom-resouces/homelab/homelab.ts delete mode 100644 src/custom-resouces/postgres-cluster/postgres-cluster.manifests.ts delete mode 100644 src/custom-resouces/postgres-cluster/postgres-cluster.resource.ts delete mode 100644 src/custom-resouces/postgres-cluster/postgres-cluster.schemas.ts delete mode 100644 src/custom-resouces/postgres-cluster/postgres-cluster.ts delete mode 100644 src/custom-resouces/postgres-connection/posgtres-connection.schemas.ts delete mode 100644 src/custom-resouces/postgres-connection/postgres-connection.resource.ts delete mode 100644 src/custom-resouces/postgres-connection/postgres-connection.ts delete mode 100644 src/custom-resouces/redis-connection/redis-connection.resource.ts delete mode 100644 src/custom-resouces/redis-connection/redis-connection.schemas.ts delete mode 100644 src/custom-resouces/redis-connection/redis-connection.ts delete mode 100644 src/custom-resouces/redis-server/redis-server.manifests.ts delete mode 100644 src/custom-resouces/redis-server/redis-server.resource.ts delete mode 100644 src/custom-resouces/redis-server/redis-server.schemas.ts delete mode 100644 src/custom-resouces/redis-server/redis-server.ts delete mode 100644 src/services/istio/istio.ts delete mode 100644 test-manifests/authentik-client.yaml delete mode 100644 test-manifests/authentik-server.yaml delete mode 100644 test-manifests/domain-service.yaml delete mode 100644 test-manifests/domain.yaml delete mode 100644 test-manifests/homelab.yaml delete mode 100644 test-manifests/postgres-cluster.yaml delete mode 100644 test-manifests/postgres-database.yaml delete mode 100644 test-manifests/redis-database.yaml diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index b8aa1d6..c44c51e 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -31,67 +31,6 @@ spec: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - # PostgreSQL Host - - name: POSTGRES_HOST - {{- if .Values.config.postgres.host.fromSecret.enabled }} - valueFrom: - secretKeyRef: - name: {{ .Values.config.postgres.host.fromSecret.secretName }} - key: {{ .Values.config.postgres.host.fromSecret.key }} - {{- else }} - value: {{ .Values.config.postgres.host.value | quote }} - {{- end }} - # PostgreSQL Port - - name: POSTGRES_PORT - {{- if .Values.config.postgres.port.fromSecret.enabled }} - valueFrom: - secretKeyRef: - name: {{ .Values.config.postgres.port.fromSecret.secretName }} - key: {{ .Values.config.postgres.port.fromSecret.key }} - {{- else }} - value: {{ .Values.config.postgres.port.value | quote }} - {{- end }} - # PostgreSQL User - - name: POSTGRES_USER - {{- if .Values.config.postgres.user.fromSecret.enabled }} - valueFrom: - secretKeyRef: - name: {{ .Values.config.postgres.user.fromSecret.secretName }} - key: {{ .Values.config.postgres.user.fromSecret.key }} - {{- else }} - value: {{ .Values.config.postgres.user.value | quote }} - {{- end }} - # PostgreSQL Password - - name: POSTGRES_PASSWORD - {{- if .Values.config.postgres.password.fromSecret.enabled }} - valueFrom: - secretKeyRef: - name: {{ .Values.config.postgres.password.fromSecret.secretName }} - key: {{ .Values.config.postgres.password.fromSecret.key }} - {{- else }} - value: {{ .Values.config.postgres.password.value | quote }} - {{- end }} - # Certificate Manager - - name: CERT_MANAGER - {{- if .Values.config.certManager.fromSecret.enabled }} - valueFrom: - secretKeyRef: - name: {{ .Values.config.certManager.fromSecret.secretName }} - key: {{ .Values.config.certManager.fromSecret.key }} - {{- else }} - value: {{ .Values.config.certManager.value | quote }} - {{- end }} - # Istio Gateway - - name: ISTIO_GATEWAY - {{- if .Values.config.istioGateway.fromSecret.enabled }} - valueFrom: - secretKeyRef: - name: {{ .Values.config.istioGateway.fromSecret.secretName }} - key: {{ .Values.config.istioGateway.fromSecret.key }} - {{- else }} - value: {{ .Values.config.istioGateway.value | quote }} - {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} diff --git a/chart/values.yaml b/chart/values.yaml index f15b893..7918b3d 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -50,54 +50,4 @@ nodeSelector: {} tolerations: [] -affinity: {} - -# Configuration for the homelab operator -config: - # PostgreSQL database configuration - postgres: - host: - # Direct value (used when fromSecret.enabled is false) - value: "127.0.0.1" - # Secret reference (used when fromSecret.enabled is true) - fromSecret: - enabled: false - secretName: "" - key: "POSTGRES_HOST" - - port: - value: "5432" - fromSecret: - enabled: false - secretName: "" - key: "POSTGRES_PORT" - - user: - value: "postgres" - fromSecret: - enabled: false - secretName: "" - key: "POSTGRES_USER" - - password: - value: "" - fromSecret: - enabled: true # Default to secret for sensitive data - secretName: "postgres-secret" - key: "POSTGRES_PASSWORD" - - # Certificate manager configuration - certManager: - value: "letsencrypt-prod" - fromSecret: - enabled: false - secretName: "" - key: "CERT_MANAGER" - - # Istio gateway configuration - istioGateway: - value: "istio-ingress" - fromSecret: - enabled: false - secretName: "" - key: "ISTIO_GATEWAY" +affinity: {} \ No newline at end of file diff --git a/src/custom-resouces/authentik-client/authentik-client.create-manifest.ts b/src/custom-resouces/authentik-client/authentik-client.create-manifest.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/custom-resouces/authentik-client/authentik-client.resource.ts b/src/custom-resouces/authentik-client/authentik-client.resource.ts index d2c3641..34599e5 100644 --- a/src/custom-resouces/authentik-client/authentik-client.resource.ts +++ b/src/custom-resouces/authentik-client/authentik-client.resource.ts @@ -1,36 +1,34 @@ import type { V1Secret } from '@kubernetes/client-node'; import type { z } from 'zod'; -import deepEqual from 'deep-equal'; import { CustomResource, - type CustomResourceObject, type CustomResourceOptions, type SubresourceResult, } from '../../services/custom-resources/custom-resources.custom-resource.ts'; import { ResourceReference } from '../../services/resources/resources.ref.ts'; import { ResourceService, type Resource } from '../../services/resources/resources.ts'; import { getWithNamespace } from '../../utils/naming.ts'; -import type { authentikServerSpecSchema } from '../authentik-server/authentik-server.scemas.ts'; -import type { domainSpecSchema } from '../domain/domain.schemas.ts'; import { decodeSecret, encodeSecret } from '../../utils/secrets.ts'; -import { API_VERSION, CONTROLLED_LABEL } from '../../utils/consts.ts'; +import { CONTROLLED_LABEL } from '../../utils/consts.ts'; +import { isDeepSubset } from '../../utils/objects.ts'; +import { AuthentikService } from '../../services/authentik/authentik.service.ts'; -import { authentikClientSecretSchema, type authentikClientSpecSchema } from './authentik-client.schemas.ts'; +import { + authentikClientSecretSchema, + authentikClientServerSecretSchema, + type authentikClientSpecSchema, +} from './authentik-client.schemas.ts'; class AuthentikClientResource extends CustomResource { - #serverResource: ResourceReference>; - #serverSecretResource: ResourceReference; - #domainResource: ResourceReference>; + #serverSecret: ResourceReference; #clientSecretResource: Resource; constructor(options: CustomResourceOptions) { super(options); const resourceService = this.services.get(ResourceService); - this.#serverResource = new ResourceReference(); - this.#serverSecretResource = new ResourceReference(); - this.#domainResource = new ResourceReference(); + this.#serverSecret = new ResourceReference(); this.#clientSecretResource = resourceService.get({ apiVersion: 'v1', kind: 'Secret', @@ -40,93 +38,45 @@ class AuthentikClientResource extends CustomResource { - const serverNames = getWithNamespace(this.spec.server, this.namespace); + const serverSecretNames = getWithNamespace(this.spec.secretRef, this.namespace); const resourceService = this.services.get(ResourceService); - this.#serverResource.current = resourceService.get({ - apiVersion: API_VERSION, - kind: 'AuthentikServer', - name: serverNames.name, - namespace: serverNames.namespace, - }); - this.#serverSecretResource.current = resourceService.get({ + this.#serverSecret.current = resourceService.get({ apiVersion: 'v1', kind: 'Secret', - name: `authentik-server-${serverNames.name}`, - namespace: serverNames.namespace, + name: serverSecretNames.name, + namespace: serverSecretNames.namespace, }); - const server = this.#serverResource.current; - if (server && server.spec) { - const domainNames = getWithNamespace(server.spec.domain, server.namespace); - this.#domainResource.current = resourceService.get({ - apiVersion: API_VERSION, - kind: 'Domain', - name: domainNames.name, - namespace: domainNames.namespace, - }); - } else { - this.#domainResource.current = undefined; - } }; #reconcileClientSecret = async (): Promise => { - const domain = this.domain; - const server = this.server; - const serverSecret = this.serverSecret; - if (!server?.exists || !server?.spec || !serverSecret?.exists || !serverSecret.data) { + const serverSecret = this.#serverSecret.current; + if (!serverSecret?.exists || !serverSecret.data) { return { ready: false, failed: true, message: 'Server or server secret not found', }; } - if (!domain?.exists || !domain?.spec) { + const serverSecretData = authentikClientServerSecretSchema.safeParse(decodeSecret(serverSecret.data)); + if (!serverSecretData.success || !serverSecretData.data) { return { ready: false, failed: true, - message: 'Domain not found', + message: 'Server secret not found', }; } - const url = `https://authentik.${domain.spec?.hostname}`; + const url = serverSecretData.data.external_url; const appName = this.name; - const values = this.clientSecretValue; - const expectedValues: Omit, 'clientSecret'> = { + const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(this.#clientSecretResource.data)); + + const expectedValues: z.infer = { clientId: this.name, + clientSecret: clientSecretData.data?.clientSecret || crypto.randomUUID(), configuration: new URL(`/application/o/${appName}/.well-known/openid-configuration`, url).toString(), configurationIssuer: new URL(`/application/o/${appName}/`, url).toString(), authorization: new URL(`/application/o/${appName}/authorize/`, url).toString(), @@ -135,31 +85,8 @@ class AuthentikClientResource extends CustomResource => { + const serverSecret = this.#serverSecret.current; + const clientSecret = this.#clientSecretResource; + + if (!serverSecret?.exists || !serverSecret.data) { + return { + ready: false, + failed: true, + message: 'Server secret not found', + }; + } + + const serverSecretData = authentikClientServerSecretSchema.safeParse(decodeSecret(serverSecret.data)); + if (!serverSecretData.success || !serverSecretData.data) { + return { + ready: false, + failed: true, + message: 'Server secret not found', + }; + } + + const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(clientSecret.data)); + if (!clientSecretData.success || !clientSecretData.data) { + return { + ready: false, + failed: true, + message: 'Client secret not found', + }; + } + + const authentikService = this.services.get(AuthentikService); + const authentikServer = authentikService.get({ + url: { + internal: serverSecretData.data.internal_url, + external: serverSecretData.data.external_url, + }, + token: serverSecretData.data.token, + }); + + (await authentikServer).upsertClient({ + ...this.spec, + name: this.name, + secret: clientSecretData.data.clientSecret, + }); + + return { + ready: true, + }; + }; + public reconcile = async () => { - if (!this.exists || this.metadata.deletionTimestamp) { + if (!this.exists || this.metadata?.deletionTimestamp) { return; } this.#updateResouces(); - await Promise.all([this.reconcileSubresource('Secret', this.#reconcileClientSecret)]); + await Promise.all([ + this.reconcileSubresource('Secret', this.#reconcileClientSecret), + this.reconcileSubresource('Server', this.#reconcileServer), + ]); const secretReady = this.conditions.get('Secret')?.status === 'True'; + const serverReady = this.conditions.get('Server')?.status === 'True'; await this.conditions.set('Ready', { - status: secretReady ? 'True' : 'False', + status: secretReady && serverReady ? 'True' : 'False', }); }; } diff --git a/src/custom-resouces/authentik-client/authentik-client.schemas.ts b/src/custom-resouces/authentik-client/authentik-client.schemas.ts index 43751a4..21c6dfc 100644 --- a/src/custom-resouces/authentik-client/authentik-client.schemas.ts +++ b/src/custom-resouces/authentik-client/authentik-client.schemas.ts @@ -1,18 +1,24 @@ -import { ClientTypeEnum, MatchingModeEnum, SubModeEnum } from '@goauthentik/api'; +import { ClientTypeEnum, SubModeEnum } from '@goauthentik/api'; import { z } from 'zod'; const authentikClientSpecSchema = z.object({ - server: z.string(), + secretRef: z.string(), subMode: z.enum(SubModeEnum).optional(), clientType: z.enum(ClientTypeEnum).optional(), redirectUris: z.array( z.object({ url: z.string(), - matchingMode: z.enum(MatchingModeEnum).optional(), + matchingMode: z.enum(['strict', 'regex']), }), ), }); +const authentikClientServerSecretSchema = z.object({ + internal_url: z.string(), + external_url: z.string(), + token: z.string(), +}); + const authentikClientSecretSchema = z.object({ clientId: z.string(), clientSecret: z.string().optional(), @@ -25,4 +31,4 @@ const authentikClientSecretSchema = z.object({ jwks: z.string(), }); -export { authentikClientSpecSchema, authentikClientSecretSchema }; +export { authentikClientSpecSchema, authentikClientSecretSchema, authentikClientServerSecretSchema }; diff --git a/src/custom-resouces/authentik-server/authentik-server.create-manifests.ts b/src/custom-resouces/authentik-server/authentik-server.create-manifests.ts deleted file mode 100644 index ede68f0..0000000 --- a/src/custom-resouces/authentik-server/authentik-server.create-manifests.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { CustomResourceObject } from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { API_VERSION, CONTROLLED_LABEL } from '../../utils/consts.ts'; -import type { domainServiceSpecSchema } from '../domain-service/domain-service.schemas.ts'; - -type CreateContainerManifestOptions = { - name: string; - namespace: string; - command: string; - owner: ExpectedAny; - secret: string; - bootstrap: { - email: string; - password: string; - token: string; - }; - posgtres: { - host: string; - port: string; - name: string; - user: string; - password: string; - }; - redis: { - host: string; - port: string; - }; -}; -const createManifest = (options: CreateContainerManifestOptions) => ({ - apiVersion: 'apps/v1', - kind: 'Deployment', - metadata: { - name: options.name, - namespace: options.namespace, - labels: { - 'app.kubernetes.io/name': options.name, - ...CONTROLLED_LABEL, - }, - ownerReferences: [options.owner], - }, - spec: { - replicas: 1, - selector: { - matchLabels: { - 'app.kubernetes.io/name': options.name, - }, - }, - template: { - metadata: { - labels: { - 'app.kubernetes.io/name': options.name, - }, - }, - spec: { - containers: [ - { - name: options.name, - image: 'ghcr.io/goauthentik/server:2025.6.4', - args: [options.command], - env: [ - { name: 'AUTHENTIK_SECRET_KEY', value: options.secret }, - { name: 'AUTHENTIK_POSTGRESQL__HOST', value: options.posgtres.host }, - { - name: 'AUTHENTIK_POSTGRESQL__PORT', - value: '5432', - }, - { - name: 'AUTHENTIK_POSTGRESQL__NAME', - value: options.posgtres.name, - }, - { - name: 'AUTHENTIK_POSTGRESQL__USER', - value: options.posgtres.user, - }, - { - name: 'AUTHENTIK_POSTGRESQL__PASSWORD', - value: options.posgtres.password, - }, - { - name: 'AUTHENTIK_REDIS__HOST', - value: options.redis.host, - }, - { - name: 'AUTHENTIK_REDIS__PORT', - value: options.redis.port, - }, - { - name: 'AUTHENTIK_BOOTSTRAP_PASSWORD', - value: options.bootstrap.password, - }, - { - name: 'AUTHENTIK_BOOTSTRAP_TOKEN', - value: options.bootstrap.token, - }, - { - name: 'AUTHENTIK_BOOTSTRAP_EMAIL', - value: options.bootstrap.email, - }, - ], - ports: [ - { - name: 'http', - containerPort: 9000, - protocol: 'TCP', - }, - ], - }, - ], - }, - }, - }, -}); - -type CreateServiceManifestOptions = { - name: string; - namespace: string; - owner: ExpectedAny; - appName: string; -}; -const createServiceManifest = (options: CreateServiceManifestOptions) => ({ - apiVersion: 'v1', - kind: 'Service', - metadata: { - name: options.name, - namespace: options.namespace, - labels: { - ...CONTROLLED_LABEL, - }, - ownerReferences: [options.owner], - }, - spec: { - type: 'ClusterIP', - ports: [ - { - port: 9000, - targetPort: 9000, - protocol: 'TCP', - name: 'http', - }, - ], - selector: { - 'app.kubernetes.io/name': options.appName, - }, - }, -}); - -type CreateDomainServiceOptions = { - name: string; - namespace: string; - owner: ExpectedAny; - subdomain: string; - host: string; - domain: string; -}; -const createDomainService = ( - options: CreateDomainServiceOptions, -): Omit, 'status'> => ({ - apiVersion: API_VERSION, - kind: 'DomainService', - metadata: { - name: options.name, - namespace: options.namespace, - ownerReferences: [options.owner], - }, - spec: { - domain: options.domain, - subdomain: options.subdomain, - destination: { - host: options.host, - port: { - number: 9000, - }, - }, - }, -}); - -export { createManifest, createServiceManifest, createDomainService }; diff --git a/src/custom-resouces/authentik-server/authentik-server.resource.ts b/src/custom-resouces/authentik-server/authentik-server.resource.ts deleted file mode 100644 index c9584ee..0000000 --- a/src/custom-resouces/authentik-server/authentik-server.resource.ts +++ /dev/null @@ -1,382 +0,0 @@ -import type { V1Service, V1Deployment, V1Secret } from '@kubernetes/client-node'; -import { z } from 'zod'; - -import { - CustomResource, - type CustomResourceObject, - type CustomResourceOptions, - type SubresourceResult, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { ResourceReference } from '../../services/resources/resources.ref.ts'; -import { ResourceService, type Resource } from '../../services/resources/resources.ts'; -import type { domainSpecSchema } from '../domain/domain.schemas.ts'; -import type { domainServiceSpecSchema } from '../domain-service/domain-service.schemas.ts'; -import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts'; -import { getWithNamespace } from '../../utils/naming.ts'; -import { API_VERSION } from '../../utils/consts.ts'; -import { SecretService } from '../../services/secrets/secrets.ts'; -import { decodeSecret } from '../../utils/secrets.ts'; -import type { postgresDatabaseSecretSchema } from '../postgres-database/postgres-database.resource.ts'; -import type { redisConnectionSpecSchema } from '../redis-connection/redis-connection.schemas.ts'; -import { isDeepSubset } from '../../utils/objects.ts'; - -import { authentikServerSecretSchema, type authentikServerSpecSchema } from './authentik-server.scemas.ts'; -import { createDomainService, createManifest, createServiceManifest } from './authentik-server.create-manifests.ts'; - -class AuthentikServerResource extends CustomResource { - #domainResource: ResourceReference>; - #databaseSecretResource: ResourceReference; - #redisResource: ResourceReference>; - #redisSecretResource: ResourceReference; - #deploymentServerResource: Resource; - #deploymentWorkerResource: Resource; - #service: Resource; - #domainServiceResource: Resource>; - #secret: EnsuredSecret; - - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - const secretService = this.services.get(SecretService); - - this.#domainResource = new ResourceReference(); - this.#databaseSecretResource = new ResourceReference(); - this.#redisResource = new ResourceReference(); - this.#redisSecretResource = new ResourceReference(); - - this.#deploymentServerResource = resourceService.get({ - apiVersion: 'apps/v1', - kind: 'Deployment', - name: this.#serverName, - namespace: this.namespace, - }); - - this.#deploymentWorkerResource = resourceService.get({ - apiVersion: 'apps/v1', - kind: 'Deployment', - name: this.#workerName, - namespace: this.namespace, - }); - - this.#domainServiceResource = resourceService.get({ - apiVersion: API_VERSION, - kind: 'DomainService', - name: this.name, - namespace: this.namespace, - }); - - this.#service = resourceService.get({ - apiVersion: 'v1', - kind: 'Service', - name: this.name, - namespace: this.namespace, - }); - - this.#secret = secretService.ensure({ - name: `authentik-server-${this.name}`, - namespace: this.namespace, - schema: authentikServerSecretSchema, - generator: () => ({ - secret: crypto.randomUUID(), - token: crypto.randomUUID(), - password: crypto.randomUUID(), - }), - }); - - this.#domainServiceResource = resourceService.get({ - apiVersion: API_VERSION, - kind: 'DomainService', - name: this.name, - namespace: this.namespace, - }); - - this.#updateResources(); - - this.#domainResource.on('changed', this.queueReconcile); - this.#databaseSecretResource.on('changed', this.queueReconcile); - this.#redisResource.on('changed', this.queueReconcile); - this.#redisSecretResource.on('changed', this.queueReconcile); - this.#deploymentServerResource.on('changed', this.queueReconcile); - this.#deploymentWorkerResource.on('changed', this.queueReconcile); - this.#domainServiceResource.on('changed', this.queueReconcile); - this.#service.on('changed', this.queueReconcile); - this.#secret.resouce.on('changed', this.queueReconcile); - } - - get #databaseSecretName() { - const { name } = getWithNamespace(this.spec.database); - return `postgres-database-${name}`; - } - - get #workerName() { - return `${this.name}-worker`; - } - - get #serverName() { - return `${this.name}-server`; - } - - #updateResources = () => { - if (!this.isValidSpec) { - return; - } - const resourceService = this.services.get(ResourceService); - const redisNames = getWithNamespace(this.spec.redis, this.namespace); - const redisResource = resourceService.get>({ - apiVersion: API_VERSION, - kind: 'RedisConnection', - name: redisNames.name, - namespace: redisNames.namespace, - }); - this.#redisResource.current = redisResource; - const redis = this.#redisResource.current; - - if (redis.exists && redis.spec) { - const redisSecretNames = getWithNamespace(redis.spec.secret, redis.namespace); - this.#redisSecretResource.current = resourceService.get({ - apiVersion: 'v1', - kind: 'Secret', - name: redisSecretNames.name, - namespace: redisSecretNames.namespace, - }); - } else { - this.#redisSecretResource.current = undefined; - } - - const domainNames = getWithNamespace(this.spec.domain, this.namespace); - const databaseNames = getWithNamespace(this.spec.database, this.namespace); - - this.#domainResource.current = resourceService.get({ - apiVersion: API_VERSION, - kind: 'Domain', - name: domainNames.name, - namespace: domainNames.namespace, - }); - - this.#databaseSecretResource.current = resourceService.get({ - apiVersion: 'v1', - kind: 'Secret', - name: this.#databaseSecretName, - namespace: databaseNames.namespace, - }); - }; - - #reconcileWorkerDeployment = async (): Promise => { - const domainService = this.#domainResource.current; - if (!domainService?.exists || !domainService.spec) { - return { - ready: false, - failed: true, - reason: 'MissingDomain', - }; - } - const databaseSecret = decodeSecret>( - this.#databaseSecretResource.current?.data, - ); - if (!databaseSecret) { - return { - ready: false, - failed: true, - reason: 'MissingDatabase', - }; - } - const secret = this.#secret.value; - if (!this.#secret.isValid || !secret) { - return { - ready: false, - syncing: true, - reason: 'WaitingForSecret', - }; - } - - const redisSecret = decodeSecret(this.#redisSecretResource.current?.data); - if (!redisSecret || !redisSecret.host) { - return { - ready: false, - failed: true, - reason: 'MissingRedisSecret', - }; - } - - const email = `admin@${domainService.spec.hostname}`; - const manifest = createManifest({ - name: this.#workerName, - namespace: this.namespace, - secret: secret.secret, - command: 'worker', - owner: this.ref, - bootstrap: { - email, - token: secret.token, - password: secret.password, - }, - redis: { - host: redisSecret.host, - port: redisSecret.port ?? '6379', - }, - posgtres: { - host: databaseSecret.host, - port: databaseSecret.port || '5432', - name: databaseSecret.database, - user: databaseSecret.user, - password: databaseSecret.password, - }, - }); - if (!isDeepSubset(this.#deploymentWorkerResource.spec, manifest.spec)) { - await this.#deploymentWorkerResource.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ManifestNeedsPatching', - }; - } - return { - ready: true, - }; - }; - - #reconcileServerDeployment = async (): Promise => { - const domainService = this.#domainResource.current; - if (!domainService?.exists || !domainService.spec) { - return { - ready: false, - failed: true, - reason: 'MissingDomain', - }; - } - const databaseSecret = decodeSecret>( - this.#databaseSecretResource.current?.data, - ); - if (!databaseSecret) { - return { - ready: false, - failed: true, - reason: 'MissingDatabase', - }; - } - const secret = this.#secret.value; - if (!this.#secret.isValid || !secret) { - return { - ready: false, - syncing: true, - reason: 'WaitingForSecret', - }; - } - - const redisSecret = decodeSecret(this.#redisSecretResource.current?.data); - if (!redisSecret || !redisSecret.host) { - return { - ready: false, - failed: true, - reason: 'MissingRedisSecret', - }; - } - - const email = `admin@${domainService.spec.hostname}`; - const manifest = createManifest({ - name: this.#serverName, - namespace: this.namespace, - secret: secret.secret, - command: 'server', - owner: this.ref, - bootstrap: { - email, - token: secret.token, - password: secret.password, - }, - redis: { - host: redisSecret.host, - port: redisSecret.port ?? '6379', - }, - posgtres: { - host: databaseSecret.host, - port: databaseSecret.port || '5432', - name: databaseSecret.database, - user: databaseSecret.user, - password: databaseSecret.password, - }, - }); - if (!isDeepSubset(this.#deploymentServerResource.spec, manifest.spec)) { - await this.#deploymentServerResource.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ManifestNeedsPatching', - }; - } - return { - ready: true, - }; - }; - - #reconcileService = async (): Promise => { - const manifest = createServiceManifest({ - name: this.name, - namespace: this.namespace, - owner: this.ref, - appName: this.#serverName, - }); - - if (!isDeepSubset(manifest.spec, this.#service.manifest)) { - await this.#service.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileDomainService = async (): Promise => { - const manifest = createDomainService({ - name: this.name, - namespace: this.namespace, - owner: this.ref, - domain: this.spec.domain, - host: `${this.name}.${this.namespace}.svc.cluster.local`, - subdomain: this.spec.subdomain, - }); - if (!isDeepSubset(manifest.spec, this.#domainServiceResource.spec)) { - await this.#domainServiceResource.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - public reconcile = async () => { - if (!this.isValidSpec) { - await this.conditions.set('Ready', { - status: 'False', - reason: 'Invalid spec', - }); - } - this.#updateResources(); - - await Promise.allSettled([ - this.reconcileSubresource('Worker', this.#reconcileWorkerDeployment), - this.reconcileSubresource('Server', this.#reconcileServerDeployment), - this.reconcileSubresource('Service', this.#reconcileService), - this.reconcileSubresource('DomainService', this.#reconcileDomainService), - ]); - - const workerReady = this.conditions.get('Worker')?.status === 'True'; - const serverReady = this.conditions.get('Server')?.status === 'True'; - const serviceReady = this.conditions.get('Service')?.status === 'True'; - const domainServiceReady = this.conditions.get('DomainService')?.status === 'True'; - - await this.conditions.set('Ready', { - status: workerReady && serverReady && serviceReady && domainServiceReady ? 'True' : 'False', - }); - }; -} - -export { AuthentikServerResource }; diff --git a/src/custom-resouces/authentik-server/authentik-server.scemas.ts b/src/custom-resouces/authentik-server/authentik-server.scemas.ts deleted file mode 100644 index 3af773e..0000000 --- a/src/custom-resouces/authentik-server/authentik-server.scemas.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from 'zod'; - -const authentikServerSpecSchema = z.object({ - domain: z.string(), - subdomain: z.string(), - database: z.string(), - redis: z.string(), -}); - -const authentikServerSecretSchema = z.object({ - secret: z.string(), - password: z.string(), - token: z.string(), -}); - -export { authentikServerSpecSchema, authentikServerSecretSchema }; diff --git a/src/custom-resouces/authentik-server/authentik-server.ts b/src/custom-resouces/authentik-server/authentik-server.ts deleted file mode 100644 index cef54a1..0000000 --- a/src/custom-resouces/authentik-server/authentik-server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { AuthentikServerResource } from './authentik-server.resource.ts'; -import { authentikServerSpecSchema } from './authentik-server.scemas.ts'; - -const authentikServerDefinition = createCustomResourceDefinition({ - group: GROUP, - version: 'v1', - kind: 'AuthentikServer', - names: { - plural: 'authentikservers', - singular: 'authentikserver', - }, - spec: authentikServerSpecSchema, - create: (options) => new AuthentikServerResource(options), -}); - -export { authentikServerDefinition }; diff --git a/src/custom-resouces/custom-resources.ts b/src/custom-resouces/custom-resources.ts index 064f9ce..4b4a520 100644 --- a/src/custom-resouces/custom-resources.ts +++ b/src/custom-resouces/custom-resources.ts @@ -1,25 +1,7 @@ -import { authentikServerDefinition } from './authentik-server/authentik-server.ts'; import { authentikClientDefinition } from './authentik-client/authentik-client.ts'; -import { domainServiceDefinition } from './domain-service/domain-service.ts'; -import { domainDefinition } from './domain/domain.ts'; -import { postgresConnectionDefinition } from './postgres-connection/postgres-connection.ts'; +import { generateSecretDefinition } from './generate-secret/generate-secret.ts'; import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts'; -import { redisConnectionDefinition } from './redis-connection/redis-connection.ts'; -import { homelabDefinition } from './homelab/homelab.ts'; -import { postgresClusterDefinition } from './postgres-cluster/postgres-cluster.ts'; -import { redisServerDefinition } from './redis-server/redis-server.ts'; -const customResources = [ - homelabDefinition, - domainDefinition, - domainServiceDefinition, - postgresClusterDefinition, - postgresConnectionDefinition, - postgresDatabaseDefinition, - redisServerDefinition, - redisConnectionDefinition, - authentikServerDefinition, - authentikClientDefinition, -]; +const customResources = [postgresDatabaseDefinition, authentikClientDefinition, generateSecretDefinition]; export { customResources }; diff --git a/src/custom-resouces/domain-service/domain-service.create-manifests.ts b/src/custom-resouces/domain-service/domain-service.create-manifests.ts deleted file mode 100644 index 399de07..0000000 --- a/src/custom-resouces/domain-service/domain-service.create-manifests.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { KubernetesObject } from '@kubernetes/client-node'; - -import type { K8SVirtualServiceV1 } from '../../__generated__/resources/K8SVirtualServiceV1.ts'; -import type { K8SDestinationRuleV1 } from '../../__generated__/resources/K8SDestinationRuleV1.ts'; -import { CONTROLLED_LABEL } from '../../utils/consts.ts'; - -type CreateVirtualServiceManifestOptions = { - name: string; - namespace: string; - owner: ExpectedAny; - host: string; - gateway: string; - destination: { - host: string; - port: { - number?: number; - name?: string; - }; - }; -}; -const createVirtualServiceManifest = ( - options: CreateVirtualServiceManifestOptions, -): KubernetesObject & K8SVirtualServiceV1 => ({ - apiVersion: 'networking.istio.io/v1', - kind: 'VirtualService', - metadata: { - name: options.name, - namespace: options.namespace, - ownerReferences: [options.owner], - labels: { - ...CONTROLLED_LABEL, - }, - }, - spec: { - hosts: [options.host], - gateways: [options.gateway], - http: [ - { - match: [ - { - uri: { - prefix: '/', - }, - }, - ], - route: [ - { - destination: { - host: options.destination.host, - port: options.destination.port, - }, - }, - ], - }, - ], - }, -}); - -type CreateDestinationRuleManifestOptions = { - name: string; - namespace: string; - host: string; -}; -const createDestinationRuleManifest = ( - options: CreateDestinationRuleManifestOptions, -): KubernetesObject & K8SDestinationRuleV1 => ({ - apiVersion: 'networking.istio.io/v1', - kind: 'DestinationRule', - metadata: { - name: options.name, - namespace: options.namespace, - labels: { - ...CONTROLLED_LABEL, - }, - }, - spec: { - host: options.host, - trafficPolicy: { - tls: { - mode: 'DISABLE', - }, - }, - }, -}); - -export { createVirtualServiceManifest, createDestinationRuleManifest }; diff --git a/src/custom-resouces/domain-service/domain-service.resource.ts b/src/custom-resouces/domain-service/domain-service.resource.ts deleted file mode 100644 index dd59224..0000000 --- a/src/custom-resouces/domain-service/domain-service.resource.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { KubernetesObject } from '@kubernetes/client-node'; -import deepEqual from 'deep-equal'; - -import type { K8SVirtualServiceV1 } from '../../__generated__/resources/K8SVirtualServiceV1.ts'; -import { - CustomResource, - type CustomResourceObject, - type CustomResourceOptions, - type SubresourceResult, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { ResourceReference, ResourceService, type Resource } from '../../services/resources/resources.ts'; -import type { K8SDestinationRuleV1 } from '../../__generated__/resources/K8SDestinationRuleV1.ts'; -import type { domainSpecSchema } from '../domain/domain.schemas.ts'; -import { getWithNamespace } from '../../utils/naming.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import type { domainServiceSpecSchema } from './domain-service.schemas.ts'; -import { createDestinationRuleManifest, createVirtualServiceManifest } from './domain-service.create-manifests.ts'; - -const VIRTUAL_SERVICE_CONDITION = 'VirtualService'; -const DESTINAION_RULE_CONDITION = 'DestinationRule'; - -class DomainServiceResource extends CustomResource { - #virtualServiceResource: Resource; - #virtualServiceCRDResource: Resource; - #destinationRuleResource: Resource; - #destinationRuleCRDResource: Resource; - #domainResource: ResourceReference>; - - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - this.#virtualServiceResource = resourceService.get({ - apiVersion: 'networking.istio.io/v1', - kind: 'VirtualService', - name: this.name, - namespace: this.namespace, - }); - - this.#virtualServiceCRDResource = resourceService.get({ - apiVersion: 'apiextensions.k8s.io/v1', - kind: 'CustomResourceDefinition', - name: 'virtualservices.networking.istio.io', - }); - - this.#destinationRuleResource = resourceService.get({ - apiVersion: 'networking.istio.io/v1', - kind: 'DestinationRule', - name: this.name, - namespace: this.namespace, - }); - - this.#destinationRuleCRDResource = resourceService.get({ - apiVersion: 'apiextensions.k8s.io/v1', - kind: 'CustomResourceDefinition', - name: 'destinationrules.networking.istio.io', - }); - - const gatewayNames = getWithNamespace(this.spec.domain); - this.#domainResource = new ResourceReference( - resourceService.get({ - apiVersion: `${GROUP}/v1`, - kind: 'Domain', - name: gatewayNames.name, - namespace: gatewayNames.namespace, - }), - ); - - this.#virtualServiceResource.on('changed', this.queueReconcile); - this.#virtualServiceCRDResource.on('changed', this.queueReconcile); - this.#destinationRuleResource.on('changed', this.queueReconcile); - this.#destinationRuleCRDResource.on('changed', this.queueReconcile); - this.#domainResource.on('changed', this.queueReconcile); - } - - #reconcileVirtualService = async (): Promise => { - if (!this.#virtualServiceCRDResource.exists) { - return { - ready: false, - failed: true, - reason: 'MissingCRD', - }; - } - const domain = this.#domainResource.current; - if (!domain?.exists || !domain.spec) { - return { - ready: false, - failed: true, - reason: 'MissingDomain', - }; - } - const manifest = createVirtualServiceManifest({ - name: this.name, - namespace: this.namespace, - gateway: `${domain.namespace}/${domain.name}`, - owner: this.ref, - host: `${this.spec.subdomain}.${domain.spec.hostname}`, - destination: this.spec.destination, - }); - - if (!deepEqual(this.#virtualServiceResource.spec, manifest.spec)) { - await this.#virtualServiceResource.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ManifestNeedsPatching', - }; - } - - return { - ready: true, - }; - }; - - #reconcileDestinationRule = async (): Promise => { - if (!this.#destinationRuleCRDResource.exists) { - return { - ready: false, - failed: true, - reason: 'MissingCRD', - }; - } - const manifest = createDestinationRuleManifest({ - name: this.name, - namespace: this.namespace, - host: this.spec.destination.host, - }); - - if (!deepEqual(this.#destinationRuleResource.spec, manifest.spec)) { - await this.#destinationRuleResource.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ManifestNeedsPatching', - }; - } - - return { - ready: true, - }; - }; - - public reconcile = async () => { - if (!this.exists || this.metadata.deletionTimestamp) { - return; - } - const resourceService = this.services.get(ResourceService); - const gatewayNames = getWithNamespace(this.spec.domain, this.namespace); - - this.#domainResource.current = resourceService.get({ - apiVersion: `${GROUP}/v1`, - kind: 'Domain', - name: gatewayNames.name, - namespace: gatewayNames.namespace, - }); - - await this.reconcileSubresource(VIRTUAL_SERVICE_CONDITION, this.#reconcileVirtualService); - await this.reconcileSubresource(DESTINAION_RULE_CONDITION, this.#reconcileDestinationRule); - - const virtualServiceReady = this.conditions.get(VIRTUAL_SERVICE_CONDITION)?.status === 'True'; - const destinationruleReady = this.conditions.get(DESTINAION_RULE_CONDITION)?.status === 'True'; - - await this.conditions.set('Ready', { - status: virtualServiceReady && destinationruleReady ? 'True' : 'False', - }); - }; -} - -export { DomainServiceResource }; diff --git a/src/custom-resouces/domain-service/domain-service.schemas.ts b/src/custom-resouces/domain-service/domain-service.schemas.ts deleted file mode 100644 index 1ab1445..0000000 --- a/src/custom-resouces/domain-service/domain-service.schemas.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod'; - -const domainServiceSpecSchema = z.object({ - domain: z.string(), - subdomain: z.string(), - destination: z.object({ - host: z.string(), - port: z.object({ - number: z.number().optional(), - name: z.string().optional(), - }), - }), -}); - -export { domainServiceSpecSchema }; diff --git a/src/custom-resouces/domain-service/domain-service.ts b/src/custom-resouces/domain-service/domain-service.ts deleted file mode 100644 index 206433e..0000000 --- a/src/custom-resouces/domain-service/domain-service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { DomainServiceResource } from './domain-service.resource.ts'; -import { domainServiceSpecSchema } from './domain-service.schemas.ts'; - -const domainServiceDefinition = createCustomResourceDefinition({ - group: GROUP, - kind: 'DomainService', - version: 'v1', - spec: domainServiceSpecSchema, - names: { - plural: 'domainservices', - singular: 'domainservice', - }, - create: (options) => new DomainServiceResource(options), -}); - -export { domainServiceDefinition }; diff --git a/src/custom-resouces/domain/domain.create-manifests.ts b/src/custom-resouces/domain/domain.create-manifests.ts deleted file mode 100644 index 407709b..0000000 --- a/src/custom-resouces/domain/domain.create-manifests.ts +++ /dev/null @@ -1,73 +0,0 @@ -type CreateGatewayManifestOptions = { - name: string; - namespace: string; - ref: ExpectedAny; - gateway: string; - domain: string; - secretName: string; -}; -const createGatewayManifest = (options: CreateGatewayManifestOptions) => ({ - apiVersion: 'networking.istio.io/v1alpha3', - kind: 'Gateway', - metadata: { - name: options.name, - namespace: options.namespace, - ownerReferences: [options.ref], - }, - spec: { - selector: { - istio: options.gateway, - }, - servers: [ - { - port: { - number: 80, - name: 'http', - protocol: 'HTTP', - }, - hosts: [`*.${options.domain}`], - tls: { - httpsRedirect: true, - }, - }, - { - port: { - number: 443, - name: 'https', - protocol: 'HTTPS', - }, - hosts: [`*.${options.domain}`], - tls: { - mode: 'SIMPLE' as const, - credentialName: options.secretName, - }, - }, - ], - }, -}); - -type CreateCertificateManifestOptions = { - name: string; - namespace: string; - domain: string; - secretName: string; - issuer: string; -}; -const createCertificateManifest = (options: CreateCertificateManifestOptions) => ({ - apiVersion: 'cert-manager.io/v1', - kind: 'Certificate', - metadata: { - name: options.name, - namespace: 'homelab', // TODO: use namespace of gateway controller - }, - spec: { - secretName: options.secretName, - dnsNames: [`*.${options.domain}`], - issuerRef: { - name: options.issuer, - kind: 'ClusterIssuer', - }, - }, -}); - -export { createGatewayManifest, createCertificateManifest }; diff --git a/src/custom-resouces/domain/domain.resource.ts b/src/custom-resouces/domain/domain.resource.ts deleted file mode 100644 index 8b89c13..0000000 --- a/src/custom-resouces/domain/domain.resource.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { KubernetesObject } from '@kubernetes/client-node'; -import deepEqual from 'deep-equal'; - -import type { K8SGatewayV1 } from '../../__generated__/resources/K8SGatewayV1.ts'; -import { - CustomResource, - type CustomResourceOptions, - type SubresourceResult, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { ResourceReference, ResourceService } from '../../services/resources/resources.ts'; -import type { K8SCertificateV1 } from '../../__generated__/resources/K8SCertificateV1.ts'; -import { IstioService } from '../../services/istio/istio.ts'; - -import type { domainSpecSchema } from './domain.schemas.ts'; -import { createCertificateManifest, createGatewayManifest } from './domain.create-manifests.ts'; - -class DomainResource extends CustomResource { - #gatewayCrdResource = new ResourceReference(); - #gatewayResource = new ResourceReference(); - #certificateCrdResource = new ResourceReference(); - #certificateResource = new ResourceReference(); - - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - const istioService = this.services.get(IstioService); - - this.#gatewayCrdResource.current = resourceService.get({ - apiVersion: 'apiextensions.k8s.io/v1', - kind: 'CustomResourceDefinition', - name: 'gateways.networking.istio.io', - }); - this.#gatewayResource.current = resourceService.get({ - apiVersion: 'networking.istio.io/v1', - kind: 'Gateway', - name: this.name, - namespace: this.namespace, - }); - - this.#certificateCrdResource.current = resourceService.get({ - apiVersion: 'apiextensions.k8s.io/v1', - kind: 'CustomResourceDefinition', - name: 'certificates.cert-manager.io', - }); - - this.#certificateResource.current = resourceService.get({ - apiVersion: 'cert-manager.io/v1', - kind: 'Certificate', - name: `domain-${this.name}`, - namespace: 'homelab', - }); - - this.#gatewayResource.on('changed', this.queueReconcile); - this.#certificateResource.on('changed', this.queueReconcile); - this.#gatewayCrdResource.on('changed', this.queueReconcile); - this.#certificateCrdResource.on('changed', this.queueReconcile); - - istioService.gateway.on('changed', this.queueReconcile); - } - - get #certSecret() { - return `cert-secret-${this.namespace}-${this.name}`; - } - - #reconcileGateway = async (): Promise => { - if (!this.#gatewayCrdResource.current?.exists) { - return { - ready: false, - failed: true, - reason: 'MissingCRD', - message: 'Missing Gateway CRD', - }; - } - const istioService = this.services.get(IstioService); - if (!istioService.gateway.current) { - return { - ready: false, - failed: true, - reason: 'MissingGatewayController', - message: 'No istio gateway controller could be found', - }; - } - const manifest = createGatewayManifest({ - name: this.name, - namespace: this.name, - domain: this.spec.hostname, - ref: this.ref, - gateway: istioService.gateway.current.metadata?.labels?.istio || 'gateway-controller', - secretName: this.#certSecret, - }); - if (!deepEqual(this.#gatewayResource.current?.spec, manifest.spec)) { - await this.#gatewayResource.current?.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ChangingGateway', - message: 'Gateway need changes', - }; - } - return { - ready: true, - }; - }; - - #reconcileCertificate = async (): Promise => { - if (!this.#certificateCrdResource.current?.exists) { - return { - ready: false, - syncing: false, - failed: true, - reason: 'MissingCRD', - message: 'Missing Certificate CRD', - }; - } - const current = this.#certificateResource.current; - if (!current || !current.namespace) { - throw new Error('Missing certificate resource'); - } - const istioService = this.services.get(IstioService); - if (!istioService.gateway.current) { - return { - ready: false, - syncing: false, - failed: true, - reason: 'MissingGatewayController', - message: 'No istio gateway controller could be found', - }; - } - const manifest = createCertificateManifest({ - name: current.name, - namespace: istioService.gateway.current.namespace || 'default', - domain: this.spec.hostname, - secretName: this.#certSecret, - issuer: this.spec.issuer, - }); - if (!this.#certificateResource.current?.exists) { - await current.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'Creating', - message: 'Creating certificate resource', - }; - } - if (!deepEqual(current.spec, manifest.spec)) { - await this.conditions.set('CertificateReady', { - status: 'False', - reason: 'Changing', - message: 'Certificate need changes', - }); - await current.patch(manifest); - } - return { - ready: true, - }; - }; - - public reconcile = async () => { - if (!this.exists || this.metadata.deletionTimestamp) { - return; - } - await this.reconcileSubresource('Gateway', this.#reconcileGateway); - await this.reconcileSubresource('Certificate', this.#reconcileCertificate); - - const gatewayReady = this.conditions.get('Gateway')?.status === 'True'; - const certificateReady = this.conditions.get('Certificate')?.status === 'True'; - - await this.conditions.set('Ready', { - status: gatewayReady && certificateReady ? 'True' : 'False', - }); - }; -} - -export { DomainResource }; diff --git a/src/custom-resouces/domain/domain.schemas.ts b/src/custom-resouces/domain/domain.schemas.ts deleted file mode 100644 index 4c9d116..0000000 --- a/src/custom-resouces/domain/domain.schemas.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from 'zod'; - -const domainSpecSchema = z.object({ - hostname: z.string(), - issuer: z.string(), -}); - -export { domainSpecSchema }; diff --git a/src/custom-resouces/domain/domain.ts b/src/custom-resouces/domain/domain.ts deleted file mode 100644 index 89765eb..0000000 --- a/src/custom-resouces/domain/domain.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { DomainResource } from './domain.resource.ts'; -import { domainSpecSchema } from './domain.schemas.ts'; - -const domainDefinition = createCustomResourceDefinition({ - version: 'v1', - kind: 'Domain', - group: GROUP, - names: { - plural: 'domains', - singular: 'domain', - }, - spec: domainSpecSchema, - create: (options) => new DomainResource(options), -}); - -export { domainDefinition }; diff --git a/src/custom-resouces/generate-secret/generate-secret.resource.ts b/src/custom-resouces/generate-secret/generate-secret.resource.ts new file mode 100644 index 0000000..075bb71 --- /dev/null +++ b/src/custom-resouces/generate-secret/generate-secret.resource.ts @@ -0,0 +1,61 @@ +import type { V1Secret } from '@kubernetes/client-node'; + +import { + CustomResource, + type CustomResourceOptions, +} from '../../services/custom-resources/custom-resources.custom-resource.ts'; +import { Resource, ResourceService } from '../../services/resources/resources.ts'; +import { decodeSecret, encodeSecret } from '../../utils/secrets.ts'; +import { isDeepSubset } from '../../utils/objects.ts'; + +import { generateSecrets } from './generate-secret.utils.ts'; +import { generateSecretSpecSchema } from './generate-secret.schemas.ts'; + +class GenerateSecretResource extends CustomResource { + #secretResource: Resource; + + constructor(options: CustomResourceOptions) { + super(options); + const resourceService = this.services.get(ResourceService); + + this.#secretResource = resourceService.get({ + apiVersion: 'v1', + kind: 'Secret', + name: this.name, + namespace: this.namespace, + }); + + this.#secretResource.on('changed', this.queueReconcile); + } + + public reconcile = async () => { + if (!this.exists || this.metadata?.deletionTimestamp) { + return; + } + + const secrets = generateSecrets(this.spec); + const current = decodeSecret(this.#secretResource.data) || {}; + + const expected = { + ...current, + ...secrets, + }; + + if (!isDeepSubset(current, expected)) { + this.#secretResource.patch({ + data: encodeSecret(expected), + }); + this.conditions.set('SecretUpdated', { + status: 'False', + reason: 'SecretUpdated', + }); + } + + this.conditions.set('Ready', { + status: 'True', + reason: 'Ready', + }); + }; +} + +export { GenerateSecretResource }; diff --git a/src/custom-resouces/generate-secret/generate-secret.schemas.ts b/src/custom-resouces/generate-secret/generate-secret.schemas.ts new file mode 100644 index 0000000..826f5bf --- /dev/null +++ b/src/custom-resouces/generate-secret/generate-secret.schemas.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +const generateSecretFieldSchema = z.object({ + name: z.string(), + value: z.string().optional(), + encoding: z.enum(['base64', 'base64url', 'hex', 'utf8', 'numeric']).optional(), + length: z.number().optional(), +}); + +const generateSecretSpecSchema = z.object({ + fields: z.array(generateSecretFieldSchema), +}); + +type GenerateSecretField = z.infer; +type GenerateSecretSpec = z.infer; + +export { generateSecretSpecSchema, type GenerateSecretField, type GenerateSecretSpec }; diff --git a/src/custom-resouces/generate-secret/generate-secret.ts b/src/custom-resouces/generate-secret/generate-secret.ts new file mode 100644 index 0000000..6f52f26 --- /dev/null +++ b/src/custom-resouces/generate-secret/generate-secret.ts @@ -0,0 +1,19 @@ +import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; +import { GROUP } from '../../utils/consts.ts'; + +import { GenerateSecretResource } from './generate-secret.resource.ts'; +import { generateSecretSpecSchema } from './generate-secret.schemas.ts'; + +const generateSecretDefinition = createCustomResourceDefinition({ + group: GROUP, + version: 'v1', + kind: 'GenerateSecret', + names: { + plural: 'generate-secrets', + singular: 'generate-secret', + }, + spec: generateSecretSpecSchema, + create: (options) => new GenerateSecretResource(options), +}); + +export { generateSecretDefinition }; diff --git a/src/custom-resouces/generate-secret/generate-secret.utils.ts b/src/custom-resouces/generate-secret/generate-secret.utils.ts new file mode 100644 index 0000000..7f2c057 --- /dev/null +++ b/src/custom-resouces/generate-secret/generate-secret.utils.ts @@ -0,0 +1,69 @@ +import * as crypto from 'crypto'; + +import type { GenerateSecretField, GenerateSecretSpec } from './generate-secret.schemas.ts'; + +const generateRandomString = (length: number, encoding: GenerateSecretField['encoding']): string => { + let byteLength = 0; + switch (encoding) { + case 'base64': + case 'base64url': + // Base64 uses 4 characters for every 3 bytes, so we'll generate slightly more bytes + // than the final length to ensure we can get a string of at least the required length. + byteLength = Math.ceil((length * 3) / 4); + break; + case 'hex': + byteLength = Math.ceil(length / 2); + break; + case 'numeric': + case 'utf8': + byteLength = length; + break; + } + + const randomBytes = crypto.randomBytes(byteLength); + + let resultString = ''; + + switch (encoding) { + case 'base64': + resultString = randomBytes.toString('base64'); + break; + case 'base64url': + resultString = randomBytes.toString('base64url'); + break; + case 'hex': + resultString = randomBytes.toString('hex'); + break; + case 'numeric': + resultString = Array.from(randomBytes) + .map((b) => (b % 10).toString()) // Get a single digit from each byte + .join(''); + break; + case 'utf8': + resultString = randomBytes.toString('utf8'); + break; + } + + return resultString.slice(0, length); +}; + +const generateSecrets = (spec: GenerateSecretSpec): Record => { + const secrets: Record = {}; + + for (const field of spec.fields) { + if (field.value !== undefined) { + // If a value is provided, use it directly. + secrets[field.name] = field.value; + } else { + // Generate a new secret based on the specification. + // Use default values if encoding or length are not provided. + const encoding = field.encoding || 'base64url'; + const length = field.length || 32; + secrets[field.name] = generateRandomString(length, encoding); + } + } + + return secrets; +}; + +export { generateRandomString, generateSecrets }; diff --git a/src/custom-resouces/homelab/homelab.manifests.ts b/src/custom-resouces/homelab/homelab.manifests.ts deleted file mode 100644 index b6e73a3..0000000 --- a/src/custom-resouces/homelab/homelab.manifests.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { KubernetesObject } from '@kubernetes/client-node'; - -import type { K8SHelmRepositoryV1 } from '../../__generated__/resources/K8SHelmRepositoryV1.ts'; -import type { K8SHelmReleaseV2 } from '../../__generated__/resources/K8SHelmReleaseV2.ts'; - -type IstioRepoManifestOptions = { - owner: ExpectedAny; -}; -const istioRepoManifest = (options: IstioRepoManifestOptions): KubernetesObject & K8SHelmRepositoryV1 => { - return { - apiVersion: 'source.toolkit.fluxcd.io/v1beta1', - kind: 'HelmRepository', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - interval: '1h', - url: 'https://istio-release.storage.googleapis.com/charts', - }, - }; -}; - -type CertManagerRepoManifestOptions = { - owner: ExpectedAny; -}; -const certManagerRepoManifest = (options: CertManagerRepoManifestOptions): KubernetesObject & K8SHelmRepositoryV1 => { - return { - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - interval: '1h', - url: 'https://charts.jetstack.io', - }, - }; -}; - -type RanchRepoManifestOptions = { - owner: ExpectedAny; -}; -const ranchRepoManifest = (options: RanchRepoManifestOptions): KubernetesObject & K8SHelmRepositoryV1 => { - return { - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - interval: '1h', - url: 'https://charts.containeroo.ch', - }, - }; -}; - -type IstioBaseManifestOptions = { - owner: ExpectedAny; -}; -const istioBaseManifest = (options: IstioBaseManifestOptions): KubernetesObject & K8SHelmReleaseV2 => { - return { - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - interval: '1h', - targetNamespace: 'istio-system', - install: { - createNamespace: true, - }, - values: { - defaultRevision: 'default', - }, - chart: { - spec: { - chart: 'base', - sourceRef: { - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'homelab-istio', - }, - reconcileStrategy: 'ChartVersion', - version: '1.24.3', - }, - }, - }, - }; -}; - -type IstiodManifestOptions = { - owner: ExpectedAny; - namespace: string; -}; -const istiodManifest = (options: IstiodManifestOptions): KubernetesObject & K8SHelmReleaseV2 => { - return { - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - targetNamespace: 'istio-system', - interval: '1h', - install: { - createNamespace: true, - }, - dependsOn: [ - { - name: 'istio', - namespace: options.namespace, - }, - ], - chart: { - spec: { - chart: 'istiod', - sourceRef: { - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'homelab-istio', - }, - reconcileStrategy: 'ChartVersion', - version: '1.24.3', - }, - }, - }, - }; -}; - -type IstioGatewayControllerManifestOptions = { - owner: ExpectedAny; - namespace: string; -}; -const istioGatewayControllerManifest = ( - options: IstioGatewayControllerManifestOptions, -): KubernetesObject & K8SHelmReleaseV2 => { - return { - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - interval: '1h', - install: { - createNamespace: true, - }, - dependsOn: [ - { - name: 'istio', - namespace: options.namespace, - }, - { - name: 'istiod', - namespace: options.namespace, - }, - ], - values: { - service: { - ports: [ - { - name: 'status-port', - port: 15021, - }, - { - name: 'tls-istiod', - port: 15012, - }, - { - name: 'tls', - port: 15443, - nodePort: 31371, - }, - { - name: 'http2', - port: 80, - nodePort: 31381, - targetPort: 8280, - }, - { - name: 'https', - port: 443, - nodePort: 31391, - targetPort: 8243, - }, - ], - }, - }, - chart: { - spec: { - chart: 'gateway', - sourceRef: { - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'homelab-istio', - }, - reconcileStrategy: 'ChartVersion', - version: '1.24.3', - }, - }, - }, - }; -}; - -type CertManagerManifestOptions = { - owner: ExpectedAny; -}; -const certManagerManifest = (options: CertManagerManifestOptions): KubernetesObject & K8SHelmReleaseV2 => { - return { - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - targetNamespace: 'cert-manager', - interval: '1h', - install: { - createNamespace: true, - }, - values: { - installCRDs: true, - }, - chart: { - spec: { - chart: 'cert-manager', - sourceRef: { - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'cert-manager', - }, - version: 'v1.18.2', - }, - }, - }, - }; -}; - -type LocalStorageManifestOptions = { - owner: ExpectedAny; - storagePath: string; -}; -const localStorageManifest = (options: LocalStorageManifestOptions): KubernetesObject & K8SHelmReleaseV2 => { - return { - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - targetNamespace: 'local-path-storage', - interval: '1h', - install: { - createNamespace: true, - }, - values: { - storageClass: { - name: 'local-path', - provisionerName: 'rancher.io/local-path', - defaultClass: true, - }, - nodePathMap: [ - { - node: 'DEFAULT_PATH_FOR_NON_LISTED_NODES', - paths: [options.storagePath], - }, - ], - helper: { - reclaimPolicy: 'Retain', - }, - }, - chart: { - spec: { - chart: 'local-path-provisioner', - sourceRef: { - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'rancher', - }, - version: '0.0.32', - }, - }, - }, - }; -}; - -export { - istioRepoManifest, - istioBaseManifest, - istiodManifest, - istioGatewayControllerManifest, - certManagerRepoManifest, - certManagerManifest, - ranchRepoManifest, - localStorageManifest, -}; diff --git a/src/custom-resouces/homelab/homelab.resource.ts b/src/custom-resouces/homelab/homelab.resource.ts deleted file mode 100644 index 7082831..0000000 --- a/src/custom-resouces/homelab/homelab.resource.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { type KubernetesObject } from '@kubernetes/client-node'; - -import { - CustomResource, - type CustomResourceOptions, - type SubresourceResult, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { Resource, ResourceService } from '../../services/resources/resources.ts'; -import type { K8SHelmRepositoryV1 } from '../../__generated__/resources/K8SHelmRepositoryV1.ts'; -import type { K8SHelmReleaseV2 } from '../../__generated__/resources/K8SHelmReleaseV2.ts'; -import { isDeepSubset } from '../../utils/objects.ts'; - -import type { homelabSpecSchema } from './homelab.schemas.ts'; -import { - certManagerRepoManifest, - istioBaseManifest, - istiodManifest, - istioGatewayControllerManifest, - istioRepoManifest, - certManagerManifest, - ranchRepoManifest, - localStorageManifest, -} from './homelab.manifests.ts'; - -class HomelabResource extends CustomResource { - #resources: { - istioRepo: Resource; - istioBase: Resource; - istiod: Resource; - istioGatewayController: Resource; - certManagerRepo: Resource; - certManager: Resource; - ranchRepo: Resource; - localStorage: Resource; - }; - - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - this.#resources = { - istioRepo: resourceService.get({ - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'homelab-istio', - namespace: this.namespace, - }), - istioBase: resourceService.get({ - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - name: 'istio', - namespace: this.namespace, - }), - istiod: resourceService.get({ - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - name: 'istiod', - namespace: this.namespace, - }), - istioGatewayController: resourceService.get({ - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - name: 'istio-gateway-controller', - namespace: this.namespace, - }), - certManagerRepo: resourceService.get({ - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'cert-manager', - namespace: this.namespace, - }), - certManager: resourceService.get({ - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - name: 'cert-manager', - namespace: this.namespace, - }), - ranchRepo: resourceService.get({ - apiVersion: 'source.toolkit.fluxcd.io/v1', - kind: 'HelmRepository', - name: 'rancher', - namespace: this.namespace, - }), - localStorage: resourceService.get({ - apiVersion: 'helm.toolkit.fluxcd.io/v2', - kind: 'HelmRelease', - name: 'local-storage', - namespace: this.namespace, - }), - }; - - for (const resource of Object.values(this.#resources)) { - resource.on('changed', this.queueReconcile); - } - } - - #reconcileIstioRepo = async (): Promise => { - const istioRepo = this.#resources.istioRepo; - const manifest = istioRepoManifest({ - owner: this.ref, - }); - if (!isDeepSubset(istioRepo.spec, manifest.spec)) { - await istioRepo.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileCertManagerRepo = async (): Promise => { - const certManagerRepo = this.#resources.certManagerRepo; - const manifest = certManagerRepoManifest({ - owner: this.ref, - }); - if (!isDeepSubset(certManagerRepo.spec, manifest.spec)) { - await certManagerRepo.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileRanchRepo = async (): Promise => { - const ranchRepo = this.#resources.ranchRepo; - const manifest = ranchRepoManifest({ - owner: this.ref, - }); - if (!isDeepSubset(ranchRepo.spec, manifest.spec)) { - await ranchRepo.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileIstioBase = async (): Promise => { - const istioBase = this.#resources.istioBase; - const manifest = istioBaseManifest({ - owner: this.ref, - }); - if (!isDeepSubset(istioBase.spec, manifest.spec)) { - await istioBase.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileIstiod = async (): Promise => { - const istiod = this.#resources.istiod; - const manifest = istiodManifest({ - owner: this.ref, - namespace: this.namespace, - }); - if (!isDeepSubset(istiod.spec, manifest.spec)) { - await istiod.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileIstioGatewayController = async (): Promise => { - const istioGatewayController = this.#resources.istioGatewayController; - const manifest = istioGatewayControllerManifest({ - owner: this.ref, - namespace: this.namespace, - }); - if (!isDeepSubset(istioGatewayController.spec, manifest.spec)) { - await istioGatewayController.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileCertManager = async (): Promise => { - const certManager = this.#resources.certManager; - const manifest = certManagerManifest({ - owner: this.ref, - }); - if (!isDeepSubset(certManager.spec, manifest.spec)) { - await certManager.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileLocalStorage = async (): Promise => { - const storage = this.spec.storage; - if (!storage || !storage.enabled) { - return { - ready: true, - }; - } - const localStorage = this.#resources.localStorage; - const manifest = localStorageManifest({ - owner: this.ref, - storagePath: storage.path, - }); - if (!isDeepSubset(localStorage.spec, manifest.spec)) { - await localStorage.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - public reconcile = async () => { - await Promise.allSettled([ - this.reconcileSubresource('IstioRepo', this.#reconcileIstioRepo), - this.reconcileSubresource('CertManagerRepo', this.#reconcileCertManagerRepo), - this.reconcileSubresource('IstioBase', this.#reconcileIstioBase), - this.reconcileSubresource('Istiod', this.#reconcileIstiod), - this.reconcileSubresource('IstioGatewayController', this.#reconcileIstioGatewayController), - this.reconcileSubresource('CertManager', this.#reconcileCertManager), - this.reconcileSubresource('RanchRepo', this.#reconcileRanchRepo), - this.reconcileSubresource('LocalStorage', this.#reconcileLocalStorage), - ]); - }; -} - -export { HomelabResource }; diff --git a/src/custom-resouces/homelab/homelab.schemas.ts b/src/custom-resouces/homelab/homelab.schemas.ts deleted file mode 100644 index 1001090..0000000 --- a/src/custom-resouces/homelab/homelab.schemas.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from 'zod'; - -const homelabSpecSchema = z.object({ - storage: z - .object({ - enabled: z.boolean(), - path: z.string(), - }) - .optional(), -}); - -const homelabSecretSchema = z.object({ - postgresPassword: z.string(), - redisPassword: z.string(), -}); - -export { homelabSpecSchema, homelabSecretSchema }; diff --git a/src/custom-resouces/homelab/homelab.ts b/src/custom-resouces/homelab/homelab.ts deleted file mode 100644 index 79e6ec3..0000000 --- a/src/custom-resouces/homelab/homelab.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { HomelabResource } from './homelab.resource.ts'; -import { homelabSpecSchema } from './homelab.schemas.ts'; - -const homelabDefinition = createCustomResourceDefinition({ - group: GROUP, - version: 'v1', - kind: 'Homelab', - names: { - plural: 'homelabs', - singular: 'homelab', - }, - spec: homelabSpecSchema, - create: (options) => new HomelabResource(options), -}); - -export { homelabDefinition }; diff --git a/src/custom-resouces/postgres-cluster/postgres-cluster.manifests.ts b/src/custom-resouces/postgres-cluster/postgres-cluster.manifests.ts deleted file mode 100644 index b304d98..0000000 --- a/src/custom-resouces/postgres-cluster/postgres-cluster.manifests.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { V1Deployment, V1PersistentVolumeClaim, V1Service } from '@kubernetes/client-node'; - -import type { CustomResourceObject } from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import type { postgresConnectionSpecSchema } from '../postgres-connection/posgtres-connection.schemas.ts'; -import { API_VERSION } from '../../utils/consts.ts'; - -type PvcOptions = { - name: string; - owner: ExpectedAny; -}; -const pvcManifest = (options: PvcOptions): V1PersistentVolumeClaim => { - return { - apiVersion: 'v1', - kind: 'PersistentVolumeClaim', - metadata: { - ownerReferences: [options.owner], - name: options.name, - labels: { - app: options.name, - }, - annotations: { - 'volume.kubernetes.io/storage-class': 'local-path', - }, - }, - spec: { - accessModes: ['ReadWriteOnce'], - resources: { - requests: { - storage: '10Gi', - }, - }, - }, - }; -}; - -type DeploymentManifetOptions = { - name: string; - owner: ExpectedAny; - user: string; - password: string; -}; -const deploymentManifest = (options: DeploymentManifetOptions): V1Deployment => { - return { - apiVersion: 'apps/v1', - kind: 'Deployment', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - replicas: 1, - selector: { - matchLabels: { - app: options.name, - }, - }, - template: { - metadata: { - labels: { - app: options.name, - }, - }, - spec: { - volumes: [{ name: options.name, persistentVolumeClaim: { claimName: options.name } }], - containers: [ - { - name: options.name, - image: 'postgres:17', - ports: [{ containerPort: 5432 }], - volumeMounts: [{ mountPath: '/var/lib/postgresql/data', name: options.name }], - env: [ - { name: 'POSTGRES_USER', value: options.user }, - { name: 'POSTGRES_PASSWORD', value: options.password }, - ], - }, - ], - }, - }, - }, - }; -}; - -type ServiceManifestOptions = { - name: string; - owner: ExpectedAny; -}; -const serviceManifest = (options: ServiceManifestOptions): V1Service => { - return { - apiVersion: 'v1', - kind: 'Service', - metadata: { - ownerReferences: [options.owner], - name: options.name, - labels: { - app: options.name, - }, - }, - spec: { - type: 'ClusterIP', - ports: [{ port: 5432, targetPort: 5432 }], - selector: { - app: options.name, - }, - }, - }; -}; - -type ConnectionManifestOptions = { - name: string; - owner: ExpectedAny; -}; -const connectionManifest = ( - options: ConnectionManifestOptions, -): CustomResourceObject => ({ - apiVersion: API_VERSION, - kind: 'PostgresConnection', - metadata: { - ownerReferences: [options.owner], - }, - spec: { - secret: `${options.name}-secret`, - }, -}); - -export { pvcManifest, deploymentManifest, serviceManifest, connectionManifest }; diff --git a/src/custom-resouces/postgres-cluster/postgres-cluster.resource.ts b/src/custom-resouces/postgres-cluster/postgres-cluster.resource.ts deleted file mode 100644 index 36c7f24..0000000 --- a/src/custom-resouces/postgres-cluster/postgres-cluster.resource.ts +++ /dev/null @@ -1,170 +0,0 @@ -import type { V1Deployment, V1PersistentVolumeClaim, V1Service } from '@kubernetes/client-node'; - -import { - CustomResource, - type CustomResourceObject, - type CustomResourceOptions, - type SubresourceResult, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { ResourceService, type Resource } from '../../services/resources/resources.ts'; -import { - postgresConnectionSecretDataSchema, - type postgresConnectionSpecSchema, -} from '../postgres-connection/posgtres-connection.schemas.ts'; -import { API_VERSION } from '../../utils/consts.ts'; -import { isDeepSubset } from '../../utils/objects.ts'; -import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts'; -import { SecretService } from '../../services/secrets/secrets.ts'; - -import type { postgresClusterSpecSchema } from './postgres-cluster.schemas.ts'; -import { connectionManifest, deploymentManifest, pvcManifest, serviceManifest } from './postgres-cluster.manifests.ts'; - -class PostgresClusterResource extends CustomResource { - #resources: { - pvc: Resource; - deployment: Resource; - service: Resource; - connection: Resource>; - secret: EnsuredSecret; - }; - - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - const secretService = this.services.get(SecretService); - this.#resources = { - pvc: resourceService.get({ - apiVersion: 'v1', - kind: 'PersistentVolumeClaim', - name: this.name, - namespace: this.namespace, - }), - deployment: resourceService.get({ - apiVersion: 'apps/v1', - kind: 'Deployment', - name: this.name, - namespace: this.namespace, - }), - service: resourceService.get({ - apiVersion: 'v1', - kind: 'Service', - name: this.name, - namespace: this.namespace, - }), - connection: resourceService.get({ - apiVersion: API_VERSION, - kind: 'PostgresConnection', - name: this.name, - namespace: this.namespace, - }), - secret: secretService.ensure({ - name: `${this.name}-secret`, - namespace: this.namespace, - schema: postgresConnectionSecretDataSchema, - generator: () => ({ - host: `${this.name}.${this.namespace}.svc.cluster.local`, - port: '5432', - user: 'postgres', - password: Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString('hex'), - }), - }), - }; - } - - #reconcilePvc = async (): Promise => { - const pvc = this.#resources.pvc; - const manifest = pvcManifest({ - name: this.name, - owner: this.ref, - }); - if (!isDeepSubset(pvc.spec, manifest.spec)) { - await pvc.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileDeployment = async (): Promise => { - const secret = this.#resources.secret; - if (!secret.isValid || !secret.value) { - return { - ready: false, - syncing: true, - reason: 'SecretNotReady', - }; - } - const deployment = this.#resources.deployment; - const manifest = deploymentManifest({ - name: this.name, - owner: this.ref, - user: secret.value.user, - password: secret.value.password, - }); - if (!isDeepSubset(deployment.spec, manifest.spec)) { - await deployment.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileService = async (): Promise => { - const service = this.#resources.service; - const manifest = serviceManifest({ - name: this.name, - owner: this.ref, - }); - if (!isDeepSubset(service.spec, manifest.spec)) { - await service.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - #reconcileConnection = async (): Promise => { - const connection = this.#resources.connection; - const manifest = connectionManifest({ - name: this.name, - owner: this.ref, - }); - if (!isDeepSubset(connection.spec, manifest.spec)) { - await connection.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'UpdatingManifest', - }; - } - return { - ready: true, - }; - }; - - public reconcile = async () => { - await Promise.allSettled([ - this.reconcileSubresource('PVC', this.#reconcilePvc), - this.reconcileSubresource('Deployment', this.#reconcileDeployment), - this.reconcileSubresource('Service', this.#reconcileService), - this.reconcileSubresource('Connection', this.#reconcileConnection), - ]); - }; -} - -export { PostgresClusterResource }; diff --git a/src/custom-resouces/postgres-cluster/postgres-cluster.schemas.ts b/src/custom-resouces/postgres-cluster/postgres-cluster.schemas.ts deleted file mode 100644 index 025ff43..0000000 --- a/src/custom-resouces/postgres-cluster/postgres-cluster.schemas.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from 'zod'; - -const postgresClusterSpecSchema = z.object({}); - -export { postgresClusterSpecSchema }; diff --git a/src/custom-resouces/postgres-cluster/postgres-cluster.ts b/src/custom-resouces/postgres-cluster/postgres-cluster.ts deleted file mode 100644 index 9703ea5..0000000 --- a/src/custom-resouces/postgres-cluster/postgres-cluster.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { postgresClusterSpecSchema } from './postgres-cluster.schemas.ts'; -import { PostgresClusterResource } from './postgres-cluster.resource.ts'; - -const postgresClusterDefinition = createCustomResourceDefinition({ - group: GROUP, - version: 'v1', - kind: 'PostgresCluster', - names: { - plural: 'postgresclusters', - singular: 'postgrescluster', - }, - spec: postgresClusterSpecSchema, - create: (options) => new PostgresClusterResource(options), -}); - -export { postgresClusterDefinition }; diff --git a/src/custom-resouces/postgres-connection/posgtres-connection.schemas.ts b/src/custom-resouces/postgres-connection/posgtres-connection.schemas.ts deleted file mode 100644 index d9756b5..0000000 --- a/src/custom-resouces/postgres-connection/posgtres-connection.schemas.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from 'zod'; - -const postgresConnectionSpecSchema = z.object({ - secret: z.string(), -}); - -const postgresConnectionSecretDataSchema = z.object({ - host: z.string(), - port: z.string().optional(), - user: z.string(), - password: z.string(), -}); - -export { postgresConnectionSpecSchema, postgresConnectionSecretDataSchema }; diff --git a/src/custom-resouces/postgres-connection/postgres-connection.resource.ts b/src/custom-resouces/postgres-connection/postgres-connection.resource.ts deleted file mode 100644 index 265faed..0000000 --- a/src/custom-resouces/postgres-connection/postgres-connection.resource.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { V1Secret } from '@kubernetes/client-node'; -import type { z } from 'zod'; - -import { - CustomResource, - type CustomResourceOptions, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { ResourceReference } from '../../services/resources/resources.ref.ts'; -import { ResourceService } from '../../services/resources/resources.ts'; -import { getWithNamespace } from '../../utils/naming.ts'; -import { PostgresService } from '../../services/postgres/postgres.service.ts'; -import { decodeSecret } from '../../utils/secrets.ts'; - -import type { - postgresConnectionSecretDataSchema, - postgresConnectionSpecSchema, -} from './posgtres-connection.schemas.ts'; - -class PostgresConnectionResource extends CustomResource { - #secret: ResourceReference; - - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - const secretNames = getWithNamespace(this.spec.secret, this.namespace); - this.#secret = new ResourceReference( - resourceService.get({ - apiVersion: 'v1', - kind: 'Secret', - name: secretNames.name, - namespace: secretNames.namespace, - }), - ); - this.#secret.on('changed', this.queueReconcile); - } - - public reconcile = async () => { - const resourceService = this.services.get(ResourceService); - const secretNames = getWithNamespace(this.spec.secret, this.namespace); - this.#secret.current = resourceService.get({ - apiVersion: 'v1', - kind: 'Secret', - name: secretNames.name, - namespace: secretNames.namespace, - }); - - const current = this.#secret.current; - if (!current?.exists || !current.data) { - return this.conditions.set('Ready', { - status: 'False', - reason: 'MissingSecret', - }); - } - const { host, user, password, port } = decodeSecret>( - current.data, - )!; - if (!host) { - return this.conditions.set('Ready', { - status: 'False', - reason: 'MissingHost', - }); - } - if (!user) { - return this.conditions.set('Ready', { - status: 'False', - reason: 'MissingUser', - }); - } - if (!password) { - return this.conditions.set('Ready', { - status: 'False', - reason: 'MissingPassword', - }); - } - const postgresService = this.services.get(PostgresService); - const database = postgresService.get({ - host, - user, - port: port ? Number(port) : 5432, - password, - }); - if (!(await database.ping())) { - return this.conditions.set('Ready', { - status: 'False', - reason: 'CanNotConnectToDatabase', - }); - } - await this.conditions.set('Ready', { - status: 'True', - }); - }; -} - -export { PostgresConnectionResource }; diff --git a/src/custom-resouces/postgres-connection/postgres-connection.ts b/src/custom-resouces/postgres-connection/postgres-connection.ts deleted file mode 100644 index 51fdcab..0000000 --- a/src/custom-resouces/postgres-connection/postgres-connection.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { postgresConnectionSpecSchema } from './posgtres-connection.schemas.ts'; -import { PostgresConnectionResource } from './postgres-connection.resource.ts'; - -const postgresConnectionDefinition = createCustomResourceDefinition({ - group: GROUP, - version: 'v1', - kind: 'PostgresConnection', - names: { - plural: 'postgresconnections', - singular: 'postgresconnection', - }, - spec: postgresConnectionSpecSchema, - create: (options) => new PostgresConnectionResource(options), -}); - -export { postgresConnectionDefinition }; diff --git a/src/custom-resouces/postgres-database/portgres-database.schemas.ts b/src/custom-resouces/postgres-database/portgres-database.schemas.ts index 99b9978..edef139 100644 --- a/src/custom-resouces/postgres-database/portgres-database.schemas.ts +++ b/src/custom-resouces/postgres-database/portgres-database.schemas.ts @@ -1,7 +1,22 @@ import { z } from 'zod'; const postgresDatabaseSpecSchema = z.object({ - connection: z.string(), + secretRef: z.string(), }); -export { postgresDatabaseSpecSchema }; +const postgresDatabaseSecretSchema = z.object({ + host: z.string(), + port: z.string(), + user: z.string(), + password: z.string(), +}); + +const postgresDatabaseConnectionSecretSchema = z.object({ + host: z.string(), + port: z.string(), + user: z.string(), + password: z.string(), + database: z.string(), +}); + +export { postgresDatabaseSpecSchema, postgresDatabaseSecretSchema, postgresDatabaseConnectionSecretSchema }; diff --git a/src/custom-resouces/postgres-database/postgres-database.resource.ts b/src/custom-resouces/postgres-database/postgres-database.resource.ts index d741ad9..9620e6b 100644 --- a/src/custom-resouces/postgres-database/postgres-database.resource.ts +++ b/src/custom-resouces/postgres-database/postgres-database.resource.ts @@ -3,22 +3,21 @@ import type { V1Secret } from '@kubernetes/client-node'; import { CustomResource, - type CustomResourceObject, type CustomResourceOptions, type SubresourceResult, } from '../../services/custom-resources/custom-resources.custom-resource.ts'; import { PostgresService } from '../../services/postgres/postgres.service.ts'; -import { - postgresConnectionSecretDataSchema, - type postgresConnectionSpecSchema, -} from '../postgres-connection/posgtres-connection.schemas.ts'; import { ResourceReference } from '../../services/resources/resources.ref.ts'; import { Resource, ResourceService } from '../../services/resources/resources.ts'; import { getWithNamespace } from '../../utils/naming.ts'; -import { API_VERSION } from '../../utils/consts.ts'; -import { decodeSecret } from '../../utils/secrets.ts'; +import { decodeSecret, encodeSecret } from '../../utils/secrets.ts'; +import { isDeepSubset } from '../../utils/objects.ts'; -import type { postgresDatabaseSpecSchema } from './portgres-database.schemas.ts'; +import { + postgresDatabaseConnectionSecretSchema, + postgresDatabaseSecretSchema, + type postgresDatabaseSpecSchema, +} from './portgres-database.schemas.ts'; const SECRET_READY_CONDITION = 'Secret'; const DATABASE_READY_CONDITION = 'Database'; @@ -32,31 +31,23 @@ const secretDataSchema = z.object({ }); class PostgresDatabaseResource extends CustomResource { - #secret: Resource; - #secretName: string; - #connection: ResourceReference>; - #connectionSecret: ResourceReference; + #serverSecret: ResourceReference; + #databaseSecret: Resource; constructor(options: CustomResourceOptions) { super(options); - const resouceService = this.services.get(ResourceService); + this.#serverSecret = new ResourceReference(); - this.#secretName = `postgres-database-${this.name}`; - this.#secret = resouceService.get({ + const resourceService = this.services.get(ResourceService); + this.#databaseSecret = resourceService.get({ apiVersion: 'v1', kind: 'Secret', - name: this.#secretName, + name: `${this.name}-connection`, namespace: this.namespace, }); - this.#connection = new ResourceReference(); - this.#connectionSecret = new ResourceReference(); - this.#updateSecret(); - - this.#secret.on('changed', this.queueReconcile); - this.#connection.on('changed', this.queueReconcile); - this.#connectionSecret.on('changed', this.queueReconcile); + this.#serverSecret.on('changed', this.queueReconcile); } get #dbName() { @@ -68,68 +59,52 @@ class PostgresDatabaseResource extends CustomResource { - const resouceService = this.services.get(ResourceService); - const connectionNames = getWithNamespace(this.spec.connection, this.namespace); - this.#connection.current = resouceService.get({ - apiVersion: API_VERSION, - kind: 'PostgresConnection', - name: connectionNames.name, - namespace: connectionNames.namespace, + const resourceService = this.services.get(ResourceService); + const secretNames = getWithNamespace(this.spec.secretRef, this.namespace); + this.#serverSecret.current = resourceService.get({ + apiVersion: 'v1', + kind: 'Secret', + name: secretNames.name, + namespace: secretNames.namespace, }); - if (this.#connection.current?.exists && this.#connection.current.spec) { - const connectionSecretNames = getWithNamespace( - this.#connection.current.spec.secret, - this.#connection.current.namespace, - ); - this.#connectionSecret.current = resouceService.get({ - apiVersion: 'v1', - kind: 'Secret', - name: connectionSecretNames.name, - namespace: connectionSecretNames.namespace, - }); - } }; #reconcileSecret = async (): Promise => { - const connectionSecret = this.#connectionSecret.current; - if (!connectionSecret?.exists || !connectionSecret.data) { + const serverSecret = this.#serverSecret.current; + const databaseSecret = this.#databaseSecret; + + if (!serverSecret?.exists || !serverSecret.data) { return { ready: false, failed: true, reason: 'MissingConnectionSecret', }; } - - const connectionSecretData = decodeSecret(connectionSecret.data); - - const secret = this.#secret; - const parsed = secretDataSchema.safeParse(decodeSecret(secret.data)); - - if (!parsed.success) { - this.#secret.patch({ - data: { - host: Buffer.from(connectionSecretData?.host || '').toString('base64'), - port: connectionSecretData?.port ? Buffer.from(connectionSecretData.port).toString('base64') : undefined, - user: Buffer.from(this.#userName).toString('base64'), - database: Buffer.from(this.#dbName).toString('base64'), - password: Buffer.from(Buffer.from(crypto.randomUUID()).toString('hex')).toString('base64'), - }, - }); + const serverSecretData = postgresDatabaseSecretSchema.safeParse(decodeSecret(serverSecret.data)); + if (!serverSecretData.success || !serverSecretData.data) { return { ready: false, syncing: true, + reason: 'SecretMissing', }; } - if (parsed.data?.host !== connectionSecretData?.host || parsed.data?.port !== connectionSecretData?.port) { - this.#secret.patch({ - data: { - host: Buffer.from(connectionSecretData?.host || '').toString('base64'), - port: connectionSecretData?.port ? Buffer.from(connectionSecretData.port).toString('base64') : undefined, - }, + const databaseSecretData = postgresDatabaseConnectionSecretSchema.safeParse(decodeSecret(databaseSecret.data)); + const expectedSecret = { + password: crypto.randomUUID(), + host: serverSecretData.data.host, + port: serverSecretData.data.port, + user: this.#userName, + database: this.#dbName, + }; + + if (!isDeepSubset(databaseSecretData.data, expectedSecret)) { + databaseSecret.patch({ + data: encodeSecret(expectedSecret), }); return { ready: false, syncing: true, + reason: 'SecretNotReady', }; } @@ -139,7 +114,7 @@ class PostgresDatabaseResource extends CustomResource => { - const connectionSecret = this.#connectionSecret.current; + const connectionSecret = this.#serverSecret.current; if (!connectionSecret?.exists || !connectionSecret.data) { return { ready: false, @@ -148,21 +123,21 @@ class PostgresDatabaseResource extends CustomResource { - if (!this.exists || this.metadata.deletionTimestamp) { + if (!this.exists || this.metadata?.deletionTimestamp) { return; } this.#updateSecret(); diff --git a/src/custom-resouces/redis-connection/redis-connection.resource.ts b/src/custom-resouces/redis-connection/redis-connection.resource.ts deleted file mode 100644 index f02c572..0000000 --- a/src/custom-resouces/redis-connection/redis-connection.resource.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { V1Secret } from '@kubernetes/client-node'; - -import { - CustomResource, - type CustomResourceOptions, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { ResourceReference } from '../../services/resources/resources.ref.ts'; -import { ResourceService } from '../../services/resources/resources.ts'; -import { getWithNamespace } from '../../utils/naming.ts'; - -import type { redisConnectionSpecSchema } from './redis-connection.schemas.ts'; - -class RedisConnectionResource extends CustomResource { - #secret: ResourceReference; - - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - const secretNames = getWithNamespace(this.spec.secret, this.namespace); - this.#secret = new ResourceReference( - resourceService.get({ - apiVersion: 'v1', - kind: 'Secret', - name: secretNames.name, - namespace: secretNames.namespace, - }), - ); - this.#secret.on('changed', this.queueReconcile); - } - - public reconcile = async () => { - const resourceService = this.services.get(ResourceService); - const secretNames = getWithNamespace(this.spec.secret, this.namespace); - this.#secret.current = resourceService.get({ - apiVersion: 'v1', - kind: 'Secret', - name: secretNames.name, - namespace: secretNames.namespace, - }); - - const current = this.#secret.current; - if (!current?.exists || !current.data) { - return this.conditions.set('Ready', { - status: 'False', - reason: 'MissingSecret', - }); - } - const { host } = current.data; - if (!host) { - return this.conditions.set('Ready', { - status: 'False', - reason: 'MissingHost', - }); - } - await this.conditions.set('Ready', { - status: 'True', - }); - }; -} - -export { RedisConnectionResource }; diff --git a/src/custom-resouces/redis-connection/redis-connection.schemas.ts b/src/custom-resouces/redis-connection/redis-connection.schemas.ts deleted file mode 100644 index 52c2c08..0000000 --- a/src/custom-resouces/redis-connection/redis-connection.schemas.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from 'zod'; - -const redisConnectionSpecSchema = z.object({ - secret: z.string(), -}); - -const redisConnectionSecretDataSchema = z.object({ - host: z.string(), - port: z.string().optional(), - user: z.string().optional(), - password: z.string().optional(), -}); - -export { redisConnectionSpecSchema, redisConnectionSecretDataSchema }; diff --git a/src/custom-resouces/redis-connection/redis-connection.ts b/src/custom-resouces/redis-connection/redis-connection.ts deleted file mode 100644 index 6e6e0f3..0000000 --- a/src/custom-resouces/redis-connection/redis-connection.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { redisConnectionSpecSchema } from './redis-connection.schemas.ts'; -import { RedisConnectionResource } from './redis-connection.resource.ts'; - -const redisConnectionDefinition = createCustomResourceDefinition({ - group: GROUP, - version: 'v1', - kind: 'RedisConnection', - names: { - plural: 'redisconnections', - singular: 'redisconnection', - }, - spec: redisConnectionSpecSchema, - create: (options) => new RedisConnectionResource(options), -}); - -export { redisConnectionDefinition }; diff --git a/src/custom-resouces/redis-server/redis-server.manifests.ts b/src/custom-resouces/redis-server/redis-server.manifests.ts deleted file mode 100644 index 021e215..0000000 --- a/src/custom-resouces/redis-server/redis-server.manifests.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { V1Deployment, V1Service } from '@kubernetes/client-node'; - -import type { CustomResourceObject } from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import type { redisConnectionSpecSchema } from '../redis-connection/redis-connection.schemas.ts'; -import { API_VERSION, CONTROLLED_LABEL } from '../../utils/consts.ts'; - -const deploymentManifest = (): V1Deployment => ({ - apiVersion: 'apps/v1', - kind: 'Deployment', - metadata: { - name: 'redis-server', - namespace: 'homelab', - }, - spec: { - replicas: 1, - selector: { - matchLabels: { - app: 'redis-server', - }, - }, - template: { - metadata: { - labels: { - app: 'redis-server', - }, - }, - spec: { - containers: [ - { - name: 'redis-server', - image: 'redis:latest', - ports: [ - { - containerPort: 6379, - }, - ], - }, - ], - }, - }, - }, -}); - -const serviceManifest = (): V1Service => ({ - apiVersion: 'v1', - kind: 'Service', - metadata: { - name: 'redis-server', - namespace: 'homelab', - }, - spec: { - selector: { - app: 'redis-server', - }, - ports: [ - { - port: 6379, - }, - ], - }, -}); - -type RedisConnectionManifestOptions = { - secretName: string; -}; - -const connectionManifest = ( - options: RedisConnectionManifestOptions, -): CustomResourceObject => ({ - apiVersion: API_VERSION, - kind: 'RedisConnection', - metadata: { - labels: { - ...CONTROLLED_LABEL, - }, - }, - spec: { - secret: options.secretName, - }, -}); - -export { deploymentManifest, serviceManifest, connectionManifest }; diff --git a/src/custom-resouces/redis-server/redis-server.resource.ts b/src/custom-resouces/redis-server/redis-server.resource.ts deleted file mode 100644 index 90a927a..0000000 --- a/src/custom-resouces/redis-server/redis-server.resource.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { V1Deployment, V1Service } from '@kubernetes/client-node'; - -import { - type CustomResourceOptions, - CustomResource, - type CustomResourceObject, -} from '../../services/custom-resources/custom-resources.custom-resource.ts'; -import { - redisConnectionSecretDataSchema, - redisConnectionSpecSchema, -} from '../redis-connection/redis-connection.schemas.ts'; -import { Resource, ResourceService } from '../../services/resources/resources.ts'; -import { API_VERSION } from '../../utils/consts.ts'; -import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts'; -import { SecretService } from '../../services/secrets/secrets.ts'; -import { isDeepSubset } from '../../utils/objects.ts'; - -import { redisServerSpecSchema } from './redis-server.schemas.ts'; -import { connectionManifest, deploymentManifest, serviceManifest } from './redis-server.manifests.ts'; - -class RedisServerResource extends CustomResource { - #resources: { - deployment: Resource; - service: Resource; - connection: Resource>; - secret: EnsuredSecret; - }; - constructor(options: CustomResourceOptions) { - super(options); - const resourceService = this.services.get(ResourceService); - const secretService = this.services.get(SecretService); - this.#resources = { - deployment: resourceService.get({ - apiVersion: 'apps/v1', - kind: 'Deployment', - name: this.name, - namespace: this.namespace, - }), - service: resourceService.get({ - apiVersion: 'v1', - kind: 'Service', - name: this.name, - namespace: this.namespace, - }), - connection: resourceService.get({ - apiVersion: API_VERSION, - kind: 'RedisConnection', - name: this.name, - namespace: this.namespace, - }), - secret: secretService.ensure({ - name: `${this.name}-connection`, - namespace: this.namespace, - schema: redisConnectionSecretDataSchema, - generator: () => ({ - host: `${this.name}.${this.namespace}.svc.cluster.local`, - }), - }), - }; - } - - #reconcileDeployment = async () => { - const { deployment } = this.#resources; - const manifest = deploymentManifest(); - if (!isDeepSubset(deployment.spec, manifest.spec)) { - await deployment.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ChangingDeployment', - message: 'Deployment need changes', - }; - } - return { - ready: true, - reason: 'DeploymentReady', - message: 'Deployment is ready', - }; - }; - - #reconcileService = async () => { - const { service } = this.#resources; - const manifest = serviceManifest(); - if (!isDeepSubset(service.spec, manifest.spec)) { - await service.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ChangingService', - message: 'Service need changes', - }; - } - return { - ready: true, - reason: 'ServiceReady', - message: 'Service is ready', - }; - }; - - #reconcileConnection = async () => { - const { connection, secret } = this.#resources; - if (!secret.isValid || !secret.value) { - return { - ready: false, - failed: true, - reason: 'MissingSecret', - message: 'Secret is missing', - }; - } - const manifest = connectionManifest({ - secretName: secret.name, - }); - if (!isDeepSubset(connection.spec, manifest.spec)) { - await connection.patch(manifest); - return { - ready: false, - syncing: true, - reason: 'ChangingConnection', - message: 'Connection need changes', - }; - } - return { - ready: true, - reason: 'ConnectionReady', - message: 'Connection is ready', - }; - }; - - public reconcile = async () => { - await Promise.allSettled([ - this.reconcileSubresource('Deployment', this.#reconcileDeployment), - this.reconcileSubresource('Service', this.#reconcileService), - this.reconcileSubresource('Connection', this.#reconcileConnection), - ]); - }; -} - -export { RedisServerResource }; diff --git a/src/custom-resouces/redis-server/redis-server.schemas.ts b/src/custom-resouces/redis-server/redis-server.schemas.ts deleted file mode 100644 index cbd36f2..0000000 --- a/src/custom-resouces/redis-server/redis-server.schemas.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from 'zod'; - -const redisServerSpecSchema = z.object({}); - -export { redisServerSpecSchema }; diff --git a/src/custom-resouces/redis-server/redis-server.ts b/src/custom-resouces/redis-server/redis-server.ts deleted file mode 100644 index 50879b6..0000000 --- a/src/custom-resouces/redis-server/redis-server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; -import { GROUP } from '../../utils/consts.ts'; - -import { RedisServerResource } from './redis-server.resource.ts'; -import { redisServerSpecSchema } from './redis-server.schemas.ts'; - -const redisServerDefinition = createCustomResourceDefinition({ - group: GROUP, - version: 'v1', - kind: 'RedisServer', - names: { - plural: 'redis-servers', - singular: 'redis-server', - }, - spec: redisServerSpecSchema, - create: (options) => new RedisServerResource(options), -}); - -export { redisServerDefinition }; diff --git a/src/index.ts b/src/index.ts index e3bae96..29241b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,6 @@ import { ApiException } from '@kubernetes/client-node'; import { Services } from './utils/service.ts'; import { CustomResourceService } from './services/custom-resources/custom-resources.ts'; import { WatcherService } from './services/watchers/watchers.ts'; -import { IstioService } from './services/istio/istio.ts'; import { customResources } from './custom-resouces/custom-resources.ts'; process.on('uncaughtException', (error) => { @@ -73,14 +72,6 @@ await watcherService }) .start(); -await watcherService.watchCustomGroup('networking.istio.io', 'v1', ['gateways', 'virtualservices', 'destinationrules']); -await watcherService.watchCustomGroup('source.toolkit.fluxcd.io', 'v1', ['helmrepositories', 'helmcharts']); -await watcherService.watchCustomGroup('helm.toolkit.fluxcd.io', 'v2', ['helmreleases']); -await watcherService.watchCustomGroup('cert-manager.io', 'v1', ['issuers', 'certificates', 'clusterissuers']); - -const istio = services.get(IstioService); -await istio.start(); - const customResourceService = services.get(CustomResourceService); customResourceService.register(...customResources); diff --git a/src/services/authentik/authentik.types.ts b/src/services/authentik/authentik.types.ts index 51a2148..622c5c4 100644 --- a/src/services/authentik/authentik.types.ts +++ b/src/services/authentik/authentik.types.ts @@ -10,7 +10,7 @@ type AuthentikServerInfo = { type UpsertClientRequest = { name: string; - secret: string; + secret?: string; scopes?: string[]; flows?: { authorization: string; diff --git a/src/services/istio/istio.ts b/src/services/istio/istio.ts deleted file mode 100644 index 1b95ae6..0000000 --- a/src/services/istio/istio.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { V1Deployment } from '@kubernetes/client-node'; - -import type { Services } from '../../utils/service.ts'; -import { ResourceReference } from '../resources/resources.ref.ts'; -import type { Watcher } from '../watchers/watchers.watcher.ts'; -import { WatcherService } from '../watchers/watchers.ts'; -import type { Resource } from '../resources/resources.ts'; - -const ISTIO_APP_SELECTOR = 'istio=gateway-controller'; - -class IstioService { - #gatewayResource: ResourceReference; - #gatewayWatcher: Watcher; - - constructor(services: Services) { - this.#gatewayResource = new ResourceReference(); - const watcherService = services.get(WatcherService); - this.#gatewayWatcher = watcherService.create({ - path: '/apis/apps/v1/deployments', - list: async (k8s) => { - return await k8s.apps.listDeploymentForAllNamespaces({ - labelSelector: ISTIO_APP_SELECTOR, - }); - }, - transform: (manifest) => ({ - apiVersion: 'apps/v1', - kind: 'Deployment', - ...manifest, - }), - verbs: ['add', 'update', 'delete'], - }); - this.#gatewayWatcher.on('changed', this.#handleChange); - } - - #handleChange = (resource: Resource) => { - this.#gatewayResource.current = resource; - }; - - public get gateway() { - return this.#gatewayResource; - } - - public start = async () => { - await this.#gatewayWatcher.start(); - }; -} - -export { IstioService }; diff --git a/test-manifests/authentik-client.yaml b/test-manifests/authentik-client.yaml deleted file mode 100644 index b7a4798..0000000 --- a/test-manifests/authentik-client.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: 'homelab.mortenolsen.pro/v1' -kind: 'AuthentikClient' -metadata: - name: homelab - namespace: homelab -spec: - server: homelab - redirectUris: - - url: http://localhost:3000/api/v1/authentik/oauth2/callback - matchingMode: strict diff --git a/test-manifests/authentik-server.yaml b/test-manifests/authentik-server.yaml deleted file mode 100644 index 9561e07..0000000 --- a/test-manifests/authentik-server.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: 'homelab.mortenolsen.pro/v1' -kind: 'AuthentikServer' -metadata: - name: authentik - namespace: homelab -spec: - domain: homelab - subdomain: authentik - database: postgres - redis: redis diff --git a/test-manifests/domain-service.yaml b/test-manifests/domain-service.yaml deleted file mode 100644 index 0ea5936..0000000 --- a/test-manifests/domain-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: homelab.mortenolsen.pro/v1 -kind: DomainService -metadata: - name: homelab - namespace: homelab -spec: - domain: homelab/homelab - subdomain: test - destination: - host: authentik.svc.cluster.local - port: - number: 80 diff --git a/test-manifests/domain.yaml b/test-manifests/domain.yaml deleted file mode 100644 index 142ea72..0000000 --- a/test-manifests/domain.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: homelab.mortenolsen.pro/v1 -kind: Domain -metadata: - name: homelab - namespace: homelab -spec: - hostname: local.olsen.cloud - issuer: letsencrypt-prod diff --git a/test-manifests/homelab.yaml b/test-manifests/homelab.yaml deleted file mode 100644 index 4c6f3fa..0000000 --- a/test-manifests/homelab.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: homelab.mortenolsen.pro/v1 -kind: Homelab -metadata: - name: homelab - namespace: homelab -spec: - storage: - enabled: true - path: /data/homelab \ No newline at end of file diff --git a/test-manifests/postgres-cluster.yaml b/test-manifests/postgres-cluster.yaml deleted file mode 100644 index 1ba7775..0000000 --- a/test-manifests/postgres-cluster.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: 'homelab.mortenolsen.pro/v1' -kind: 'PostgresCluster' -metadata: - name: 'postgres' - namespace: 'homelab' -spec: {} diff --git a/test-manifests/postgres-database.yaml b/test-manifests/postgres-database.yaml deleted file mode 100644 index 05a697d..0000000 --- a/test-manifests/postgres-database.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: 'homelab.mortenolsen.pro/v1' -kind: 'PostgresDatabase' -metadata: - name: postgres - namespace: 'homelab' -spec: - connection: homelab/postgres diff --git a/test-manifests/redis-database.yaml b/test-manifests/redis-database.yaml deleted file mode 100644 index 230933d..0000000 --- a/test-manifests/redis-database.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: 'homelab.mortenolsen.pro/v1' -kind: 'RedisServer' -metadata: - name: redis - namespace: 'homelab' -spec: {}