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 | |
|---|---|---|---|
|
|
91298b3cf7 | ||
|
|
638c288a5c | ||
|
|
2be6bdca84 | ||
|
|
f362f4afc4 |
@@ -26,7 +26,7 @@ rules:
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list", "patch"]
|
||||
verbs: ["get", "watch", "list", "patch", "create", "update", "replace"]
|
||||
- apiGroups: ["apiextensions.k8s.io"]
|
||||
resources: ["customresourcedefinitions"]
|
||||
verbs: ["get", "create", "update", "replace", "patch"]
|
||||
|
||||
9
manifests/client.yaml
Normal file
9
manifests/client.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: homelab.mortenolsen.pro/v1
|
||||
kind: AuthentikClient
|
||||
metadata:
|
||||
name: test-client
|
||||
spec:
|
||||
server: dev/dev-authentik-server
|
||||
redirectUris:
|
||||
- url: https://localhost:3000/api/v1/authentik/oauth2/callback
|
||||
matchingMode: strict
|
||||
@@ -136,7 +136,7 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
|
||||
const authentikService = this.services.get(AuthentikService);
|
||||
const authentikServer = authentikService.get({
|
||||
url: {
|
||||
internal: `http://${serverSecretData.data.host}`,
|
||||
internal: `http://${serverSecretData.data.host}-server`,
|
||||
external: serverSecretData.data.url,
|
||||
},
|
||||
token: serverSecretData.data.token,
|
||||
|
||||
@@ -18,8 +18,13 @@ import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
||||
import type { environmentSpecSchema } from '../environment/environment.schemas.ts';
|
||||
import { HttpServiceInstance } from '../../instances/http-service.ts';
|
||||
import type { redisServerSpecSchema } from '../redis-server/redis-server.schemas.ts';
|
||||
import { PostgresDatabaseInstance } from '../../instances/postgres-database.ts';
|
||||
|
||||
import { authentikServerInitSecretSchema, type authentikServerSpecSchema } from './authentik-server.schemas.ts';
|
||||
import {
|
||||
authentikServerInitSecretSchema,
|
||||
authentikServerSecretSchema,
|
||||
type authentikServerSpecSchema,
|
||||
} from './authentik-server.schemas.ts';
|
||||
|
||||
class AuthentikServerController extends CustomResource<typeof authentikServerSpecSchema> {
|
||||
#environment: ResourceReference<CustomResourceObject<typeof environmentSpecSchema>>;
|
||||
@@ -29,6 +34,7 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
#postgresSecret: ResourceReference<V1Secret>;
|
||||
#httpService: HttpServiceInstance;
|
||||
#redisServer: ResourceReference<CustomResourceObject<typeof redisServerSpecSchema>>;
|
||||
#postgresDatabase: PostgresDatabaseInstance;
|
||||
|
||||
constructor(options: CustomResourceOptions<typeof authentikServerSpecSchema>) {
|
||||
super(options);
|
||||
@@ -55,7 +61,7 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
name: `${this.name}-server`,
|
||||
namespace: this.namespace,
|
||||
},
|
||||
SecretInstance,
|
||||
SecretInstance<typeof authentikServerSecretSchema>,
|
||||
);
|
||||
this.#authentikRelease = resourceService.getInstance(
|
||||
{
|
||||
@@ -75,6 +81,15 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
},
|
||||
HttpServiceInstance,
|
||||
);
|
||||
this.#postgresDatabase = resourceService.getInstance(
|
||||
{
|
||||
apiVersion: API_VERSION,
|
||||
kind: 'PostgresDatabase',
|
||||
name: this.name,
|
||||
namespace: this.namespace,
|
||||
},
|
||||
PostgresDatabaseInstance,
|
||||
);
|
||||
this.#redisServer = new ResourceReference();
|
||||
this.#postgresSecret = new ResourceReference();
|
||||
this.#authentikSecret.on('changed', this.queueReconcile);
|
||||
@@ -92,6 +107,7 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
}
|
||||
|
||||
if (!this.#authentikInitSecret.isValid) {
|
||||
await this.markNotReady('MissingAuthentikInitSecret', 'The authentik init secret is not found');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,25 +121,33 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
namespace: this.namespace,
|
||||
});
|
||||
|
||||
const postgresNames = getWithNamespace(this.spec.postgresCluster, this.namespace);
|
||||
this.#postgresSecret.current = resourceService.get({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
name: postgresNames.name,
|
||||
namespace: postgresNames.namespace,
|
||||
await this.#postgresDatabase.ensure({
|
||||
metadata: {
|
||||
ownerReferences: [this.ref],
|
||||
},
|
||||
spec: {
|
||||
cluster: this.spec.postgresCluster,
|
||||
},
|
||||
});
|
||||
const postgresSecret = this.#postgresDatabase.secret;
|
||||
|
||||
if (!this.#postgresSecret.current?.exists) {
|
||||
if (!postgresSecret.exists) {
|
||||
await this.markNotReady('MissingPostgresSecret', 'The postgres secret is not found');
|
||||
return;
|
||||
}
|
||||
const postgresSecret = decodeSecret(this.#postgresSecret.current.data) || {};
|
||||
const postgresSecretData = decodeSecret(postgresSecret.data) || {};
|
||||
|
||||
if (!this.#environment.current?.exists) {
|
||||
await this.markNotReady(
|
||||
'MissingEnvironment',
|
||||
`Environment ${this.#environment.current?.namespace}/${this.#environment.current?.name} not found`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const domain = this.#environment.current.spec?.domain;
|
||||
if (!domain) {
|
||||
await this.markNotReady('MissingDomain', 'The domain is not set');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,9 +202,9 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
enabled: false,
|
||||
},
|
||||
postgresql: {
|
||||
host: postgresSecret.host,
|
||||
name: postgresSecret.database,
|
||||
user: postgresSecret.username,
|
||||
host: postgresSecretData.host,
|
||||
name: postgresSecretData.database,
|
||||
user: postgresSecretData.username,
|
||||
password: 'file:///postgres-creds/password',
|
||||
},
|
||||
redis: {
|
||||
@@ -192,7 +216,7 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
{
|
||||
name: 'postgres-creds',
|
||||
secret: {
|
||||
secretName: this.#postgresSecret.current.name,
|
||||
secretName: postgresSecret.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -209,7 +233,7 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
{
|
||||
name: 'postgres-creds',
|
||||
secret: {
|
||||
secretName: this.#postgresSecret.current.name,
|
||||
secretName: postgresSecret.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -240,6 +264,7 @@ class AuthentikServerController extends CustomResource<typeof authentikServerSpe
|
||||
},
|
||||
},
|
||||
});
|
||||
await this.markReady();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
||||
import { PostgresService } from '../../services/postgres/postgres.service.ts';
|
||||
import { ResourceReference } from '../../services/resources/resources.ref.ts';
|
||||
import { Resource, ResourceService } from '../../services/resources/resources.ts';
|
||||
import { ResourceService } from '../../services/resources/resources.ts';
|
||||
import { getWithNamespace } from '../../utils/naming.ts';
|
||||
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
||||
import { isDeepSubset } from '../../utils/objects.ts';
|
||||
import { decodeSecret } from '../../utils/secrets.ts';
|
||||
import { postgresClusterSecretSchema } from '../postgres-cluster/postgres-cluster.schemas.ts';
|
||||
import { SecretInstance } from '../../instances/secret.ts';
|
||||
|
||||
import { type postgresDatabaseSpecSchema } from './portgres-database.schemas.ts';
|
||||
|
||||
@@ -20,22 +20,27 @@ const DATABASE_READY_CONDITION = 'Database';
|
||||
|
||||
class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpecSchema> {
|
||||
#clusterSecret: ResourceReference<V1Secret>;
|
||||
#databaseSecret: Resource<V1Secret>;
|
||||
#databaseSecret: SecretInstance<typeof postgresClusterSecretSchema>;
|
||||
|
||||
constructor(options: CustomResourceOptions<typeof postgresDatabaseSpecSchema>) {
|
||||
super(options);
|
||||
const resourceService = this.services.get(ResourceService);
|
||||
|
||||
this.#clusterSecret = new ResourceReference();
|
||||
|
||||
const resourceService = this.services.get(ResourceService);
|
||||
this.#databaseSecret = resourceService.get({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
name: `${this.name}-postgres-database`,
|
||||
namespace: this.namespace,
|
||||
});
|
||||
this.#databaseSecret = resourceService.getInstance(
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
name: `${this.name}-postgres-database`,
|
||||
namespace: this.namespace,
|
||||
},
|
||||
SecretInstance<typeof postgresClusterSecretSchema>,
|
||||
);
|
||||
|
||||
this.#updateSecret();
|
||||
this.#clusterSecret.on('changed', this.queueReconcile);
|
||||
this.#databaseSecret.on('changed', this.queueReconcile);
|
||||
}
|
||||
|
||||
get #dbName() {
|
||||
@@ -52,7 +57,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
||||
this.#clusterSecret.current = resourceService.get({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
name: `${secretNames.name}-postgres-cluster`,
|
||||
name: secretNames.name,
|
||||
namespace: secretNames.namespace,
|
||||
});
|
||||
};
|
||||
@@ -81,21 +86,12 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
||||
password: crypto.randomUUID(),
|
||||
host: serverSecretData.data.host,
|
||||
port: serverSecretData.data.port,
|
||||
user: this.#userName,
|
||||
username: this.#userName,
|
||||
database: this.#dbName,
|
||||
...databaseSecretData.data,
|
||||
};
|
||||
|
||||
if (!isDeepSubset(databaseSecretData.data, expectedSecret)) {
|
||||
databaseSecret.patch({
|
||||
data: encodeSecret(expectedSecret),
|
||||
});
|
||||
return {
|
||||
ready: false,
|
||||
syncing: true,
|
||||
reason: 'SecretNotReady',
|
||||
};
|
||||
}
|
||||
await databaseSecret.ensureData(expectedSecret);
|
||||
|
||||
return {
|
||||
ready: true,
|
||||
|
||||
23
src/instances/postgres-database.ts
Normal file
23
src/instances/postgres-database.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { postgresDatabaseSpecSchema } from '../custom-resouces/postgres-database/portgres-database.schemas.ts';
|
||||
import type { CustomResourceObject } from '../services/custom-resources/custom-resources.custom-resource.ts';
|
||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
||||
import { ResourceService } from '../services/resources/resources.ts';
|
||||
|
||||
import { SecretInstance } from './secret.ts';
|
||||
|
||||
class PostgresDatabaseInstance extends ResourceInstance<CustomResourceObject<typeof postgresDatabaseSpecSchema>> {
|
||||
public get secret() {
|
||||
const resourceService = this.services.get(ResourceService);
|
||||
return resourceService.getInstance(
|
||||
{
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
name: `${this.name}-postgres-database`,
|
||||
namespace: this.namespace,
|
||||
},
|
||||
SecretInstance,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { PostgresDatabaseInstance };
|
||||
@@ -1,20 +1,23 @@
|
||||
import type { V1Secret } from '@kubernetes/client-node';
|
||||
import type { z, ZodObject } from 'zod';
|
||||
|
||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
||||
import { decodeSecret, encodeSecret } from '../utils/secrets.ts';
|
||||
|
||||
class SecretInstance extends ResourceInstance<V1Secret> {
|
||||
class SecretInstance<T extends ZodObject = ExpectedAny> extends ResourceInstance<V1Secret> {
|
||||
public get values() {
|
||||
return decodeSecret(this.data);
|
||||
return decodeSecret(this.data) as z.infer<T>;
|
||||
}
|
||||
|
||||
public ensureData = async (values: Record<string, string>) => {
|
||||
public ensureData = async (values: z.infer<T>) => {
|
||||
await this.ensure({
|
||||
data: encodeSecret(values),
|
||||
data: encodeSecret(values as Record<string, string>),
|
||||
});
|
||||
};
|
||||
|
||||
public readonly ready = true;
|
||||
public get ready() {
|
||||
return this.exists;
|
||||
}
|
||||
}
|
||||
|
||||
export { SecretInstance };
|
||||
|
||||
@@ -179,6 +179,20 @@ abstract class CustomResource<TSpec extends ZodObject> extends EventEmitter<Cust
|
||||
}
|
||||
};
|
||||
|
||||
public markNotReady = async (reason?: string, message?: string) => {
|
||||
await this.conditions.set('Ready', {
|
||||
status: 'False',
|
||||
reason,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
public markReady = async () => {
|
||||
await this.conditions.set('Ready', {
|
||||
status: 'True',
|
||||
});
|
||||
};
|
||||
|
||||
public patchStatus = async (status: Partial<CustomResourceStatus>) => {
|
||||
const k8s = this.services.get(K8sService);
|
||||
const [group, version] = this.apiVersion?.split('/') || [];
|
||||
|
||||
@@ -42,9 +42,9 @@ class PostgresInstance {
|
||||
const existingRole = await this.#db.raw('SELECT 1 FROM pg_roles WHERE rolname = ?', [role.name]);
|
||||
|
||||
if (existingRole.rows.length === 0) {
|
||||
await this.#db.raw(`CREATE ROLE ${role.name} WITH LOGIN PASSWORD '${role.password}'`);
|
||||
await this.#db.raw(`CREATE ROLE "${role.name}" WITH LOGIN PASSWORD '${role.password}'`);
|
||||
} else {
|
||||
await this.#db.raw(`ALTER ROLE ${role.name} WITH PASSWORD '${role.password}'`);
|
||||
await this.#db.raw(`ALTER ROLE "${role.name}" WITH PASSWORD '${role.password}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,9 +52,9 @@ class PostgresInstance {
|
||||
const existingDatabase = await this.#db.raw('SELECT * FROM pg_database WHERE datname = ?', [database.name]);
|
||||
|
||||
if (existingDatabase.rows.length === 0) {
|
||||
await this.#db.raw(`CREATE DATABASE ${database.name} OWNER ${database.owner}`);
|
||||
await this.#db.raw(`CREATE DATABASE "${database.name}" OWNER "${database.owner}"`);
|
||||
} else {
|
||||
await this.#db.raw(`ALTER DATABASE ${database.name} OWNER TO ${database.owner}`);
|
||||
await this.#db.raw(`ALTER DATABASE "${database.name}" OWNER TO "${database.owner}"`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ abstract class ResourceInstance<T extends KubernetesObject> extends ResourceRefe
|
||||
return this.current;
|
||||
}
|
||||
|
||||
public get services() {
|
||||
return this.resource.services;
|
||||
}
|
||||
|
||||
public get exists() {
|
||||
return this.resource.exists;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ class ResourceReference<T extends KubernetesObject = KubernetesObject> extends E
|
||||
this.current = current;
|
||||
}
|
||||
|
||||
public get services() {
|
||||
return this.#current?.services;
|
||||
}
|
||||
|
||||
public get current() {
|
||||
return this.#current;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ class Resource<T extends KubernetesObject = UnknownResource> extends EventEmitte
|
||||
this.#queue = new Queue({ concurrency: 1 });
|
||||
}
|
||||
|
||||
public get services() {
|
||||
return this.#options.services;
|
||||
}
|
||||
|
||||
public get specifier() {
|
||||
return this.#options.data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user