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
|
||||
|
||||
# Create the secret
|
||||
kubectl create secret generic cloudflare-api-token-secret \
|
||||
kubectl create secret generic cloudflare-api-token \
|
||||
--namespace cert-manager \
|
||||
--from-literal=api-token="${CLOUDFLARE_API_KEY}"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { V1Service, V1Deployment, V1Secret } from '@kubernetes/client-node';
|
||||
import { z } from 'zod';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import {
|
||||
CustomResource,
|
||||
@@ -19,6 +18,7 @@ import { SecretService } from '../../services/secrets/secrets.ts';
|
||||
import { decodeSecret } from '../../utils/secrets.ts';
|
||||
import type { postgresDatabaseSecretSchema } from '../postgres-database/postgres-database.resource.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 { createDomainService, createManifest, createServiceManifest } from './authentik-server.create-manifests.ts';
|
||||
@@ -222,7 +222,7 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
||||
password: databaseSecret.password,
|
||||
},
|
||||
});
|
||||
if (!deepEqual(this.#deploymentWorkerResource.spec, manifest.spec)) {
|
||||
if (!isDeepSubset(this.#deploymentWorkerResource.spec, manifest.spec)) {
|
||||
await this.#deploymentWorkerResource.patch(manifest);
|
||||
return {
|
||||
ready: false,
|
||||
@@ -296,7 +296,7 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
||||
password: databaseSecret.password,
|
||||
},
|
||||
});
|
||||
if (!deepEqual(this.#deploymentServerResource.spec, manifest.spec)) {
|
||||
if (!isDeepSubset(this.#deploymentServerResource.spec, manifest.spec)) {
|
||||
await this.#deploymentServerResource.patch(manifest);
|
||||
return {
|
||||
ready: false,
|
||||
@@ -317,7 +317,7 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
||||
appName: this.#serverName,
|
||||
});
|
||||
|
||||
if (!deepEqual(this.#service.manifest, manifest.spec)) {
|
||||
if (!isDeepSubset(manifest.spec, this.#service.manifest)) {
|
||||
await this.#service.patch(manifest);
|
||||
return {
|
||||
ready: false,
|
||||
@@ -337,9 +337,9 @@ class AuthentikServerResource extends CustomResource<typeof authentikServerSpecS
|
||||
owner: this.ref,
|
||||
domain: this.spec.domain,
|
||||
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);
|
||||
return {
|
||||
ready: false,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
const authentikServerSpecSchema = z.object({
|
||||
domain: z.string(),
|
||||
subdomain: z.string(),
|
||||
database: 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 { redisConnectionDefinition } from './redis-connection/redis-connection.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 = [
|
||||
homelabDefinition,
|
||||
domainDefinition,
|
||||
domainServiceDefinition,
|
||||
postgresClusterDefinition,
|
||||
postgresConnectionDefinition,
|
||||
postgresDatabaseDefinition,
|
||||
redisServerDefinition,
|
||||
redisConnectionDefinition,
|
||||
authentikServerDefinition,
|
||||
authentikClientDefinition,
|
||||
|
||||
@@ -58,7 +58,7 @@ const createCertificateManifest = (options: CreateCertificateManifestOptions) =>
|
||||
kind: 'Certificate',
|
||||
metadata: {
|
||||
name: options.name,
|
||||
namespace: 'istio-ingress',
|
||||
namespace: 'homelab', // TODO: use namespace of gateway controller
|
||||
},
|
||||
spec: {
|
||||
secretName: options.secretName,
|
||||
|
||||
@@ -47,7 +47,7 @@ class DomainResource extends CustomResource<typeof domainSpecSchema> {
|
||||
apiVersion: 'cert-manager.io/v1',
|
||||
kind: 'Certificate',
|
||||
name: `domain-${this.name}`,
|
||||
namespace: 'istio-ingress',
|
||||
namespace: 'homelab',
|
||||
});
|
||||
|
||||
this.#gatewayResource.on('changed', this.queueReconcile);
|
||||
@@ -85,7 +85,7 @@ class DomainResource extends CustomResource<typeof domainSpecSchema> {
|
||||
namespace: this.name,
|
||||
domain: this.spec.hostname,
|
||||
ref: this.ref,
|
||||
gateway: istioService.gateway.current.metadata?.labels?.istio || 'ingress',
|
||||
gateway: istioService.gateway.current.metadata?.labels?.istio || 'gateway-controller',
|
||||
secretName: this.#certSecret,
|
||||
});
|
||||
if (!deepEqual(this.#gatewayResource.current?.spec, manifest.spec)) {
|
||||
|
||||
@@ -257,12 +257,13 @@ const localStorageManifest = (options: LocalStorageManifestOptions): KubernetesO
|
||||
values: {
|
||||
storageClass: {
|
||||
name: 'local-path',
|
||||
provisionerName: 'rancher.io/local-path',
|
||||
defaultClass: true,
|
||||
},
|
||||
nodePathMap: [
|
||||
{
|
||||
node: 'DEFAULT_PATH_FOR_NON_LISTED_NODES',
|
||||
path: options.storagePath,
|
||||
paths: [options.storagePath],
|
||||
},
|
||||
],
|
||||
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 { z } from 'zod';
|
||||
|
||||
import {
|
||||
CustomResource,
|
||||
@@ -8,8 +9,12 @@ import { ResourceReference } from '../../services/resources/resources.ref.ts';
|
||||
import { ResourceService } from '../../services/resources/resources.ts';
|
||||
import { getWithNamespace } from '../../utils/naming.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> {
|
||||
#secret: ResourceReference<V1Secret>;
|
||||
@@ -46,7 +51,9 @@ class PostgresConnectionResource extends CustomResource<typeof postgresConnectio
|
||||
reason: 'MissingSecret',
|
||||
});
|
||||
}
|
||||
const { host, user, password, port } = current.data;
|
||||
const { host, user, password, port } = decodeSecret<z.infer<typeof postgresConnectionSecretDataSchema>>(
|
||||
current.data,
|
||||
)!;
|
||||
if (!host) {
|
||||
return this.conditions.set('Ready', {
|
||||
status: 'False',
|
||||
|
||||
@@ -186,10 +186,10 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
||||
};
|
||||
|
||||
public reconcile = async () => {
|
||||
this.#updateSecret();
|
||||
if (!this.exists || this.metadata.deletionTimestamp) {
|
||||
return;
|
||||
}
|
||||
this.#updateSecret();
|
||||
await Promise.allSettled([
|
||||
await this.reconcileSubresource(DATABASE_READY_CONDITION, this.#reconcileDatabase),
|
||||
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;
|
||||
}
|
||||
|
||||
public get metadata() {
|
||||
public get metadata(): KubernetesObject['metadata'] {
|
||||
const metadata = this.resource.metadata;
|
||||
if (!metadata) {
|
||||
throw new Error('Custom resources needs metadata');
|
||||
return (
|
||||
metadata || {
|
||||
name: this.name,
|
||||
namespace: this.namespace,
|
||||
}
|
||||
return metadata;
|
||||
);
|
||||
}
|
||||
|
||||
public get name() {
|
||||
const name = this.metadata.name;
|
||||
if (!name) {
|
||||
throw new Error('Custom resources needs a name');
|
||||
}
|
||||
return name;
|
||||
return this.resource.specifier.name;
|
||||
}
|
||||
|
||||
public get namespace() {
|
||||
@@ -130,7 +128,7 @@ abstract class CustomResource<TSpec extends ZodObject> extends EventEmitter<Cust
|
||||
}
|
||||
|
||||
public get isSeen() {
|
||||
return this.metadata.generation === this.status?.observedGeneration;
|
||||
return this.metadata?.generation === this.status?.observedGeneration;
|
||||
}
|
||||
|
||||
public get isValidSpec() {
|
||||
@@ -146,7 +144,7 @@ abstract class CustomResource<TSpec extends ZodObject> extends EventEmitter<Cust
|
||||
return;
|
||||
}
|
||||
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 type { Resource } from '../resources/resources.ts';
|
||||
|
||||
const ISTIO_APP_SELECTOR = 'istio=ingress';
|
||||
const ISTIO_APP_SELECTOR = 'istio=gateway-controller';
|
||||
|
||||
class IstioService {
|
||||
#gatewayResource: ResourceReference<V1Deployment>;
|
||||
|
||||
@@ -33,6 +33,14 @@ class EnsuredSecret<T extends ZodObject> {
|
||||
this.#handleChanged();
|
||||
}
|
||||
|
||||
public get name() {
|
||||
return this.#options.name;
|
||||
}
|
||||
|
||||
public get namespace() {
|
||||
return this.#options.namespace;
|
||||
}
|
||||
|
||||
public get resouce() {
|
||||
return this.#resource;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
||||
kind: 'AuthentikServer'
|
||||
metadata:
|
||||
name: homelab
|
||||
name: authentik
|
||||
namespace: homelab
|
||||
spec:
|
||||
domain: homelab
|
||||
database: test2
|
||||
subdomain: authentik
|
||||
database: postgres
|
||||
redis: redis
|
||||
|
||||
@@ -7,6 +7,6 @@ spec:
|
||||
domain: homelab/homelab
|
||||
subdomain: test
|
||||
destination:
|
||||
host: foo.svc.cluster.local
|
||||
host: authentik.svc.cluster.local
|
||||
port:
|
||||
number: 80
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
||||
kind: 'RedisConnection'
|
||||
kind: 'PostgresCluster'
|
||||
metadata:
|
||||
name: 'redis'
|
||||
name: 'postgres'
|
||||
namespace: 'homelab'
|
||||
spec:
|
||||
secret: redis/connection
|
||||
spec: {}
|
||||
@@ -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'
|
||||
kind: 'PostgresDatabase'
|
||||
metadata:
|
||||
name: 'test2'
|
||||
name: postgres
|
||||
namespace: 'homelab'
|
||||
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