This commit is contained in:
Morten Olsen
2025-08-15 20:45:28 +02:00
parent f362f4afc4
commit 2be6bdca84
8 changed files with 116 additions and 43 deletions

View File

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

View File

@@ -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({
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,

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

View File

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

View File

@@ -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('/') || [];

View File

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

View File

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

View File

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