Compare commits

..

2 Commits

Author SHA1 Message Date
Morten Olsen
d4b56007f1 add authentik connection crd 2025-08-12 08:36:29 +02:00
Morten Olsen
130bfec468 fix reconciliation of db 2025-08-11 20:00:01 +02:00
10 changed files with 291 additions and 8 deletions

View File

@@ -70,7 +70,7 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
message: 'Server secret not found',
};
}
const url = serverSecretData.data.external_url;
const url = serverSecretData.data.url;
const appName = this.name;
const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(this.#clientSecretResource.data));
@@ -139,8 +139,8 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
const authentikService = this.services.get(AuthentikService);
const authentikServer = authentikService.get({
url: {
internal: serverSecretData.data.internal_url,
external: serverSecretData.data.external_url,
internal: `${serverSecretData.data.name}.${serverSecret.namespace}.svc.cluster.local`,
external: serverSecretData.data.url,
},
token: serverSecretData.data.token,
});

View File

@@ -14,8 +14,8 @@ const authentikClientSpecSchema = z.object({
});
const authentikClientServerSecretSchema = z.object({
internal_url: z.string(),
external_url: z.string(),
name: z.string(),
url: z.string(),
token: z.string(),
});

View File

@@ -0,0 +1,93 @@
import type { V1Secret } from '@kubernetes/client-node';
import deepEqual from 'deep-equal';
import {
CustomResource,
type CustomResourceOptions,
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
import { ResourceService, type Resource } from '../../services/resources/resources.ts';
import type { ValueReference } from '../../services/value-reference/value-reference.instance.ts';
import { ValueReferenceService } from '../../services/value-reference/value-reference.ts';
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
import type { authentikConnectionSpecSchema } from './authentik-connection.schemas.ts';
class AuthentikConnectionResource extends CustomResource<typeof authentikConnectionSpecSchema> {
#name: ValueReference;
#url: ValueReference;
#token: ValueReference;
#secret: Resource<V1Secret>;
constructor(options: CustomResourceOptions<typeof authentikConnectionSpecSchema>) {
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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -168,8 +168,8 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
}
this.#updateSecret();
await Promise.allSettled([
await this.reconcileSubresource(DATABASE_READY_CONDITION, this.#reconcileDatabase),
await this.reconcileSubresource(SECRET_READY_CONDITION, this.#reconcileSecret),
this.reconcileSubresource(DATABASE_READY_CONDITION, this.#reconcileDatabase),
this.reconcileSubresource(SECRET_READY_CONDITION, this.#reconcileSecret),
]);
const secretReady = this.conditions.get(SECRET_READY_CONDITION)?.status === 'True';

View File

@@ -0,0 +1,50 @@
import type { KubernetesObject } from '@kubernetes/client-node';
import { ResourceReference } from './resources.ref.ts';
abstract class ResourceInstance<T extends KubernetesObject> extends ResourceReference<T> {
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 };

View File

@@ -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<typeof valueReferenceInfoSchema>;
type ValueRefOptions = {
services: Services;
namespace: string;
};
type ValueReferenceEvents = {
changed: () => void;
};
class ValueReference extends EventEmitter<ValueReferenceEvents> {
#options: ValueRefOptions;
#ref?: ValueReferenceInfo;
#resource: ResourceReference;
constructor(options: ValueRefOptions) {
super();
this.#options = options;
this.#resource = new ResourceReference<V1Secret>();
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 };

View File

@@ -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 };