mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
add authentik connection crd
This commit is contained in:
@@ -70,7 +70,7 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
|
|||||||
message: 'Server secret not found',
|
message: 'Server secret not found',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const url = serverSecretData.data.external_url;
|
const url = serverSecretData.data.url;
|
||||||
const appName = this.name;
|
const appName = this.name;
|
||||||
const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(this.#clientSecretResource.data));
|
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 authentikService = this.services.get(AuthentikService);
|
||||||
const authentikServer = authentikService.get({
|
const authentikServer = authentikService.get({
|
||||||
url: {
|
url: {
|
||||||
internal: serverSecretData.data.internal_url,
|
internal: `${serverSecretData.data.name}.${serverSecret.namespace}.svc.cluster.local`,
|
||||||
external: serverSecretData.data.external_url,
|
external: serverSecretData.data.url,
|
||||||
},
|
},
|
||||||
token: serverSecretData.data.token,
|
token: serverSecretData.data.token,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const authentikClientSpecSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const authentikClientServerSecretSchema = z.object({
|
const authentikClientServerSecretSchema = z.object({
|
||||||
internal_url: z.string(),
|
name: z.string(),
|
||||||
external_url: z.string(),
|
url: z.string(),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import { authentikClientDefinition } from './authentik-client/authentik-client.ts';
|
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 { generateSecretDefinition } from './generate-secret/generate-secret.ts';
|
||||||
import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts';
|
import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts';
|
||||||
|
|
||||||
const customResources = [postgresDatabaseDefinition, authentikClientDefinition, generateSecretDefinition];
|
const customResources = [
|
||||||
|
postgresDatabaseDefinition,
|
||||||
|
authentikClientDefinition,
|
||||||
|
generateSecretDefinition,
|
||||||
|
authentikConnectionDefinition,
|
||||||
|
];
|
||||||
|
|
||||||
export { customResources };
|
export { customResources };
|
||||||
|
|||||||
50
src/services/resources/resources.instance.ts
Normal file
50
src/services/resources/resources.instance.ts
Normal 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 };
|
||||||
83
src/services/value-reference/value-reference.instance.ts
Normal file
83
src/services/value-reference/value-reference.instance.ts
Normal 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 };
|
||||||
21
src/services/value-reference/value-reference.ts
Normal file
21
src/services/value-reference/value-reference.ts
Normal 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 };
|
||||||
Reference in New Issue
Block a user