diff --git a/src/custom-resouces/authentik-client/authentik-client.resource.ts b/src/custom-resouces/authentik-client/authentik-client.resource.ts index 34599e5..41a51a8 100644 --- a/src/custom-resouces/authentik-client/authentik-client.resource.ts +++ b/src/custom-resouces/authentik-client/authentik-client.resource.ts @@ -70,7 +70,7 @@ class AuthentikClientResource extends CustomResource { + #name: ValueReference; + #url: ValueReference; + #token: ValueReference; + #secret: Resource; + + constructor(options: CustomResourceOptions) { + super(options); + const valueReferenceService = this.services.get(ValueReferenceService); + const resourceService = this.services.get(ResourceService); + + this.#name = valueReferenceService.get(this.namespace); + this.#url = valueReferenceService.get(this.namespace); + this.#token = valueReferenceService.get(this.namespace); + this.#secret = resourceService.get({ + apiVersion: 'v1', + kind: 'Secret', + name: `${this.name}-authentik-server`, + namespace: this.namespace, + }); + + this.#name.on('changed', this.queueReconcile); + this.#url.on('changed', this.queueReconcile); + this.#token.on('changed', this.queueReconcile); + this.#secret.on('changed', this.queueReconcile); + } + + #updateResources = () => { + this.#name.ref = this.spec.name; + this.#url.ref = this.spec.url; + this.#token.ref = this.spec.token; + }; + + public reconcile = async () => { + this.#updateResources(); + const name = this.#name.value; + const url = this.#url.value; + const token = this.#token.value; + if (!name) { + return await this.conditions.set('Ready', { + status: 'False', + reason: 'MissingName', + }); + } + if (!url) { + return await this.conditions.set('Ready', { + status: 'False', + reason: 'MissingUrl', + }); + } + if (!token) { + return await this.conditions.set('Ready', { + status: 'False', + reason: 'MissingToken', + }); + } + const values = { + name, + url, + token, + }; + const secretValue = decodeSecret(this.#secret.data); + if (!deepEqual(secretValue, values)) { + await this.#secret.patch({ + data: encodeSecret(values), + }); + return await this.conditions.set('Ready', { + status: 'False', + reason: 'UpdatingSecret', + }); + } + + return await this.conditions.set('Ready', { + status: 'True', + }); + }; +} + +export { AuthentikConnectionResource }; diff --git a/src/custom-resouces/authentik-connection/authentik-connection.schemas.ts b/src/custom-resouces/authentik-connection/authentik-connection.schemas.ts new file mode 100644 index 0000000..1e6a415 --- /dev/null +++ b/src/custom-resouces/authentik-connection/authentik-connection.schemas.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +import { valueReferenceInfoSchema } from '../../services/value-reference/value-reference.instance.ts'; + +const authentikConnectionSpecSchema = z.object({ + name: valueReferenceInfoSchema, + url: valueReferenceInfoSchema, + token: valueReferenceInfoSchema, +}); + +export { authentikConnectionSpecSchema }; diff --git a/src/custom-resouces/authentik-connection/authentik-connection.ts b/src/custom-resouces/authentik-connection/authentik-connection.ts new file mode 100644 index 0000000..e5f10c5 --- /dev/null +++ b/src/custom-resouces/authentik-connection/authentik-connection.ts @@ -0,0 +1,19 @@ +import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts'; +import { GROUP } from '../../utils/consts.ts'; + +import { AuthentikConnectionResource } from './authentik-connection.resource.ts'; +import { authentikConnectionSpecSchema } from './authentik-connection.schemas.ts'; + +const authentikConnectionDefinition = createCustomResourceDefinition({ + group: GROUP, + version: 'v1', + kind: 'AuthentikConnection', + names: { + plural: 'authentikconnections', + singular: 'authentikconnection', + }, + spec: authentikConnectionSpecSchema, + create: (options) => new AuthentikConnectionResource(options), +}); + +export { authentikConnectionDefinition }; diff --git a/src/custom-resouces/custom-resources.ts b/src/custom-resouces/custom-resources.ts index 4b4a520..906e224 100644 --- a/src/custom-resouces/custom-resources.ts +++ b/src/custom-resouces/custom-resources.ts @@ -1,7 +1,13 @@ import { authentikClientDefinition } from './authentik-client/authentik-client.ts'; +import { authentikConnectionDefinition } from './authentik-connection/authentik-connection.ts'; import { generateSecretDefinition } from './generate-secret/generate-secret.ts'; import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts'; -const customResources = [postgresDatabaseDefinition, authentikClientDefinition, generateSecretDefinition]; +const customResources = [ + postgresDatabaseDefinition, + authentikClientDefinition, + generateSecretDefinition, + authentikConnectionDefinition, +]; export { customResources }; diff --git a/src/services/resources/resources.instance.ts b/src/services/resources/resources.instance.ts new file mode 100644 index 0000000..3bbaee5 --- /dev/null +++ b/src/services/resources/resources.instance.ts @@ -0,0 +1,50 @@ +import type { KubernetesObject } from '@kubernetes/client-node'; + +import { ResourceReference } from './resources.ref.ts'; + +abstract class ResourceInstance extends ResourceReference { + public get resource() { + if (!this.current) { + throw new Error('Instance needs a resource'); + } + return this.current; + } + + public get manifest() { + return this.resource.metadata; + } + + public get apiVersion() { + return this.resource.apiVersion; + } + + public get kind() { + return this.resource.kind; + } + + public get name() { + return this.resource.name; + } + + public get namespace() { + return this.resource.namespace; + } + + public get metadata() { + return this.resource.metadata; + } + + public get spec() { + return this.resource.spec; + } + + public get data() { + return this.resource.data; + } + + public patch = this.resource.patch; + public reload = this.resource.load; + public delete = this.resource.delete; +} + +export { ResourceInstance }; diff --git a/src/services/value-reference/value-reference.instance.ts b/src/services/value-reference/value-reference.instance.ts new file mode 100644 index 0000000..8d256e7 --- /dev/null +++ b/src/services/value-reference/value-reference.instance.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; +import { V1Secret } from '@kubernetes/client-node'; +import { EventEmitter } from 'eventemitter3'; +import deepEqual from 'deep-equal'; + +import { ResourceReference, ResourceService } from '../resources/resources.ts'; +import type { Services } from '../../utils/service.ts'; +import { getWithNamespace } from '../../utils/naming.ts'; +import { decodeSecret } from '../../utils/secrets.ts'; + +const valueReferenceInfoSchema = z.object({ + value: z.string().optional(), + secretRef: z.string().optional(), + key: z.string().optional(), +}); + +type ValueReferenceInfo = z.infer; + +type ValueRefOptions = { + services: Services; + namespace: string; +}; + +type ValueReferenceEvents = { + changed: () => void; +}; +class ValueReference extends EventEmitter { + #options: ValueRefOptions; + #ref?: ValueReferenceInfo; + #resource: ResourceReference; + + constructor(options: ValueRefOptions) { + super(); + this.#options = options; + this.#resource = new ResourceReference(); + this.#resource.on('changed', this.#handleChange); + } + + public get ref() { + return this.#ref; + } + + public set ref(ref: ValueReferenceInfo | undefined) { + if (deepEqual(this.#ref, ref)) { + return; + } + if (ref?.secretRef && ref.key) { + const { services, namespace } = this.#options; + const resourceService = services.get(ResourceService); + const refNames = getWithNamespace(ref.secretRef, namespace); + this.#resource.current = resourceService.get({ + apiVersion: 'v1', + kind: 'Secret', + name: refNames.name, + namespace: refNames.namespace, + }); + } else { + this.#resource.current = undefined; + } + this.#ref = ref; + } + + public get value() { + console.log('get', this.#ref); + if (!this.#ref) { + return undefined; + } + if (this.#ref.value) { + return this.#ref.value; + } + if (this.#resource.current && this.#ref.key) { + const decoded = decodeSecret(this.#resource.current.data); + return decoded?.[this.#ref.key]; + } + return undefined; + } + + #handleChange = () => { + this.emit('changed'); + }; +} + +export { ValueReference, valueReferenceInfoSchema, type ValueReferenceInfo }; diff --git a/src/services/value-reference/value-reference.ts b/src/services/value-reference/value-reference.ts new file mode 100644 index 0000000..cc8f97e --- /dev/null +++ b/src/services/value-reference/value-reference.ts @@ -0,0 +1,21 @@ +import type { Services } from '../../utils/service.ts'; + +import { ValueReference } from './value-reference.instance.ts'; + +class ValueReferenceService { + #services: Services; + + constructor(services: Services) { + this.#services = services; + } + + public get = (namespace: string) => { + return new ValueReference({ + namespace, + services: this.#services, + }); + }; +} + +export * from './value-reference.instance.ts'; +export { ValueReferenceService };