mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
stuff
This commit is contained in:
@@ -15,6 +15,6 @@ fi
|
|||||||
kubectl get namespace postgres > /dev/null 2>&1 || kubectl create namespace postgres
|
kubectl get namespace postgres > /dev/null 2>&1 || kubectl create namespace postgres
|
||||||
|
|
||||||
# Create the secret
|
# Create the secret
|
||||||
kubectl create secret generic cloudflare-api-token-secret \
|
kubectl create secret generic cloudflare-api-token \
|
||||||
--namespace cert-manager \
|
--namespace cert-manager \
|
||||||
--from-literal=api-token="${CLOUDFLARE_API_KEY}"
|
--from-literal=api-token="${CLOUDFLARE_API_KEY}"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { V1Service, V1Deployment, V1Secret } from '@kubernetes/client-node';
|
import type { V1Service, V1Deployment, V1Secret } from '@kubernetes/client-node';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import deepEqual from 'deep-equal';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CustomResource,
|
CustomResource,
|
||||||
@@ -19,6 +18,7 @@ import { SecretService } from '../../services/secrets/secrets.ts';
|
|||||||
import { decodeSecret } from '../../utils/secrets.ts';
|
import { decodeSecret } from '../../utils/secrets.ts';
|
||||||
import type { postgresDatabaseSecretSchema } from '../postgres-database/postgres-database.resource.ts';
|
import type { postgresDatabaseSecretSchema } from '../postgres-database/postgres-database.resource.ts';
|
||||||
import type { redisConnectionSpecSchema } from '../redis-connection/redis-connection.schemas.ts';
|
import type { redisConnectionSpecSchema } from '../redis-connection/redis-connection.schemas.ts';
|
||||||
|
import { isDeepSubset } from '../../utils/objects.ts';
|
||||||
|
|
||||||
import { authentikServerSecretSchema, type authentikServerSpecSchema } from './authentik-server.scemas.ts';
|
import { authentikServerSecretSchema, type authentikServerSpecSchema } from './authentik-server.scemas.ts';
|
||||||
import { createDomainService, createManifest, createServiceManifest } from './authentik-server.create-manifests.ts';
|
import { createDomainService, createManifest, createServiceManifest } from './authentik-server.create-manifests.ts';
|
||||||
@@ -222,7 +222,7 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
|||||||
password: databaseSecret.password,
|
password: databaseSecret.password,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!deepEqual(this.#deploymentWorkerResource.spec, manifest.spec)) {
|
if (!isDeepSubset(this.#deploymentWorkerResource.spec, manifest.spec)) {
|
||||||
await this.#deploymentWorkerResource.patch(manifest);
|
await this.#deploymentWorkerResource.patch(manifest);
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -296,7 +296,7 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
|||||||
password: databaseSecret.password,
|
password: databaseSecret.password,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!deepEqual(this.#deploymentServerResource.spec, manifest.spec)) {
|
if (!isDeepSubset(this.#deploymentServerResource.spec, manifest.spec)) {
|
||||||
await this.#deploymentServerResource.patch(manifest);
|
await this.#deploymentServerResource.patch(manifest);
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -317,7 +317,7 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
|||||||
appName: this.#serverName,
|
appName: this.#serverName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deepEqual(this.#service.manifest, manifest.spec)) {
|
if (!isDeepSubset(manifest.spec, this.#service.manifest)) {
|
||||||
await this.#service.patch(manifest);
|
await this.#service.patch(manifest);
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -337,9 +337,9 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
|||||||
owner: this.ref,
|
owner: this.ref,
|
||||||
domain: this.spec.domain,
|
domain: this.spec.domain,
|
||||||
host: `${this.name}.${this.namespace}.svc.cluster.local`,
|
host: `${this.name}.${this.namespace}.svc.cluster.local`,
|
||||||
subdomain: 'authentik',
|
subdomain: this.spec.subdomain,
|
||||||
});
|
});
|
||||||
if (!deepEqual(manifest.spec, this.#domainServiceResource.spec)) {
|
if (!isDeepSubset(manifest.spec, this.#domainServiceResource.spec)) {
|
||||||
await this.#domainServiceResource.patch(manifest);
|
await this.#domainServiceResource.patch(manifest);
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
const authentikServerSpecSchema = z.object({
|
const authentikServerSpecSchema = z.object({
|
||||||
domain: z.string(),
|
domain: z.string(),
|
||||||
|
subdomain: z.string(),
|
||||||
database: z.string(),
|
database: z.string(),
|
||||||
redis: z.string(),
|
redis: z.string(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import { postgresConnectionDefinition } from './postgres-connection/postgres-con
|
|||||||
import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts';
|
import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts';
|
||||||
import { redisConnectionDefinition } from './redis-connection/redis-connection.ts';
|
import { redisConnectionDefinition } from './redis-connection/redis-connection.ts';
|
||||||
import { homelabDefinition } from './homelab/homelab.ts';
|
import { homelabDefinition } from './homelab/homelab.ts';
|
||||||
|
import { postgresClusterDefinition } from './postgres-cluster/postgres-cluster.ts';
|
||||||
|
import { redisServerDefinition } from './redis-server/redis-server.ts';
|
||||||
|
|
||||||
const customResources = [
|
const customResources = [
|
||||||
homelabDefinition,
|
homelabDefinition,
|
||||||
domainDefinition,
|
domainDefinition,
|
||||||
domainServiceDefinition,
|
domainServiceDefinition,
|
||||||
|
postgresClusterDefinition,
|
||||||
postgresConnectionDefinition,
|
postgresConnectionDefinition,
|
||||||
postgresDatabaseDefinition,
|
postgresDatabaseDefinition,
|
||||||
|
redisServerDefinition,
|
||||||
redisConnectionDefinition,
|
redisConnectionDefinition,
|
||||||
authentikServerDefinition,
|
authentikServerDefinition,
|
||||||
authentikClientDefinition,
|
authentikClientDefinition,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const createCertificateManifest = (options: CreateCertificateManifestOptions) =>
|
|||||||
kind: 'Certificate',
|
kind: 'Certificate',
|
||||||
metadata: {
|
metadata: {
|
||||||
name: options.name,
|
name: options.name,
|
||||||
namespace: 'istio-ingress',
|
namespace: 'homelab', // TODO: use namespace of gateway controller
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
secretName: options.secretName,
|
secretName: options.secretName,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class DomainResource extends CustomResource<typeof domainSpecSchema> {
|
|||||||
apiVersion: 'cert-manager.io/v1',
|
apiVersion: 'cert-manager.io/v1',
|
||||||
kind: 'Certificate',
|
kind: 'Certificate',
|
||||||
name: `domain-${this.name}`,
|
name: `domain-${this.name}`,
|
||||||
namespace: 'istio-ingress',
|
namespace: 'homelab',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#gatewayResource.on('changed', this.queueReconcile);
|
this.#gatewayResource.on('changed', this.queueReconcile);
|
||||||
@@ -85,7 +85,7 @@ class DomainResource extends CustomResource<typeof domainSpecSchema> {
|
|||||||
namespace: this.name,
|
namespace: this.name,
|
||||||
domain: this.spec.hostname,
|
domain: this.spec.hostname,
|
||||||
ref: this.ref,
|
ref: this.ref,
|
||||||
gateway: istioService.gateway.current.metadata?.labels?.istio || 'ingress',
|
gateway: istioService.gateway.current.metadata?.labels?.istio || 'gateway-controller',
|
||||||
secretName: this.#certSecret,
|
secretName: this.#certSecret,
|
||||||
});
|
});
|
||||||
if (!deepEqual(this.#gatewayResource.current?.spec, manifest.spec)) {
|
if (!deepEqual(this.#gatewayResource.current?.spec, manifest.spec)) {
|
||||||
|
|||||||
@@ -257,12 +257,13 @@ const localStorageManifest = (options: LocalStorageManifestOptions): KubernetesO
|
|||||||
values: {
|
values: {
|
||||||
storageClass: {
|
storageClass: {
|
||||||
name: 'local-path',
|
name: 'local-path',
|
||||||
|
provisionerName: 'rancher.io/local-path',
|
||||||
defaultClass: true,
|
defaultClass: true,
|
||||||
},
|
},
|
||||||
nodePathMap: [
|
nodePathMap: [
|
||||||
{
|
{
|
||||||
node: 'DEFAULT_PATH_FOR_NON_LISTED_NODES',
|
node: 'DEFAULT_PATH_FOR_NON_LISTED_NODES',
|
||||||
path: options.storagePath,
|
paths: [options.storagePath],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
helper: {
|
helper: {
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import type { V1Deployment, V1PersistentVolumeClaim, V1Service } from '@kubernetes/client-node';
|
||||||
|
|
||||||
|
import type { CustomResourceObject } from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
||||||
|
import type { postgresConnectionSpecSchema } from '../postgres-connection/posgtres-connection.schemas.ts';
|
||||||
|
import { API_VERSION } from '../../utils/consts.ts';
|
||||||
|
|
||||||
|
type PvcOptions = {
|
||||||
|
name: string;
|
||||||
|
owner: ExpectedAny;
|
||||||
|
};
|
||||||
|
const pvcManifest = (options: PvcOptions): V1PersistentVolumeClaim => {
|
||||||
|
return {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'PersistentVolumeClaim',
|
||||||
|
metadata: {
|
||||||
|
ownerReferences: [options.owner],
|
||||||
|
name: options.name,
|
||||||
|
labels: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
'volume.kubernetes.io/storage-class': 'local-path',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
accessModes: ['ReadWriteOnce'],
|
||||||
|
resources: {
|
||||||
|
requests: {
|
||||||
|
storage: '10Gi',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeploymentManifetOptions = {
|
||||||
|
name: string;
|
||||||
|
owner: ExpectedAny;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
const deploymentManifest = (options: DeploymentManifetOptions): V1Deployment => {
|
||||||
|
return {
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'Deployment',
|
||||||
|
metadata: {
|
||||||
|
ownerReferences: [options.owner],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
replicas: 1,
|
||||||
|
selector: {
|
||||||
|
matchLabels: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
metadata: {
|
||||||
|
labels: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
volumes: [{ name: options.name, persistentVolumeClaim: { claimName: options.name } }],
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
name: options.name,
|
||||||
|
image: 'postgres:17',
|
||||||
|
ports: [{ containerPort: 5432 }],
|
||||||
|
volumeMounts: [{ mountPath: '/var/lib/postgresql/data', name: options.name }],
|
||||||
|
env: [
|
||||||
|
{ name: 'POSTGRES_USER', value: options.user },
|
||||||
|
{ name: 'POSTGRES_PASSWORD', value: options.password },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServiceManifestOptions = {
|
||||||
|
name: string;
|
||||||
|
owner: ExpectedAny;
|
||||||
|
};
|
||||||
|
const serviceManifest = (options: ServiceManifestOptions): V1Service => {
|
||||||
|
return {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Service',
|
||||||
|
metadata: {
|
||||||
|
ownerReferences: [options.owner],
|
||||||
|
name: options.name,
|
||||||
|
labels: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
type: 'ClusterIP',
|
||||||
|
ports: [{ port: 5432, targetPort: 5432 }],
|
||||||
|
selector: {
|
||||||
|
app: options.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConnectionManifestOptions = {
|
||||||
|
name: string;
|
||||||
|
owner: ExpectedAny;
|
||||||
|
};
|
||||||
|
const connectionManifest = (
|
||||||
|
options: ConnectionManifestOptions,
|
||||||
|
): CustomResourceObject<typeof postgresConnectionSpecSchema> => ({
|
||||||
|
apiVersion: API_VERSION,
|
||||||
|
kind: 'PostgresConnection',
|
||||||
|
metadata: {
|
||||||
|
ownerReferences: [options.owner],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
secret: `${options.name}-secret`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { pvcManifest, deploymentManifest, serviceManifest, connectionManifest };
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import type { V1Deployment, V1PersistentVolumeClaim, V1Service } from '@kubernetes/client-node';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomResource,
|
||||||
|
type CustomResourceObject,
|
||||||
|
type CustomResourceOptions,
|
||||||
|
type SubresourceResult,
|
||||||
|
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
||||||
|
import { ResourceService, type Resource } from '../../services/resources/resources.ts';
|
||||||
|
import {
|
||||||
|
postgresConnectionSecretDataSchema,
|
||||||
|
type postgresConnectionSpecSchema,
|
||||||
|
} from '../postgres-connection/posgtres-connection.schemas.ts';
|
||||||
|
import { API_VERSION } from '../../utils/consts.ts';
|
||||||
|
import { isDeepSubset } from '../../utils/objects.ts';
|
||||||
|
import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts';
|
||||||
|
import { SecretService } from '../../services/secrets/secrets.ts';
|
||||||
|
|
||||||
|
import type { postgresClusterSpecSchema } from './postgres-cluster.schemas.ts';
|
||||||
|
import { connectionManifest, deploymentManifest, pvcManifest, serviceManifest } from './postgres-cluster.manifests.ts';
|
||||||
|
|
||||||
|
class PostgresClusterResource extends CustomResource<typeof postgresClusterSpecSchema> {
|
||||||
|
#resources: {
|
||||||
|
pvc: Resource<V1PersistentVolumeClaim>;
|
||||||
|
deployment: Resource<V1Deployment>;
|
||||||
|
service: Resource<V1Service>;
|
||||||
|
connection: Resource<CustomResourceObject<typeof postgresConnectionSpecSchema>>;
|
||||||
|
secret: EnsuredSecret<typeof postgresConnectionSecretDataSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(options: CustomResourceOptions<typeof postgresClusterSpecSchema>) {
|
||||||
|
super(options);
|
||||||
|
const resourceService = this.services.get(ResourceService);
|
||||||
|
const secretService = this.services.get(SecretService);
|
||||||
|
this.#resources = {
|
||||||
|
pvc: resourceService.get({
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'PersistentVolumeClaim',
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
}),
|
||||||
|
deployment: resourceService.get({
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'Deployment',
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
}),
|
||||||
|
service: resourceService.get({
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Service',
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
}),
|
||||||
|
connection: resourceService.get({
|
||||||
|
apiVersion: API_VERSION,
|
||||||
|
kind: 'PostgresConnection',
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
}),
|
||||||
|
secret: secretService.ensure({
|
||||||
|
name: `${this.name}-secret`,
|
||||||
|
namespace: this.namespace,
|
||||||
|
schema: postgresConnectionSecretDataSchema,
|
||||||
|
generator: () => ({
|
||||||
|
host: `${this.name}.${this.namespace}.svc.cluster.local`,
|
||||||
|
port: '5432',
|
||||||
|
user: 'postgres',
|
||||||
|
password: Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString('hex'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#reconcilePvc = async (): Promise<SubresourceResult> => {
|
||||||
|
const pvc = this.#resources.pvc;
|
||||||
|
const manifest = pvcManifest({
|
||||||
|
name: this.name,
|
||||||
|
owner: this.ref,
|
||||||
|
});
|
||||||
|
if (!isDeepSubset(pvc.spec, manifest.spec)) {
|
||||||
|
await pvc.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'UpdatingManifest',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#reconcileDeployment = async (): Promise<SubresourceResult> => {
|
||||||
|
const secret = this.#resources.secret;
|
||||||
|
if (!secret.isValid || !secret.value) {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'SecretNotReady',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const deployment = this.#resources.deployment;
|
||||||
|
const manifest = deploymentManifest({
|
||||||
|
name: this.name,
|
||||||
|
owner: this.ref,
|
||||||
|
user: secret.value.user,
|
||||||
|
password: secret.value.password,
|
||||||
|
});
|
||||||
|
if (!isDeepSubset(deployment.spec, manifest.spec)) {
|
||||||
|
await deployment.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'UpdatingManifest',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#reconcileService = async (): Promise<SubresourceResult> => {
|
||||||
|
const service = this.#resources.service;
|
||||||
|
const manifest = serviceManifest({
|
||||||
|
name: this.name,
|
||||||
|
owner: this.ref,
|
||||||
|
});
|
||||||
|
if (!isDeepSubset(service.spec, manifest.spec)) {
|
||||||
|
await service.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'UpdatingManifest',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#reconcileConnection = async (): Promise<SubresourceResult> => {
|
||||||
|
const connection = this.#resources.connection;
|
||||||
|
const manifest = connectionManifest({
|
||||||
|
name: this.name,
|
||||||
|
owner: this.ref,
|
||||||
|
});
|
||||||
|
if (!isDeepSubset(connection.spec, manifest.spec)) {
|
||||||
|
await connection.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'UpdatingManifest',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public reconcile = async () => {
|
||||||
|
await Promise.allSettled([
|
||||||
|
this.reconcileSubresource('PVC', this.#reconcilePvc),
|
||||||
|
this.reconcileSubresource('Deployment', this.#reconcileDeployment),
|
||||||
|
this.reconcileSubresource('Service', this.#reconcileService),
|
||||||
|
this.reconcileSubresource('Connection', this.#reconcileConnection),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PostgresClusterResource };
|
||||||
19
src/custom-resouces/postgres-cluster/postgres-cluster.ts
Normal file
19
src/custom-resouces/postgres-cluster/postgres-cluster.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
||||||
|
import { GROUP } from '../../utils/consts.ts';
|
||||||
|
|
||||||
|
import { postgresClusterSpecSchema } from './postgres-cluster.schemas.ts';
|
||||||
|
import { PostgresClusterResource } from './postgres-cluster.resource.ts';
|
||||||
|
|
||||||
|
const postgresClusterDefinition = createCustomResourceDefinition({
|
||||||
|
group: GROUP,
|
||||||
|
version: 'v1',
|
||||||
|
kind: 'PostgresCluster',
|
||||||
|
names: {
|
||||||
|
plural: 'postgresclusters',
|
||||||
|
singular: 'postgrescluster',
|
||||||
|
},
|
||||||
|
spec: postgresClusterSpecSchema,
|
||||||
|
create: (options) => new PostgresClusterResource(options),
|
||||||
|
});
|
||||||
|
|
||||||
|
export { postgresClusterDefinition };
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { V1Secret } from '@kubernetes/client-node';
|
import type { V1Secret } from '@kubernetes/client-node';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CustomResource,
|
CustomResource,
|
||||||
@@ -8,8 +9,12 @@ import { ResourceReference } from '../../services/resources/resources.ref.ts';
|
|||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
import { ResourceService } from '../../services/resources/resources.ts';
|
||||||
import { getWithNamespace } from '../../utils/naming.ts';
|
import { getWithNamespace } from '../../utils/naming.ts';
|
||||||
import { PostgresService } from '../../services/postgres/postgres.service.ts';
|
import { PostgresService } from '../../services/postgres/postgres.service.ts';
|
||||||
|
import { decodeSecret } from '../../utils/secrets.ts';
|
||||||
|
|
||||||
import type { postgresConnectionSpecSchema } from './posgtres-connection.schemas.ts';
|
import type {
|
||||||
|
postgresConnectionSecretDataSchema,
|
||||||
|
postgresConnectionSpecSchema,
|
||||||
|
} from './posgtres-connection.schemas.ts';
|
||||||
|
|
||||||
class PostgresConnectionResource extends CustomResource<typeof postgresConnectionSpecSchema> {
|
class PostgresConnectionResource extends CustomResource<typeof postgresConnectionSpecSchema> {
|
||||||
#secret: ResourceReference<V1Secret>;
|
#secret: ResourceReference<V1Secret>;
|
||||||
@@ -46,7 +51,9 @@ class PostgresConnectionResource extends CustomResource<typeof postgresConnectio
|
|||||||
reason: 'MissingSecret',
|
reason: 'MissingSecret',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { host, user, password, port } = current.data;
|
const { host, user, password, port } = decodeSecret<z.infer<typeof postgresConnectionSecretDataSchema>>(
|
||||||
|
current.data,
|
||||||
|
)!;
|
||||||
if (!host) {
|
if (!host) {
|
||||||
return this.conditions.set('Ready', {
|
return this.conditions.set('Ready', {
|
||||||
status: 'False',
|
status: 'False',
|
||||||
|
|||||||
@@ -186,10 +186,10 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
};
|
};
|
||||||
|
|
||||||
public reconcile = async () => {
|
public reconcile = async () => {
|
||||||
this.#updateSecret();
|
|
||||||
if (!this.exists || this.metadata.deletionTimestamp) {
|
if (!this.exists || this.metadata.deletionTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.#updateSecret();
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
await this.reconcileSubresource(DATABASE_READY_CONDITION, this.#reconcileDatabase),
|
await this.reconcileSubresource(DATABASE_READY_CONDITION, this.#reconcileDatabase),
|
||||||
await this.reconcileSubresource(SECRET_READY_CONDITION, this.#reconcileSecret),
|
await this.reconcileSubresource(SECRET_READY_CONDITION, this.#reconcileSecret),
|
||||||
|
|||||||
82
src/custom-resouces/redis-server/redis-server.manifests.ts
Normal file
82
src/custom-resouces/redis-server/redis-server.manifests.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { V1Deployment, V1Service } from '@kubernetes/client-node';
|
||||||
|
|
||||||
|
import type { CustomResourceObject } from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
||||||
|
import type { redisConnectionSpecSchema } from '../redis-connection/redis-connection.schemas.ts';
|
||||||
|
import { API_VERSION, CONTROLLED_LABEL } from '../../utils/consts.ts';
|
||||||
|
|
||||||
|
const deploymentManifest = (): V1Deployment => ({
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'Deployment',
|
||||||
|
metadata: {
|
||||||
|
name: 'redis-server',
|
||||||
|
namespace: 'homelab',
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
replicas: 1,
|
||||||
|
selector: {
|
||||||
|
matchLabels: {
|
||||||
|
app: 'redis-server',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
metadata: {
|
||||||
|
labels: {
|
||||||
|
app: 'redis-server',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
name: 'redis-server',
|
||||||
|
image: 'redis:latest',
|
||||||
|
ports: [
|
||||||
|
{
|
||||||
|
containerPort: 6379,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serviceManifest = (): V1Service => ({
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Service',
|
||||||
|
metadata: {
|
||||||
|
name: 'redis-server',
|
||||||
|
namespace: 'homelab',
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
selector: {
|
||||||
|
app: 'redis-server',
|
||||||
|
},
|
||||||
|
ports: [
|
||||||
|
{
|
||||||
|
port: 6379,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type RedisConnectionManifestOptions = {
|
||||||
|
secretName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectionManifest = (
|
||||||
|
options: RedisConnectionManifestOptions,
|
||||||
|
): CustomResourceObject<typeof redisConnectionSpecSchema> => ({
|
||||||
|
apiVersion: API_VERSION,
|
||||||
|
kind: 'RedisConnection',
|
||||||
|
metadata: {
|
||||||
|
labels: {
|
||||||
|
...CONTROLLED_LABEL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
secret: options.secretName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { deploymentManifest, serviceManifest, connectionManifest };
|
||||||
138
src/custom-resouces/redis-server/redis-server.resource.ts
Normal file
138
src/custom-resouces/redis-server/redis-server.resource.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { V1Deployment, V1Service } from '@kubernetes/client-node';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CustomResourceOptions,
|
||||||
|
CustomResource,
|
||||||
|
type CustomResourceObject,
|
||||||
|
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
||||||
|
import {
|
||||||
|
redisConnectionSecretDataSchema,
|
||||||
|
redisConnectionSpecSchema,
|
||||||
|
} from '../redis-connection/redis-connection.schemas.ts';
|
||||||
|
import { Resource, ResourceService } from '../../services/resources/resources.ts';
|
||||||
|
import { API_VERSION } from '../../utils/consts.ts';
|
||||||
|
import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts';
|
||||||
|
import { SecretService } from '../../services/secrets/secrets.ts';
|
||||||
|
import { isDeepSubset } from '../../utils/objects.ts';
|
||||||
|
|
||||||
|
import { redisServerSpecSchema } from './redis-server.schemas.ts';
|
||||||
|
import { connectionManifest, deploymentManifest, serviceManifest } from './redis-server.manifests.ts';
|
||||||
|
|
||||||
|
class RedisServerResource extends CustomResource<typeof redisServerSpecSchema> {
|
||||||
|
#resources: {
|
||||||
|
deployment: Resource<V1Deployment>;
|
||||||
|
service: Resource<V1Service>;
|
||||||
|
connection: Resource<CustomResourceObject<typeof redisConnectionSpecSchema>>;
|
||||||
|
secret: EnsuredSecret<typeof redisConnectionSecretDataSchema>;
|
||||||
|
};
|
||||||
|
constructor(options: CustomResourceOptions<typeof redisServerSpecSchema>) {
|
||||||
|
super(options);
|
||||||
|
const resourceService = this.services.get(ResourceService);
|
||||||
|
const secretService = this.services.get(SecretService);
|
||||||
|
this.#resources = {
|
||||||
|
deployment: resourceService.get({
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'Deployment',
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
}),
|
||||||
|
service: resourceService.get({
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Service',
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
}),
|
||||||
|
connection: resourceService.get({
|
||||||
|
apiVersion: API_VERSION,
|
||||||
|
kind: 'RedisConnection',
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
}),
|
||||||
|
secret: secretService.ensure({
|
||||||
|
name: `${this.name}-connection`,
|
||||||
|
namespace: this.namespace,
|
||||||
|
schema: redisConnectionSecretDataSchema,
|
||||||
|
generator: () => ({
|
||||||
|
host: `${this.name}.${this.namespace}.svc.cluster.local`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#reconcileDeployment = async () => {
|
||||||
|
const { deployment } = this.#resources;
|
||||||
|
const manifest = deploymentManifest();
|
||||||
|
if (!isDeepSubset(deployment.spec, manifest.spec)) {
|
||||||
|
await deployment.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'ChangingDeployment',
|
||||||
|
message: 'Deployment need changes',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
reason: 'DeploymentReady',
|
||||||
|
message: 'Deployment is ready',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#reconcileService = async () => {
|
||||||
|
const { service } = this.#resources;
|
||||||
|
const manifest = serviceManifest();
|
||||||
|
if (!isDeepSubset(service.spec, manifest.spec)) {
|
||||||
|
await service.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'ChangingService',
|
||||||
|
message: 'Service need changes',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
reason: 'ServiceReady',
|
||||||
|
message: 'Service is ready',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#reconcileConnection = async () => {
|
||||||
|
const { connection, secret } = this.#resources;
|
||||||
|
if (!secret.isValid || !secret.value) {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
failed: true,
|
||||||
|
reason: 'MissingSecret',
|
||||||
|
message: 'Secret is missing',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const manifest = connectionManifest({
|
||||||
|
secretName: secret.name,
|
||||||
|
});
|
||||||
|
if (!isDeepSubset(connection.spec, manifest.spec)) {
|
||||||
|
await connection.patch(manifest);
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
syncing: true,
|
||||||
|
reason: 'ChangingConnection',
|
||||||
|
message: 'Connection need changes',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ready: true,
|
||||||
|
reason: 'ConnectionReady',
|
||||||
|
message: 'Connection is ready',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public reconcile = async () => {
|
||||||
|
await Promise.allSettled([
|
||||||
|
this.reconcileSubresource('Deployment', this.#reconcileDeployment),
|
||||||
|
this.reconcileSubresource('Service', this.#reconcileService),
|
||||||
|
this.reconcileSubresource('Connection', this.#reconcileConnection),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RedisServerResource };
|
||||||
5
src/custom-resouces/redis-server/redis-server.schemas.ts
Normal file
5
src/custom-resouces/redis-server/redis-server.schemas.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const redisServerSpecSchema = z.object({});
|
||||||
|
|
||||||
|
export { redisServerSpecSchema };
|
||||||
19
src/custom-resouces/redis-server/redis-server.ts
Normal file
19
src/custom-resouces/redis-server/redis-server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
||||||
|
import { GROUP } from '../../utils/consts.ts';
|
||||||
|
|
||||||
|
import { RedisServerResource } from './redis-server.resource.ts';
|
||||||
|
import { redisServerSpecSchema } from './redis-server.schemas.ts';
|
||||||
|
|
||||||
|
const redisServerDefinition = createCustomResourceDefinition({
|
||||||
|
group: GROUP,
|
||||||
|
version: 'v1',
|
||||||
|
kind: 'RedisServer',
|
||||||
|
names: {
|
||||||
|
plural: 'redis-servers',
|
||||||
|
singular: 'redis-server',
|
||||||
|
},
|
||||||
|
spec: redisServerSpecSchema,
|
||||||
|
create: (options) => new RedisServerResource(options),
|
||||||
|
});
|
||||||
|
|
||||||
|
export { redisServerDefinition };
|
||||||
@@ -89,20 +89,18 @@ abstract class CustomResource<TSpec extends ZodObject> extends EventEmitter<Cust
|
|||||||
return this.resource.kind;
|
return this.resource.kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get metadata() {
|
public get metadata(): KubernetesObject['metadata'] {
|
||||||
const metadata = this.resource.metadata;
|
const metadata = this.resource.metadata;
|
||||||
if (!metadata) {
|
return (
|
||||||
throw new Error('Custom resources needs metadata');
|
metadata || {
|
||||||
|
name: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
}
|
}
|
||||||
return metadata;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get name() {
|
public get name() {
|
||||||
const name = this.metadata.name;
|
return this.resource.specifier.name;
|
||||||
if (!name) {
|
|
||||||
throw new Error('Custom resources needs a name');
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get namespace() {
|
public get namespace() {
|
||||||
@@ -130,7 +128,7 @@ abstract class CustomResource<TSpec extends ZodObject> extends EventEmitter<Cust
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get isSeen() {
|
public get isSeen() {
|
||||||
return this.metadata.generation === this.status?.observedGeneration;
|
return this.metadata?.generation === this.status?.observedGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isValidSpec() {
|
public get isValidSpec() {
|
||||||
@@ -146,7 +144,7 @@ abstract class CustomResource<TSpec extends ZodObject> extends EventEmitter<Cust
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.patchStatus({
|
await this.patchStatus({
|
||||||
observedGeneration: this.metadata.generation,
|
observedGeneration: this.metadata?.generation,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Watcher } from '../watchers/watchers.watcher.ts';
|
|||||||
import { WatcherService } from '../watchers/watchers.ts';
|
import { WatcherService } from '../watchers/watchers.ts';
|
||||||
import type { Resource } from '../resources/resources.ts';
|
import type { Resource } from '../resources/resources.ts';
|
||||||
|
|
||||||
const ISTIO_APP_SELECTOR = 'istio=ingress';
|
const ISTIO_APP_SELECTOR = 'istio=gateway-controller';
|
||||||
|
|
||||||
class IstioService {
|
class IstioService {
|
||||||
#gatewayResource: ResourceReference<V1Deployment>;
|
#gatewayResource: ResourceReference<V1Deployment>;
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ class EnsuredSecret<T extends ZodObject> {
|
|||||||
this.#handleChanged();
|
this.#handleChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get name() {
|
||||||
|
return this.#options.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get namespace() {
|
||||||
|
return this.#options.namespace;
|
||||||
|
}
|
||||||
|
|
||||||
public get resouce() {
|
public get resouce() {
|
||||||
return this.#resource;
|
return this.#resource;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
apiVersion: 'homelab.mortenolsen.pro/v1'
|
||||||
kind: 'AuthentikServer'
|
kind: 'AuthentikServer'
|
||||||
metadata:
|
metadata:
|
||||||
name: homelab
|
name: authentik
|
||||||
namespace: homelab
|
namespace: homelab
|
||||||
spec:
|
spec:
|
||||||
domain: homelab
|
domain: homelab
|
||||||
database: test2
|
subdomain: authentik
|
||||||
|
database: postgres
|
||||||
redis: redis
|
redis: redis
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ spec:
|
|||||||
domain: homelab/homelab
|
domain: homelab/homelab
|
||||||
subdomain: test
|
subdomain: test
|
||||||
destination:
|
destination:
|
||||||
host: foo.svc.cluster.local
|
host: authentik.svc.cluster.local
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
apiVersion: 'homelab.mortenolsen.pro/v1'
|
||||||
kind: 'RedisConnection'
|
kind: 'PostgresCluster'
|
||||||
metadata:
|
metadata:
|
||||||
name: 'redis'
|
name: 'postgres'
|
||||||
namespace: 'homelab'
|
namespace: 'homelab'
|
||||||
spec:
|
spec: {}
|
||||||
secret: redis/connection
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
|
||||||
kind: 'PostgresConnection'
|
|
||||||
metadata:
|
|
||||||
name: 'db'
|
|
||||||
namespace: 'homelab'
|
|
||||||
spec:
|
|
||||||
secret: postgres/postgres-secret
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
apiVersion: 'homelab.mortenolsen.pro/v1'
|
||||||
kind: 'PostgresDatabase'
|
kind: 'PostgresDatabase'
|
||||||
metadata:
|
metadata:
|
||||||
name: 'test2'
|
name: postgres
|
||||||
namespace: 'homelab'
|
namespace: 'homelab'
|
||||||
spec:
|
spec:
|
||||||
connection: homelab/db
|
connection: homelab/postgres
|
||||||
|
|||||||
6
test-manifests/redis-database.yaml
Normal file
6
test-manifests/redis-database.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: 'homelab.mortenolsen.pro/v1'
|
||||||
|
kind: 'RedisServer'
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: 'homelab'
|
||||||
|
spec: {}
|
||||||
Reference in New Issue
Block a user