This commit is contained in:
Morten Olsen
2025-08-07 23:10:29 +02:00
parent 9cdbaf7929
commit 32576ad37d
51 changed files with 332 additions and 2862 deletions

View File

@@ -3,22 +3,21 @@ import type { V1Secret } from '@kubernetes/client-node';
import {
CustomResource,
type CustomResourceObject,
type CustomResourceOptions,
type SubresourceResult,
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
import { PostgresService } from '../../services/postgres/postgres.service.ts';
import {
postgresConnectionSecretDataSchema,
type postgresConnectionSpecSchema,
} from '../postgres-connection/posgtres-connection.schemas.ts';
import { ResourceReference } from '../../services/resources/resources.ref.ts';
import { Resource, ResourceService } from '../../services/resources/resources.ts';
import { getWithNamespace } from '../../utils/naming.ts';
import { API_VERSION } from '../../utils/consts.ts';
import { decodeSecret } from '../../utils/secrets.ts';
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
import { isDeepSubset } from '../../utils/objects.ts';
import type { postgresDatabaseSpecSchema } from './portgres-database.schemas.ts';
import {
postgresDatabaseConnectionSecretSchema,
postgresDatabaseSecretSchema,
type postgresDatabaseSpecSchema,
} from './portgres-database.schemas.ts';
const SECRET_READY_CONDITION = 'Secret';
const DATABASE_READY_CONDITION = 'Database';
@@ -32,31 +31,23 @@ const secretDataSchema = z.object({
});
class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpecSchema> {
#secret: Resource<V1Secret>;
#secretName: string;
#connection: ResourceReference<CustomResourceObject<typeof postgresConnectionSpecSchema>>;
#connectionSecret: ResourceReference<V1Secret>;
#serverSecret: ResourceReference<V1Secret>;
#databaseSecret: Resource<V1Secret>;
constructor(options: CustomResourceOptions<typeof postgresDatabaseSpecSchema>) {
super(options);
const resouceService = this.services.get(ResourceService);
this.#serverSecret = new ResourceReference();
this.#secretName = `postgres-database-${this.name}`;
this.#secret = resouceService.get({
const resourceService = this.services.get(ResourceService);
this.#databaseSecret = resourceService.get({
apiVersion: 'v1',
kind: 'Secret',
name: this.#secretName,
name: `${this.name}-connection`,
namespace: this.namespace,
});
this.#connection = new ResourceReference();
this.#connectionSecret = new ResourceReference();
this.#updateSecret();
this.#secret.on('changed', this.queueReconcile);
this.#connection.on('changed', this.queueReconcile);
this.#connectionSecret.on('changed', this.queueReconcile);
this.#serverSecret.on('changed', this.queueReconcile);
}
get #dbName() {
@@ -68,68 +59,52 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
}
#updateSecret = () => {
const resouceService = this.services.get(ResourceService);
const connectionNames = getWithNamespace(this.spec.connection, this.namespace);
this.#connection.current = resouceService.get({
apiVersion: API_VERSION,
kind: 'PostgresConnection',
name: connectionNames.name,
namespace: connectionNames.namespace,
const resourceService = this.services.get(ResourceService);
const secretNames = getWithNamespace(this.spec.secretRef, this.namespace);
this.#serverSecret.current = resourceService.get({
apiVersion: 'v1',
kind: 'Secret',
name: secretNames.name,
namespace: secretNames.namespace,
});
if (this.#connection.current?.exists && this.#connection.current.spec) {
const connectionSecretNames = getWithNamespace(
this.#connection.current.spec.secret,
this.#connection.current.namespace,
);
this.#connectionSecret.current = resouceService.get({
apiVersion: 'v1',
kind: 'Secret',
name: connectionSecretNames.name,
namespace: connectionSecretNames.namespace,
});
}
};
#reconcileSecret = async (): Promise<SubresourceResult> => {
const connectionSecret = this.#connectionSecret.current;
if (!connectionSecret?.exists || !connectionSecret.data) {
const serverSecret = this.#serverSecret.current;
const databaseSecret = this.#databaseSecret;
if (!serverSecret?.exists || !serverSecret.data) {
return {
ready: false,
failed: true,
reason: 'MissingConnectionSecret',
};
}
const connectionSecretData = decodeSecret(connectionSecret.data);
const secret = this.#secret;
const parsed = secretDataSchema.safeParse(decodeSecret(secret.data));
if (!parsed.success) {
this.#secret.patch({
data: {
host: Buffer.from(connectionSecretData?.host || '').toString('base64'),
port: connectionSecretData?.port ? Buffer.from(connectionSecretData.port).toString('base64') : undefined,
user: Buffer.from(this.#userName).toString('base64'),
database: Buffer.from(this.#dbName).toString('base64'),
password: Buffer.from(Buffer.from(crypto.randomUUID()).toString('hex')).toString('base64'),
},
});
const serverSecretData = postgresDatabaseSecretSchema.safeParse(decodeSecret(serverSecret.data));
if (!serverSecretData.success || !serverSecretData.data) {
return {
ready: false,
syncing: true,
reason: 'SecretMissing',
};
}
if (parsed.data?.host !== connectionSecretData?.host || parsed.data?.port !== connectionSecretData?.port) {
this.#secret.patch({
data: {
host: Buffer.from(connectionSecretData?.host || '').toString('base64'),
port: connectionSecretData?.port ? Buffer.from(connectionSecretData.port).toString('base64') : undefined,
},
const databaseSecretData = postgresDatabaseConnectionSecretSchema.safeParse(decodeSecret(databaseSecret.data));
const expectedSecret = {
password: crypto.randomUUID(),
host: serverSecretData.data.host,
port: serverSecretData.data.port,
user: this.#userName,
database: this.#dbName,
};
if (!isDeepSubset(databaseSecretData.data, expectedSecret)) {
databaseSecret.patch({
data: encodeSecret(expectedSecret),
});
return {
ready: false,
syncing: true,
reason: 'SecretNotReady',
};
}
@@ -139,7 +114,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
};
#reconcileDatabase = async (): Promise<SubresourceResult> => {
const connectionSecret = this.#connectionSecret.current;
const connectionSecret = this.#serverSecret.current;
if (!connectionSecret?.exists || !connectionSecret.data) {
return {
ready: false,
@@ -148,21 +123,21 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
};
}
const connectionSecretData = postgresConnectionSecretDataSchema.safeParse(decodeSecret(connectionSecret.data));
const connectionSecretData = postgresDatabaseSecretSchema.safeParse(decodeSecret(connectionSecret.data));
if (!connectionSecretData.success || !connectionSecretData.data) {
return {
ready: false,
syncing: true,
reason: 'ConnectionSecretMissing',
reason: 'SecretMissing',
};
}
const secretData = secretDataSchema.safeParse(decodeSecret(this.#secret.data));
const secretData = postgresDatabaseConnectionSecretSchema.safeParse(decodeSecret(this.#serverSecret.current?.data));
if (!secretData.success || !secretData.data) {
return {
ready: false,
syncing: true,
reason: 'SecretMissing',
reason: 'ConnectionSecretMissing',
};
}
@@ -186,7 +161,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
};
public reconcile = async () => {
if (!this.exists || this.metadata.deletionTimestamp) {
if (!this.exists || this.metadata?.deletionTimestamp) {
return;
}
this.#updateSecret();