mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4b56007f1 | ||
|
|
130bfec468 | ||
|
|
ddb3c79657 | ||
|
|
47cf43b44e |
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 { 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 };
|
||||
|
||||
@@ -37,8 +37,8 @@ class GenerateSecretResource extends CustomResource<typeof generateSecretSpecSch
|
||||
const current = decodeSecret(this.#secretResource.data) || {};
|
||||
|
||||
const expected = {
|
||||
...current,
|
||||
...secrets,
|
||||
...current,
|
||||
};
|
||||
|
||||
if (!isDeepSubset(current, expected)) {
|
||||
|
||||
@@ -9,6 +9,7 @@ const postgresDatabaseSecretSchema = z.object({
|
||||
port: z.string(),
|
||||
user: z.string(),
|
||||
password: z.string(),
|
||||
database: z.string().optional(),
|
||||
});
|
||||
|
||||
const postgresDatabaseConnectionSecretSchema = z.object({
|
||||
|
||||
@@ -95,6 +95,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
||||
port: serverSecretData.data.port,
|
||||
user: this.#userName,
|
||||
database: this.#dbName,
|
||||
...databaseSecretData.data,
|
||||
};
|
||||
|
||||
if (!isDeepSubset(databaseSecretData.data, expectedSecret)) {
|
||||
@@ -132,7 +133,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
||||
};
|
||||
}
|
||||
|
||||
const secretData = postgresDatabaseConnectionSecretSchema.safeParse(decodeSecret(this.#serverSecret.current?.data));
|
||||
const secretData = postgresDatabaseConnectionSecretSchema.safeParse(decodeSecret(this.#databaseSecret.data));
|
||||
if (!secretData.success || !secretData.data) {
|
||||
return {
|
||||
ready: false,
|
||||
@@ -145,6 +146,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
||||
const database = postgresService.get({
|
||||
...connectionSecretData.data,
|
||||
port: connectionSecretData.data.port ? Number(connectionSecretData.data.port) : 5432,
|
||||
database: connectionSecretData.data.database,
|
||||
});
|
||||
await database.upsertRole({
|
||||
name: secretData.data.user,
|
||||
@@ -166,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';
|
||||
|
||||
@@ -10,6 +10,7 @@ type PostgresInstanceOptions = {
|
||||
port?: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database?: string;
|
||||
};
|
||||
|
||||
class PostgresInstance {
|
||||
@@ -23,6 +24,7 @@ class PostgresInstance {
|
||||
user: process.env.FORCE_PG_USER ?? options.user,
|
||||
password: process.env.FORCE_PG_PASSWORD ?? options.password,
|
||||
port: process.env.FORCE_PG_PORT ? parseInt(process.env.FORCE_PG_PORT) : options.port,
|
||||
database: options.database,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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