This commit is contained in:
Morten Olsen
2025-08-12 22:32:09 +02:00
parent d4b56007f1
commit b8bb16ccbb
59 changed files with 1855 additions and 290 deletions

View File

@@ -0,0 +1,155 @@
import { ServiceInstance } from '../../instances/service.ts';
import { StatefulSetInstance } from '../../instances/stateful-set.ts';
import { ResourceService } from '../../services/resources/resources.ts';
import {
CustomResource,
type CustomResourceOptions,
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts';
import { SecretService } from '../../services/secrets/secrets.ts';
import { postgresClusterSecretSchema, type postgresClusterSpecSchema } from './postgres-cluster.schemas.ts';
class PostgresClusterController extends CustomResource<typeof postgresClusterSpecSchema> {
#statefulSet: StatefulSetInstance;
#headlessService: ServiceInstance;
#service: ServiceInstance;
#secret: EnsuredSecret<typeof postgresClusterSecretSchema>;
constructor(options: CustomResourceOptions<typeof postgresClusterSpecSchema>) {
super(options);
const resourceService = this.services.get(ResourceService);
const secretService = this.services.get(SecretService);
this.#statefulSet = resourceService.getInstance(
{
apiVersion: 'apps/v1',
kind: 'StatefulSet',
name: this.name,
namespace: this.namespace,
},
StatefulSetInstance,
);
this.#headlessService = resourceService.getInstance(
{
apiVersion: 'v1',
kind: 'Service',
name: `${this.name}-headless`,
namespace: this.namespace,
},
ServiceInstance,
);
this.#service = resourceService.getInstance(
{
apiVersion: 'v1',
kind: 'Service',
name: this.name,
namespace: this.namespace,
},
ServiceInstance,
);
this.#secret = secretService.ensure({
name: this.name,
namespace: this.namespace,
schema: postgresClusterSecretSchema,
generator: () => {
return {
database: 'postgres',
host: `${this.name}.${this.namespace}.svc.cluster.local`,
port: '5432',
username: 'postgres',
password: crypto.randomUUID(),
};
},
});
this.#statefulSet.on('changed', this.queueReconcile);
this.#service.on('changed', this.queueReconcile);
this.#headlessService.on('changed', this.queueReconcile);
this.#secret.resource.on('changed', this.queueReconcile);
}
public reconcile = async () => {
if (!this.exists || this.metadata?.deletionTimestamp || !this.#secret.isValid) {
return;
}
await this.#headlessService.ensure({
metadata: {
ownerReferences: [this.ref],
},
spec: {
clusterIP: 'None',
selector: {
app: this.name,
},
ports: [{ name: 'postgres', port: 5432, targetPort: 5432 }],
},
});
await this.#statefulSet.ensure({
metadata: {
ownerReferences: [this.ref],
},
spec: {
replicas: 1,
serviceName: this.name,
selector: {
matchLabels: {
app: this.name,
},
},
template: {
metadata: {
labels: {
app: this.name,
},
},
spec: {
containers: [
{
name: this.name,
image: 'postgres:17',
ports: [{ containerPort: 5432, name: 'postgres' }],
env: [
{ name: 'POSTGRES_PASSWORD', valueFrom: { secretKeyRef: { name: this.name, key: 'password' } } },
{ name: 'POSTGRES_USER', valueFrom: { secretKeyRef: { name: this.name, key: 'username' } } },
{ name: 'POSTGRES_DB', value: this.name },
{ name: 'PGDATA', value: '/var/lib/postgresql/data/pgdata' },
],
volumeMounts: [{ name: this.name, mountPath: '/var/lib/postgresql/data' }],
},
],
},
},
volumeClaimTemplates: [
{
metadata: {
name: this.name,
},
spec: {
accessModes: ['ReadWriteOnce'],
storageClassName: `${this.spec.environment}-retain`,
resources: {
requests: {
storage: this.spec.storage?.size || '1Gi',
},
},
},
},
],
},
});
await this.#service.ensure({
metadata: {
ownerReferences: [this.ref],
},
spec: {
type: 'ClusterIP',
selector: {
app: this.name,
},
ports: [{ name: 'postgres', port: 5432, targetPort: 5432 }],
},
});
};
}
export { PostgresClusterController };

View File

@@ -0,0 +1,20 @@
import { z } from 'zod';
const postgresClusterSpecSchema = z.object({
environment: z.string(),
storage: z
.object({
size: z.string().optional(),
})
.optional(),
});
const postgresClusterSecretSchema = z.object({
database: z.string(),
host: z.string(),
port: z.string(),
username: z.string(),
password: z.string(),
});
export { postgresClusterSpecSchema, postgresClusterSecretSchema };

View File

@@ -0,0 +1,19 @@
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
import { GROUP } from '../../utils/consts.ts';
import { PostgresClusterController } from './postgres-cluster.controller.ts';
import { postgresClusterSpecSchema } from './postgres-cluster.schemas.ts';
const postgresClusterDefinition = createCustomResourceDefinition({
group: GROUP,
version: 'v1',
kind: 'PostgresCluster',
names: {
plural: 'postgres-clusters',
singular: 'postgres-cluster',
},
spec: postgresClusterSpecSchema,
create: (options) => new PostgresClusterController(options),
});
export { postgresClusterDefinition };