mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af658ad6c |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -75,7 +75,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
config-name: release-drafter-config.yml
|
config-name: release-drafter-config.yml
|
||||||
publish: true
|
publish: true
|
||||||
assets: |
|
|
||||||
operator.yaml
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -1,14 +1,15 @@
|
|||||||
.PHONY: dev-recreate dev-destroy server-install
|
.PHONY: setup dev-recreate dev-create dev-destroy
|
||||||
|
|
||||||
|
setup:
|
||||||
|
./scripts/setup-server.sh
|
||||||
|
|
||||||
dev-destroy:
|
dev-destroy:
|
||||||
colima delete -f
|
colima delete -f
|
||||||
|
|
||||||
dev-recreate: dev-destroy
|
dev-create:
|
||||||
colima start --network-address --kubernetes -m 8 --k3s-arg="--disable=helm-controller,local-storage,traefik" # --mount ${PWD}/data:/data:w
|
colima start --network-address --kubernetes -m 8 --mount ${PWD}/data:/data:w --k3s-arg="--disable=helm-controller,local-storage"
|
||||||
flux install --components="source-controller,helm-controller"
|
|
||||||
|
|
||||||
setup-flux:
|
dev-recreate: dev-destroy dev-create setup
|
||||||
flux install --components="source-controller,helm-controller"
|
|
||||||
|
|
||||||
server-install:
|
server-install:
|
||||||
curl -sfL https://get.k3s.io | sh -s - --disable traefik,local-storage,helm-controller
|
curl -sfL https://get.k3s.io | sh -s - --disable traefik,local-storage,helm-controller
|
||||||
14
chart/templates/clusterrole.yaml
Normal file
14
chart/templates/clusterrole.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ include "homelab-operator.fullname" . }}
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["create", "get", "watch", "list"]
|
||||||
|
- apiGroups: ["*"]
|
||||||
|
resources: ["*"]
|
||||||
|
verbs: ["get", "watch", "list", "patch"]
|
||||||
|
- apiGroups: ["apiextensions.k8s.io"]
|
||||||
|
resources: ["customresourcedefinitions"]
|
||||||
|
verbs: ["get", "create", "replace"]
|
||||||
@@ -14,9 +14,6 @@ fullnameOverride: ''
|
|||||||
|
|
||||||
storage:
|
storage:
|
||||||
path: /data/volumes
|
path: /data/volumes
|
||||||
reclaimPolicy: Retain
|
|
||||||
allowVolumeExpansion: false
|
|
||||||
volumeBindingMode: WaitForFirstConsumer
|
|
||||||
|
|
||||||
serviceAccount:
|
serviceAccount:
|
||||||
# Specifies whether a service account should be created
|
# Specifies whether a service account should be created
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
apiVersion: storage.k8s.io/v1
|
|
||||||
kind: StorageClass
|
|
||||||
metadata:
|
|
||||||
name: {{ include "homelab-operator.fullname" . }}-local-path
|
|
||||||
labels:
|
|
||||||
{{- include "homelab-operator.labels" . | nindent 4 }}
|
|
||||||
provisioner: reuse-local-path-provisioner
|
|
||||||
parameters:
|
|
||||||
# Add any provisioner-specific parameters here
|
|
||||||
reclaimPolicy: {{ .Values.storage.reclaimPolicy | default "Retain" }}
|
|
||||||
allowVolumeExpansion: {{ .Values.storage.allowVolumeExpansion | default false }}
|
|
||||||
volumeBindingMode: {{ .Values.storage.volumeBindingMode | default "WaitForFirstConsumer" }}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRole
|
|
||||||
metadata:
|
|
||||||
name: {{ include "homelab-operator.fullname" . }}
|
|
||||||
rules:
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["secrets"]
|
|
||||||
verbs: ["create", "get", "watch", "list"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["persistentvolumes"]
|
|
||||||
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["persistentvolumeclaims"]
|
|
||||||
verbs: ["get", "list", "watch", "update", "patch"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["persistentvolumeclaims/status"]
|
|
||||||
verbs: ["update", "patch"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["events"]
|
|
||||||
verbs: ["create", "patch"]
|
|
||||||
- apiGroups: ["storage.k8s.io"]
|
|
||||||
resources: ["storageclasses"]
|
|
||||||
verbs: ["get", "list", "watch"]
|
|
||||||
- apiGroups: ["*"]
|
|
||||||
resources: ["*"]
|
|
||||||
verbs: ["get", "watch", "list", "patch"]
|
|
||||||
- apiGroups: ["apiextensions.k8s.io"]
|
|
||||||
resources: ["customresourcedefinitions"]
|
|
||||||
verbs: ["get", "create", "replace"]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: dev
|
|
||||||
---
|
|
||||||
apiVersion: homelab.mortenolsen.pro/v1
|
|
||||||
kind: Environment
|
|
||||||
metadata:
|
|
||||||
name: dev
|
|
||||||
namespace: dev
|
|
||||||
spec:
|
|
||||||
domain: one.dev.olsen.cloud
|
|
||||||
tls:
|
|
||||||
issuer: letsencrypt-prod
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: example-pvc
|
|
||||||
namespace: default
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
storageClassName: homelab-operator-local-path
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: example-pod
|
|
||||||
namespace: default
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: example-container
|
|
||||||
image: alpine
|
|
||||||
command: ["/bin/sh", "-c", "sleep infinity"]
|
|
||||||
volumeMounts:
|
|
||||||
- name: example-volume
|
|
||||||
mountPath: /data
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 100Mi
|
|
||||||
cpu: "0.1"
|
|
||||||
requests:
|
|
||||||
memory: 50Mi
|
|
||||||
cpu: "0.05"
|
|
||||||
volumes:
|
|
||||||
- name: example-volume
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: example-pvc
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: homelab
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
apiVersion: source.toolkit.fluxcd.io/v1
|
|
||||||
kind: GitRepository
|
|
||||||
metadata:
|
|
||||||
name: homelab
|
|
||||||
namespace: homelab
|
|
||||||
spec:
|
|
||||||
interval: 60m
|
|
||||||
url: https://github.com/morten-olsen/homelab-operator
|
|
||||||
ref:
|
|
||||||
branch: main
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
|
||||||
kind: HelmRelease
|
|
||||||
metadata:
|
|
||||||
name: operator
|
|
||||||
namespace: homelab
|
|
||||||
spec:
|
|
||||||
releaseName: operator
|
|
||||||
chart:
|
|
||||||
spec:
|
|
||||||
chart: chart
|
|
||||||
sourceRef:
|
|
||||||
kind: GitRepository
|
|
||||||
name: homelab
|
|
||||||
namespace: homelab
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import type { Services } from '../utils/service.ts';
|
|
||||||
|
|
||||||
import { NamespaceService } from './namespaces/namespaces.ts';
|
|
||||||
import { ReleaseService } from './releases/releases.ts';
|
|
||||||
import { RepoService } from './repos/repos.ts';
|
|
||||||
import { ClusterIssuerService } from './resources/issuer.ts';
|
|
||||||
|
|
||||||
class BootstrapService {
|
|
||||||
#services: Services;
|
|
||||||
|
|
||||||
constructor(services: Services) {
|
|
||||||
this.#services = services;
|
|
||||||
}
|
|
||||||
public get namespaces() {
|
|
||||||
return this.#services.get(NamespaceService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get repos() {
|
|
||||||
return this.#services.get(RepoService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get releases() {
|
|
||||||
return this.#services.get(ReleaseService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get clusterIssuer() {
|
|
||||||
return this.#services.get(ClusterIssuerService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ensure = async () => {
|
|
||||||
await this.namespaces.ensure();
|
|
||||||
await this.repos.ensure();
|
|
||||||
await this.releases.ensure();
|
|
||||||
await this.clusterIssuer.ensure();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { BootstrapService };
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { NamespaceInstance } from '../../instances/namespace.ts';
|
|
||||||
import type { Services } from '../../utils/service.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
|
|
||||||
class NamespaceService {
|
|
||||||
#homelab: NamespaceInstance;
|
|
||||||
#istioSystem: NamespaceInstance;
|
|
||||||
#certManager: NamespaceInstance;
|
|
||||||
|
|
||||||
constructor(services: Services) {
|
|
||||||
const resourceService = services.get(ResourceService);
|
|
||||||
this.#homelab = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
name: 'homelab',
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#istioSystem = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
name: 'istio-system',
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#certManager = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
name: 'cert-manager',
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#homelab.on('changed', this.ensure);
|
|
||||||
this.#istioSystem.on('changed', this.ensure);
|
|
||||||
this.#certManager.on('changed', this.ensure);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get homelab() {
|
|
||||||
return this.#homelab;
|
|
||||||
}
|
|
||||||
public get istioSystem() {
|
|
||||||
return this.#istioSystem;
|
|
||||||
}
|
|
||||||
public get certManager() {
|
|
||||||
return this.#certManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ensure = async () => {
|
|
||||||
await this.#homelab.ensure({
|
|
||||||
metadata: {
|
|
||||||
labels: {
|
|
||||||
'istio-injection': 'enabled',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#istioSystem.ensure({});
|
|
||||||
await this.#certManager.ensure({});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { NamespaceService };
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import { HelmReleaseInstance } from '../../instances/helm-release.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import { NAMESPACE } from '../../utils/consts.ts';
|
|
||||||
import { Services } from '../../utils/service.ts';
|
|
||||||
import { NamespaceService } from '../namespaces/namespaces.ts';
|
|
||||||
import { RepoService } from '../repos/repos.ts';
|
|
||||||
|
|
||||||
class ReleaseService {
|
|
||||||
#services: Services;
|
|
||||||
#certManager: HelmReleaseInstance;
|
|
||||||
#istioBase: HelmReleaseInstance;
|
|
||||||
#istiod: HelmReleaseInstance;
|
|
||||||
#istioGateway: HelmReleaseInstance;
|
|
||||||
|
|
||||||
constructor(services: Services) {
|
|
||||||
this.#services = services;
|
|
||||||
const resourceService = services.get(ResourceService);
|
|
||||||
this.#certManager = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: 'cert-manager',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#istioBase = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: 'istio-base',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#istiod = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: 'istiod',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#istioGateway = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: 'istio-gateway',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#certManager.on('changed', this.ensure);
|
|
||||||
this.#istioBase.on('changed', this.ensure);
|
|
||||||
this.#istiod.on('changed', this.ensure);
|
|
||||||
this.#istioGateway.on('changed', this.ensure);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get certManager() {
|
|
||||||
return this.#certManager;
|
|
||||||
}
|
|
||||||
public get istioBase() {
|
|
||||||
return this.#istioBase;
|
|
||||||
}
|
|
||||||
public get istiod() {
|
|
||||||
return this.#istiod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ensure = async () => {
|
|
||||||
const namespaceService = this.#services.get(NamespaceService);
|
|
||||||
const repoService = this.#services.get(RepoService);
|
|
||||||
await this.#certManager.ensure({
|
|
||||||
spec: {
|
|
||||||
targetNamespace: namespaceService.certManager.name,
|
|
||||||
interval: '1h',
|
|
||||||
values: {
|
|
||||||
installCRDs: true,
|
|
||||||
},
|
|
||||||
chart: {
|
|
||||||
spec: {
|
|
||||||
chart: 'cert-manager',
|
|
||||||
version: 'v1.18.2',
|
|
||||||
sourceRef: {
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: repoService.jetstack.name,
|
|
||||||
namespace: repoService.jetstack.namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#istioBase.ensure({
|
|
||||||
spec: {
|
|
||||||
targetNamespace: namespaceService.istioSystem.name,
|
|
||||||
interval: '1h',
|
|
||||||
values: {
|
|
||||||
defaultRevision: 'default',
|
|
||||||
profile: 'ambient',
|
|
||||||
},
|
|
||||||
chart: {
|
|
||||||
spec: {
|
|
||||||
chart: 'base',
|
|
||||||
version: '1.24.3',
|
|
||||||
sourceRef: {
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: repoService.istio.name,
|
|
||||||
namespace: repoService.istio.namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#istiod.ensure({
|
|
||||||
spec: {
|
|
||||||
targetNamespace: namespaceService.istioSystem.name,
|
|
||||||
interval: '1h',
|
|
||||||
dependsOn: [
|
|
||||||
{
|
|
||||||
name: this.#istioBase.name,
|
|
||||||
namespace: this.#istioBase.namespace,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
chart: {
|
|
||||||
spec: {
|
|
||||||
chart: 'istiod',
|
|
||||||
version: '1.24.3',
|
|
||||||
sourceRef: {
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: repoService.istio.name,
|
|
||||||
namespace: repoService.istio.namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#istioGateway.ensure({
|
|
||||||
spec: {
|
|
||||||
targetNamespace: NAMESPACE,
|
|
||||||
interval: '1h',
|
|
||||||
dependsOn: [
|
|
||||||
{
|
|
||||||
name: this.#istioBase.name,
|
|
||||||
namespace: this.#istioBase.namespace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: this.#istiod.name,
|
|
||||||
namespace: this.#istiod.namespace,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
chart: {
|
|
||||||
spec: {
|
|
||||||
chart: 'gateway',
|
|
||||||
version: '1.24.3',
|
|
||||||
sourceRef: {
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: repoService.istio.name,
|
|
||||||
namespace: repoService.istio.namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ReleaseService };
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import type { Services } from '../../utils/service.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import { HelmRepoInstance } from '../../instances/helm-repo.ts';
|
|
||||||
import { NAMESPACE } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
class RepoService {
|
|
||||||
#jetstack: HelmRepoInstance;
|
|
||||||
#istio: HelmRepoInstance;
|
|
||||||
#authentik: HelmRepoInstance;
|
|
||||||
#containerro: HelmRepoInstance;
|
|
||||||
|
|
||||||
constructor(services: Services) {
|
|
||||||
const resourceService = services.get(ResourceService);
|
|
||||||
this.#jetstack = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: 'jetstack',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#istio = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: 'istio',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#authentik = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: 'authentik',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#containerro = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: 'containerro',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
HelmRepoInstance,
|
|
||||||
);
|
|
||||||
this.#jetstack.on('changed', this.ensure);
|
|
||||||
this.#istio.on('changed', this.ensure);
|
|
||||||
this.#authentik.on('changed', this.ensure);
|
|
||||||
this.#containerro.on('changed', this.ensure);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get jetstack() {
|
|
||||||
return this.#jetstack;
|
|
||||||
}
|
|
||||||
public get istio() {
|
|
||||||
return this.#istio;
|
|
||||||
}
|
|
||||||
public get authentik() {
|
|
||||||
return this.#authentik;
|
|
||||||
}
|
|
||||||
public get containerro() {
|
|
||||||
return this.#containerro;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ensure = async () => {
|
|
||||||
await this.#jetstack.ensure({
|
|
||||||
metadata: {
|
|
||||||
name: 'jetstack',
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '1h',
|
|
||||||
url: 'https://charts.jetstack.io',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#istio.ensure({
|
|
||||||
metadata: {
|
|
||||||
name: 'istio',
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '1h',
|
|
||||||
url: 'https://istio-release.storage.googleapis.com/charts',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#authentik.ensure({
|
|
||||||
metadata: {
|
|
||||||
name: 'authentik',
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '1h',
|
|
||||||
url: 'https://charts.goauthentik.io',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#containerro.ensure({
|
|
||||||
metadata: {
|
|
||||||
name: 'containerro',
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '1h',
|
|
||||||
url: 'https://charts.containeroo.ch',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RepoService };
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { ClusterIssuerInstance } from '../../instances/cluster-issuer.ts';
|
|
||||||
import { CustomDefinitionInstance } from '../../instances/custom-resource-definition.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import type { Services } from '../../utils/service.ts';
|
|
||||||
|
|
||||||
class ClusterIssuerService {
|
|
||||||
#clusterIssuerCrd: CustomDefinitionInstance;
|
|
||||||
#clusterIssuer: ClusterIssuerInstance;
|
|
||||||
|
|
||||||
constructor(services: Services) {
|
|
||||||
const resourceService = services.get(ResourceService);
|
|
||||||
this.#clusterIssuerCrd = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'CustomResourceDefinition',
|
|
||||||
name: 'clusterissuers.cert-manager.io',
|
|
||||||
},
|
|
||||||
CustomDefinitionInstance,
|
|
||||||
);
|
|
||||||
this.#clusterIssuer = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'ClusterIssuer',
|
|
||||||
name: 'cluster-issuer',
|
|
||||||
},
|
|
||||||
ClusterIssuerInstance,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#clusterIssuerCrd.on('changed', this.ensure);
|
|
||||||
this.#clusterIssuer.on('changed', this.ensure);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ensure = async () => {
|
|
||||||
if (!this.#clusterIssuerCrd.ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.#clusterIssuer.ensure({
|
|
||||||
spec: {
|
|
||||||
acme: {
|
|
||||||
server: 'https://acme-v02.api.letsencrypt.org/directory',
|
|
||||||
email: 'admin@example.com',
|
|
||||||
privateKeySecretRef: {
|
|
||||||
name: 'cluster-issuer-key',
|
|
||||||
},
|
|
||||||
solvers: [
|
|
||||||
{
|
|
||||||
dns01: {
|
|
||||||
cloudflare: {
|
|
||||||
email: 'admin@example.com',
|
|
||||||
apiKeySecretRef: {
|
|
||||||
name: 'cloudflare-api-key',
|
|
||||||
key: 'api-key',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ClusterIssuerService };
|
|
||||||
@@ -13,9 +13,12 @@ import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
|||||||
import { CONTROLLED_LABEL } from '../../utils/consts.ts';
|
import { CONTROLLED_LABEL } from '../../utils/consts.ts';
|
||||||
import { isDeepSubset } from '../../utils/objects.ts';
|
import { isDeepSubset } from '../../utils/objects.ts';
|
||||||
import { AuthentikService } from '../../services/authentik/authentik.service.ts';
|
import { AuthentikService } from '../../services/authentik/authentik.service.ts';
|
||||||
import { authentikServerSecretSchema } from '../authentik-server/authentik-server.schemas.ts';
|
|
||||||
|
|
||||||
import { authentikClientSecretSchema, type authentikClientSpecSchema } from './authentik-client.schemas.ts';
|
import {
|
||||||
|
authentikClientSecretSchema,
|
||||||
|
authentikClientServerSecretSchema,
|
||||||
|
type authentikClientSpecSchema,
|
||||||
|
} from './authentik-client.schemas.ts';
|
||||||
|
|
||||||
class AuthentikClientResource extends CustomResource<typeof authentikClientSpecSchema> {
|
class AuthentikClientResource extends CustomResource<typeof authentikClientSpecSchema> {
|
||||||
#serverSecret: ResourceReference<V1Secret>;
|
#serverSecret: ResourceReference<V1Secret>;
|
||||||
@@ -40,7 +43,7 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
|
|||||||
}
|
}
|
||||||
|
|
||||||
#updateResouces = () => {
|
#updateResouces = () => {
|
||||||
const serverSecretNames = getWithNamespace(`${this.spec.server}-server`, this.namespace);
|
const serverSecretNames = getWithNamespace(this.spec.secretRef, this.namespace);
|
||||||
const resourceService = this.services.get(ResourceService);
|
const resourceService = this.services.get(ResourceService);
|
||||||
this.#serverSecret.current = resourceService.get({
|
this.#serverSecret.current = resourceService.get({
|
||||||
apiVersion: 'v1',
|
apiVersion: 'v1',
|
||||||
@@ -59,7 +62,7 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
|
|||||||
message: 'Server or server secret not found',
|
message: 'Server or server secret not found',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const serverSecretData = authentikServerSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
const serverSecretData = authentikClientServerSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
||||||
if (!serverSecretData.success || !serverSecretData.data) {
|
if (!serverSecretData.success || !serverSecretData.data) {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -67,7 +70,7 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
|
|||||||
message: 'Server secret not found',
|
message: 'Server secret not found',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const url = serverSecretData.data.url;
|
const url = serverSecretData.data.external_url;
|
||||||
const appName = this.name;
|
const appName = this.name;
|
||||||
const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(this.#clientSecretResource.data));
|
const clientSecretData = authentikClientSecretSchema.safeParse(decodeSecret(this.#clientSecretResource.data));
|
||||||
|
|
||||||
@@ -115,7 +118,7 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverSecretData = authentikServerSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
const serverSecretData = authentikClientServerSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
||||||
if (!serverSecretData.success || !serverSecretData.data) {
|
if (!serverSecretData.success || !serverSecretData.data) {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -136,8 +139,8 @@ class AuthentikClientResource extends CustomResource<typeof authentikClientSpecS
|
|||||||
const authentikService = this.services.get(AuthentikService);
|
const authentikService = this.services.get(AuthentikService);
|
||||||
const authentikServer = authentikService.get({
|
const authentikServer = authentikService.get({
|
||||||
url: {
|
url: {
|
||||||
internal: `http://${serverSecretData.data.host}`,
|
internal: serverSecretData.data.internal_url,
|
||||||
external: serverSecretData.data.url,
|
external: serverSecretData.data.external_url,
|
||||||
},
|
},
|
||||||
token: serverSecretData.data.token,
|
token: serverSecretData.data.token,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ClientTypeEnum, SubModeEnum } from '@goauthentik/api';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const authentikClientSpecSchema = z.object({
|
const authentikClientSpecSchema = z.object({
|
||||||
server: z.string(),
|
secretRef: z.string(),
|
||||||
subMode: z.enum(SubModeEnum).optional(),
|
subMode: z.enum(SubModeEnum).optional(),
|
||||||
clientType: z.enum(ClientTypeEnum).optional(),
|
clientType: z.enum(ClientTypeEnum).optional(),
|
||||||
redirectUris: z.array(
|
redirectUris: z.array(
|
||||||
@@ -13,6 +13,12 @@ const authentikClientSpecSchema = z.object({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authentikClientServerSecretSchema = z.object({
|
||||||
|
internal_url: z.string(),
|
||||||
|
external_url: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const authentikClientSecretSchema = z.object({
|
const authentikClientSecretSchema = z.object({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string().optional(),
|
clientSecret: z.string().optional(),
|
||||||
@@ -25,4 +31,4 @@ const authentikClientSecretSchema = z.object({
|
|||||||
jwks: z.string(),
|
jwks: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export { authentikClientSpecSchema, authentikClientSecretSchema };
|
export { authentikClientSpecSchema, authentikClientSecretSchema, authentikClientServerSecretSchema };
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
import type { V1Secret } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { RepoService } from '../../bootstrap/repos/repos.ts';
|
|
||||||
import { HelmReleaseInstance } from '../../instances/helm-release.ts';
|
|
||||||
import { SecretInstance } from '../../instances/secret.ts';
|
|
||||||
import {
|
|
||||||
CustomResource,
|
|
||||||
type CustomResourceOptions,
|
|
||||||
type CustomResourceObject,
|
|
||||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceReference } from '../../services/resources/resources.ref.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import type { EnsuredSecret } from '../../services/secrets/secrets.secret.ts';
|
|
||||||
import { SecretService } from '../../services/secrets/secrets.ts';
|
|
||||||
import { API_VERSION } from '../../utils/consts.ts';
|
|
||||||
import { getWithNamespace } from '../../utils/naming.ts';
|
|
||||||
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 { authentikServerInitSecretSchema, type authentikServerSpecSchema } from './authentik-server.schemas.ts';
|
|
||||||
|
|
||||||
class AuthentikServerController extends CustomResource<typeof authentikServerSpecSchema> {
|
|
||||||
#environment: ResourceReference<CustomResourceObject<typeof environmentSpecSchema>>;
|
|
||||||
#authentikInitSecret: EnsuredSecret<typeof authentikServerInitSecretSchema>;
|
|
||||||
#authentikSecret: SecretInstance;
|
|
||||||
#authentikRelease: HelmReleaseInstance;
|
|
||||||
#postgresSecret: ResourceReference<V1Secret>;
|
|
||||||
#httpService: HttpServiceInstance;
|
|
||||||
#redisServer: ResourceReference<CustomResourceObject<typeof redisServerSpecSchema>>;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof authentikServerSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const secretService = this.services.get(SecretService);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
|
|
||||||
this.#environment = new ResourceReference();
|
|
||||||
this.#authentikInitSecret = secretService.ensure({
|
|
||||||
owner: [this.ref],
|
|
||||||
name: `${this.name}-init`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
schema: authentikServerInitSecretSchema,
|
|
||||||
generator: () => ({
|
|
||||||
AUTHENTIK_BOOTSTRAP_TOKEN: crypto.randomUUID(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_PASSWORD: crypto.randomUUID(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_EMAIL: 'admin@example.com',
|
|
||||||
AUTHENTIK_SECRET_KEY: crypto.randomUUID(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
this.#authentikSecret = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Secret',
|
|
||||||
name: `${this.name}-server`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
SecretInstance,
|
|
||||||
);
|
|
||||||
this.#authentikRelease = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'helm.toolkit.fluxcd.io/v2',
|
|
||||||
kind: 'HelmRelease',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
HelmReleaseInstance,
|
|
||||||
);
|
|
||||||
this.#httpService = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'HttpService',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
HttpServiceInstance,
|
|
||||||
);
|
|
||||||
this.#redisServer = new ResourceReference();
|
|
||||||
this.#postgresSecret = new ResourceReference();
|
|
||||||
this.#authentikSecret.on('changed', this.queueReconcile);
|
|
||||||
this.#authentikInitSecret.resource.on('deleted', this.queueReconcile);
|
|
||||||
this.#environment.on('changed', this.queueReconcile);
|
|
||||||
this.#authentikRelease.on('changed', this.queueReconcile);
|
|
||||||
this.#postgresSecret.on('changed', this.queueReconcile);
|
|
||||||
this.#httpService.on('changed', this.queueReconcile);
|
|
||||||
this.#redisServer.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
if (!this.exists || this.metadata?.deletionTimestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.#authentikInitSecret.isValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
const environmentNames = getWithNamespace(this.spec.environment, this.namespace);
|
|
||||||
|
|
||||||
this.#environment.current = resourceService.get({
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'Environment',
|
|
||||||
name: environmentNames.name,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.#postgresSecret.current?.exists) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const postgresSecret = decodeSecret(this.#postgresSecret.current.data) || {};
|
|
||||||
|
|
||||||
if (!this.#environment.current?.exists) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const domain = this.#environment.current.spec?.domain;
|
|
||||||
if (!domain) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretData = {
|
|
||||||
url: `https://${this.spec.subdomain}.${domain}`,
|
|
||||||
host: `${this.name}.${this.namespace}.svc.cluster.local`,
|
|
||||||
token: this.#authentikInitSecret.value?.AUTHENTIK_BOOTSTRAP_TOKEN ?? '',
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.#authentikSecret.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
data: encodeSecret(secretData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const repoService = this.services.get(RepoService);
|
|
||||||
|
|
||||||
const redisNames = getWithNamespace(this.spec.redisServer, this.namespace);
|
|
||||||
const redisHost = `${redisNames.name}.${redisNames.namespace}.svc.cluster.local`;
|
|
||||||
|
|
||||||
await this.#authentikRelease.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
interval: '60m',
|
|
||||||
chart: {
|
|
||||||
spec: {
|
|
||||||
chart: 'authentik',
|
|
||||||
version: '2025.6.4',
|
|
||||||
sourceRef: {
|
|
||||||
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
||||||
kind: 'HelmRepository',
|
|
||||||
name: repoService.authentik.name,
|
|
||||||
namespace: repoService.authentik.namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
global: {
|
|
||||||
envFrom: [
|
|
||||||
{
|
|
||||||
secretRef: {
|
|
||||||
name: this.#authentikInitSecret.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
authentik: {
|
|
||||||
error_reporting: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
postgresql: {
|
|
||||||
host: postgresSecret.host,
|
|
||||||
name: postgresSecret.database,
|
|
||||||
user: postgresSecret.username,
|
|
||||||
password: 'file:///postgres-creds/password',
|
|
||||||
},
|
|
||||||
redis: {
|
|
||||||
host: redisHost,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
secret: {
|
|
||||||
secretName: this.#postgresSecret.current.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
volumeMounts: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
mountPath: '/postgres-creds',
|
|
||||||
readOnly: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
worker: {
|
|
||||||
volumes: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
secret: {
|
|
||||||
secretName: this.#postgresSecret.current.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
volumeMounts: [
|
|
||||||
{
|
|
||||||
name: 'postgres-creds',
|
|
||||||
mountPath: '/postgres-creds',
|
|
||||||
readOnly: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#httpService.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
environment: this.spec.environment,
|
|
||||||
subdomain: this.spec.subdomain,
|
|
||||||
destination: {
|
|
||||||
host: `${this.name}-server.${this.namespace}.svc.cluster.local`,
|
|
||||||
port: {
|
|
||||||
number: 80,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AuthentikServerController };
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const authentikServerSpecSchema = z.object({
|
|
||||||
redisServer: z.string(),
|
|
||||||
postgresCluster: z.string(),
|
|
||||||
environment: z.string(),
|
|
||||||
subdomain: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const authentikServerInitSecretSchema = z.object({
|
|
||||||
AUTHENTIK_BOOTSTRAP_TOKEN: z.string(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_PASSWORD: z.string(),
|
|
||||||
AUTHENTIK_BOOTSTRAP_EMAIL: z.string(),
|
|
||||||
AUTHENTIK_SECRET_KEY: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const authentikServerSecretSchema = z.object({
|
|
||||||
url: z.string(),
|
|
||||||
host: z.string(),
|
|
||||||
token: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { authentikServerSpecSchema, authentikServerInitSecretSchema, authentikServerSecretSchema };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { authentikServerSpecSchema } from './authentik-server.schemas.ts';
|
|
||||||
import { AuthentikServerController } from './authentik-server.controller.ts';
|
|
||||||
|
|
||||||
const authentikServerDefinition = createCustomResourceDefinition({
|
|
||||||
group: GROUP,
|
|
||||||
version: 'v1',
|
|
||||||
kind: 'AuthentikServer',
|
|
||||||
names: {
|
|
||||||
plural: 'authentikservers',
|
|
||||||
singular: 'authentikserver',
|
|
||||||
},
|
|
||||||
spec: authentikServerSpecSchema,
|
|
||||||
create: (options) => new AuthentikServerController(options),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { authentikServerDefinition };
|
|
||||||
@@ -1,21 +1,7 @@
|
|||||||
import { authentikClientDefinition } from './authentik-client/authentik-client.ts';
|
import { authentikClientDefinition } from './authentik-client/authentik-client.ts';
|
||||||
import { authentikServerDefinition } from './authentik-server/authentik-server.ts';
|
|
||||||
import { environmentDefinition } from './environment/environment.ts';
|
|
||||||
import { generateSecretDefinition } from './generate-secret/generate-secret.ts';
|
import { generateSecretDefinition } from './generate-secret/generate-secret.ts';
|
||||||
import { httpServiceDefinition } from './http-service/http-service.ts';
|
|
||||||
import { postgresClusterDefinition } from './postgres-cluster/postgres-cluster.ts';
|
|
||||||
import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts';
|
import { postgresDatabaseDefinition } from './postgres-database/postgres-database.ts';
|
||||||
import { redisServerDefinition } from './redis-server/redis-server.ts';
|
|
||||||
|
|
||||||
const customResources = [
|
const customResources = [postgresDatabaseDefinition, authentikClientDefinition, generateSecretDefinition];
|
||||||
postgresDatabaseDefinition,
|
|
||||||
authentikClientDefinition,
|
|
||||||
generateSecretDefinition,
|
|
||||||
environmentDefinition,
|
|
||||||
postgresClusterDefinition,
|
|
||||||
authentikServerDefinition,
|
|
||||||
httpServiceDefinition,
|
|
||||||
redisServerDefinition,
|
|
||||||
];
|
|
||||||
|
|
||||||
export { customResources };
|
export { customResources };
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
import { CertificateInstance } from '../../instances/certificate.ts';
|
|
||||||
import { CustomDefinitionInstance } from '../../instances/custom-resource-definition.ts';
|
|
||||||
import { NamespaceInstance } from '../../instances/namespace.ts';
|
|
||||||
import {
|
|
||||||
CustomResource,
|
|
||||||
type CustomResourceOptions,
|
|
||||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import { GatewayInstance } from '../../instances/gateway.ts';
|
|
||||||
import { PostgresClusterInstance } from '../../instances/postgres-cluster.ts';
|
|
||||||
import { API_VERSION } from '../../utils/consts.ts';
|
|
||||||
import { AuthentikServerInstance } from '../../instances/authentik-server.ts';
|
|
||||||
import { StorageClassInstance } from '../../instances/storageclass.ts';
|
|
||||||
import { PROVISIONER } from '../../storage-provider/storage-provider.ts';
|
|
||||||
import { RedisServerInstance } from '../../instances/redis-server.ts';
|
|
||||||
import { NamespaceService } from '../../bootstrap/namespaces/namespaces.ts';
|
|
||||||
|
|
||||||
import type { environmentSpecSchema } from './environment.schemas.ts';
|
|
||||||
|
|
||||||
class EnvironmentController extends CustomResource<typeof environmentSpecSchema> {
|
|
||||||
#namespace: NamespaceInstance;
|
|
||||||
#certificateCrd: CustomDefinitionInstance;
|
|
||||||
#certificate: CertificateInstance;
|
|
||||||
#gatewayCrd: CustomDefinitionInstance;
|
|
||||||
#gateway: GatewayInstance;
|
|
||||||
#storageClass: StorageClassInstance;
|
|
||||||
#postgresCluster: PostgresClusterInstance;
|
|
||||||
#authentikServer: AuthentikServerInstance;
|
|
||||||
#redisServer: RedisServerInstance;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof environmentSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
const namespaceService = this.services.get(NamespaceService);
|
|
||||||
this.#namespace = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
name: this.namespace,
|
|
||||||
},
|
|
||||||
NamespaceInstance,
|
|
||||||
);
|
|
||||||
this.#certificateCrd = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'apiextensions.k8s.io/v1',
|
|
||||||
kind: 'CustomResourceDefinition',
|
|
||||||
name: 'certificates.cert-manager.io',
|
|
||||||
},
|
|
||||||
CustomDefinitionInstance,
|
|
||||||
);
|
|
||||||
this.#certificate = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'cert-manager.io/v1',
|
|
||||||
kind: 'Certificate',
|
|
||||||
name: `${this.name}-tls`,
|
|
||||||
namespace: namespaceService.homelab.name,
|
|
||||||
},
|
|
||||||
CertificateInstance,
|
|
||||||
);
|
|
||||||
this.#gatewayCrd = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'apiextensions.k8s.io/v1',
|
|
||||||
kind: 'CustomResourceDefinition',
|
|
||||||
name: 'gateways.networking.istio.io',
|
|
||||||
},
|
|
||||||
CustomDefinitionInstance,
|
|
||||||
);
|
|
||||||
this.#gateway = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'networking.istio.io/v1',
|
|
||||||
kind: 'Gateway',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
GatewayInstance,
|
|
||||||
);
|
|
||||||
this.#storageClass = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'storage.k8s.io/v1',
|
|
||||||
kind: 'StorageClass',
|
|
||||||
name: `${this.name}-retain`,
|
|
||||||
},
|
|
||||||
StorageClassInstance,
|
|
||||||
);
|
|
||||||
this.#postgresCluster = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'PostgresCluster',
|
|
||||||
name: `${this.name}-postgres-cluster`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
PostgresClusterInstance,
|
|
||||||
);
|
|
||||||
this.#authentikServer = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'AuthentikServer',
|
|
||||||
name: `${this.name}-authentik-server`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
AuthentikServerInstance,
|
|
||||||
);
|
|
||||||
this.#redisServer = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'RedisServer',
|
|
||||||
name: `${this.name}-redis-server`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
RedisServerInstance,
|
|
||||||
);
|
|
||||||
this.#gatewayCrd.on('changed', this.queueReconcile);
|
|
||||||
this.#gateway.on('changed', this.queueReconcile);
|
|
||||||
this.#certificateCrd.on('changed', this.queueReconcile);
|
|
||||||
this.#namespace.on('changed', this.queueReconcile);
|
|
||||||
this.#certificate.on('changed', this.queueReconcile);
|
|
||||||
this.#postgresCluster.on('changed', this.queueReconcile);
|
|
||||||
this.#authentikServer.on('changed', this.queueReconcile);
|
|
||||||
this.#storageClass.on('changed', this.queueReconcile);
|
|
||||||
this.#redisServer.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
if (!this.exists || this.metadata?.deletionTimestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.#namespace.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
labels: {
|
|
||||||
'istio-injection': 'enabled',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (this.#certificateCrd.ready) {
|
|
||||||
await this.#certificate.ensure({
|
|
||||||
spec: {
|
|
||||||
secretName: `${this.name}-tls`,
|
|
||||||
issuerRef: {
|
|
||||||
name: this.spec.tls.issuer,
|
|
||||||
kind: 'ClusterIssuer',
|
|
||||||
},
|
|
||||||
dnsNames: [`*.${this.spec.domain}`],
|
|
||||||
privateKey: {
|
|
||||||
rotationPolicy: 'Always',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.#gatewayCrd.ready) {
|
|
||||||
await this.#gateway.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
selector: {
|
|
||||||
istio: 'homelab-istio-gateway',
|
|
||||||
},
|
|
||||||
servers: [
|
|
||||||
{
|
|
||||||
hosts: [`*.${this.spec.domain}`],
|
|
||||||
port: {
|
|
||||||
name: 'http',
|
|
||||||
number: 80,
|
|
||||||
protocol: 'HTTP',
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
httpsRedirect: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hosts: [`*.${this.spec.domain}`],
|
|
||||||
port: {
|
|
||||||
name: 'https',
|
|
||||||
number: 443,
|
|
||||||
protocol: 'HTTPS',
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
mode: 'SIMPLE',
|
|
||||||
credentialName: `${this.name}-tls`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#storageClass.ensure({
|
|
||||||
provisioner: PROVISIONER,
|
|
||||||
parameters: {
|
|
||||||
storageLocation: this.spec.storage?.location || `/data/volumes/${this.name}`,
|
|
||||||
reclaimPolicy: 'Retain',
|
|
||||||
allowVolumeExpansion: 'true',
|
|
||||||
volumeBindingMode: 'Immediate',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#postgresCluster.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
environment: this.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#authentikServer.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
environment: `${this.namespace}/${this.name}`,
|
|
||||||
subdomain: 'authentik',
|
|
||||||
postgresCluster: `${this.name}-postgres-cluster`,
|
|
||||||
redisServer: `${this.name}-redis-server`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#redisServer.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { EnvironmentController };
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const environmentSpecSchema = z.object({
|
|
||||||
domain: z.string(),
|
|
||||||
tls: z.object({
|
|
||||||
issuer: z.string(),
|
|
||||||
}),
|
|
||||||
storage: z
|
|
||||||
.object({
|
|
||||||
location: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type EnvironmentSpec = z.infer<typeof environmentSpecSchema>;
|
|
||||||
|
|
||||||
export { environmentSpecSchema, type EnvironmentSpec };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { EnvironmentController } from './environment.controller.ts';
|
|
||||||
import { environmentSpecSchema } from './environment.schemas.ts';
|
|
||||||
|
|
||||||
const environmentDefinition = createCustomResourceDefinition({
|
|
||||||
group: GROUP,
|
|
||||||
version: 'v1',
|
|
||||||
kind: 'Environment',
|
|
||||||
names: {
|
|
||||||
plural: 'environments',
|
|
||||||
singular: 'environment',
|
|
||||||
},
|
|
||||||
spec: environmentSpecSchema,
|
|
||||||
create: (options) => new EnvironmentController(options),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { environmentDefinition };
|
|
||||||
@@ -37,8 +37,8 @@ class GenerateSecretResource extends CustomResource<typeof generateSecretSpecSch
|
|||||||
const current = decodeSecret(this.#secretResource.data) || {};
|
const current = decodeSecret(this.#secretResource.data) || {};
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
...secrets,
|
|
||||||
...current,
|
...current,
|
||||||
|
...secrets,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isDeepSubset(current, expected)) {
|
if (!isDeepSubset(current, expected)) {
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import { DestinationRuleInstance } from '../../instances/destination-rule.ts';
|
|
||||||
import { VirtualServiceInstance } from '../../instances/virtual-service.ts';
|
|
||||||
import {
|
|
||||||
CustomResource,
|
|
||||||
type CustomResourceObject,
|
|
||||||
type CustomResourceOptions,
|
|
||||||
} from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceReference, ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
import { API_VERSION } from '../../utils/consts.ts';
|
|
||||||
import { getWithNamespace } from '../../utils/naming.ts';
|
|
||||||
import { environmentSpecSchema } from '../environment/environment.schemas.ts';
|
|
||||||
|
|
||||||
import { httpServiceSpecSchema } from './http-service.schemas.ts';
|
|
||||||
|
|
||||||
class HttpServiceController extends CustomResource<typeof httpServiceSpecSchema> {
|
|
||||||
#environment: ResourceReference<CustomResourceObject<typeof environmentSpecSchema>>;
|
|
||||||
#virtualService: VirtualServiceInstance;
|
|
||||||
#destinationRule: DestinationRuleInstance;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof httpServiceSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
this.#environment = new ResourceReference();
|
|
||||||
this.#virtualService = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'networking.istio.io/v1beta1',
|
|
||||||
kind: 'VirtualService',
|
|
||||||
name: `${this.name}-virtual-service`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
VirtualServiceInstance,
|
|
||||||
);
|
|
||||||
this.#destinationRule = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'networking.istio.io/v1beta1',
|
|
||||||
kind: 'DestinationRule',
|
|
||||||
name: `${this.name}-destination-rule`,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
DestinationRuleInstance,
|
|
||||||
);
|
|
||||||
this.#destinationRule.on('changed', this.queueReconcile);
|
|
||||||
this.#virtualService.on('changed', this.queueReconcile);
|
|
||||||
this.#environment.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
if (!this.exists || this.metadata?.deletionTimestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
const environmentNames = getWithNamespace(this.spec.environment, this.namespace);
|
|
||||||
this.#environment.current = resourceService.get({
|
|
||||||
apiVersion: API_VERSION,
|
|
||||||
kind: 'Environment',
|
|
||||||
name: environmentNames.name,
|
|
||||||
namespace: environmentNames.namespace,
|
|
||||||
});
|
|
||||||
const environment = this.#environment.current;
|
|
||||||
if (!environment?.exists) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.#virtualService.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
hosts: [`${this.spec.subdomain}.${environment.spec?.domain}`],
|
|
||||||
gateways: [`${this.#environment.current.namespace}/${this.#environment.current.name}`],
|
|
||||||
http: [
|
|
||||||
{
|
|
||||||
route: [
|
|
||||||
{
|
|
||||||
destination: {
|
|
||||||
host: this.spec.destination.host,
|
|
||||||
port: this.spec.destination.port,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#destinationRule.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
host: this.spec.destination.host,
|
|
||||||
trafficPolicy: {
|
|
||||||
tls: {
|
|
||||||
mode: 'DISABLE',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { HttpServiceController };
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const httpServiceSpecSchema = z.object({
|
|
||||||
environment: z.string(),
|
|
||||||
subdomain: z.string(),
|
|
||||||
destination: z.object({
|
|
||||||
host: z.string(),
|
|
||||||
port: z
|
|
||||||
.object({
|
|
||||||
number: z.number().optional(),
|
|
||||||
protocol: z.enum(['http', 'https']).optional(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { httpServiceSpecSchema };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { HttpServiceController } from './http-service.controller.ts';
|
|
||||||
import { httpServiceSpecSchema } from './http-service.schemas.ts';
|
|
||||||
|
|
||||||
const httpServiceDefinition = createCustomResourceDefinition({
|
|
||||||
group: GROUP,
|
|
||||||
version: 'v1',
|
|
||||||
kind: 'HttpService',
|
|
||||||
names: {
|
|
||||||
plural: 'httpservices',
|
|
||||||
singular: 'httpservice',
|
|
||||||
},
|
|
||||||
spec: httpServiceSpecSchema,
|
|
||||||
create: (options) => new HttpServiceController(options),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { httpServiceDefinition };
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const postgresDatabaseSpecSchema = z.object({
|
const postgresDatabaseSpecSchema = z.object({
|
||||||
cluster: z.string(),
|
secretRef: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export { postgresDatabaseSpecSchema };
|
const postgresDatabaseSecretSchema = z.object({
|
||||||
|
host: z.string(),
|
||||||
|
port: z.string(),
|
||||||
|
user: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const postgresDatabaseConnectionSecretSchema = z.object({
|
||||||
|
host: z.string(),
|
||||||
|
port: z.string(),
|
||||||
|
user: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
database: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export { postgresDatabaseSpecSchema, postgresDatabaseSecretSchema, postgresDatabaseConnectionSecretSchema };
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
import type { V1Secret } from '@kubernetes/client-node';
|
import type { V1Secret } from '@kubernetes/client-node';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,31 +12,42 @@ import { Resource, ResourceService } from '../../services/resources/resources.ts
|
|||||||
import { getWithNamespace } from '../../utils/naming.ts';
|
import { getWithNamespace } from '../../utils/naming.ts';
|
||||||
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
import { decodeSecret, encodeSecret } from '../../utils/secrets.ts';
|
||||||
import { isDeepSubset } from '../../utils/objects.ts';
|
import { isDeepSubset } from '../../utils/objects.ts';
|
||||||
import { postgresClusterSecretSchema } from '../postgres-cluster/postgres-cluster.schemas.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 SECRET_READY_CONDITION = 'Secret';
|
||||||
const DATABASE_READY_CONDITION = 'Database';
|
const DATABASE_READY_CONDITION = 'Database';
|
||||||
|
|
||||||
|
const secretDataSchema = z.object({
|
||||||
|
host: z.string(),
|
||||||
|
port: z.string().optional(),
|
||||||
|
database: z.string(),
|
||||||
|
user: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpecSchema> {
|
class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpecSchema> {
|
||||||
#clusterSecret: ResourceReference<V1Secret>;
|
#serverSecret: ResourceReference<V1Secret>;
|
||||||
#databaseSecret: Resource<V1Secret>;
|
#databaseSecret: Resource<V1Secret>;
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof postgresDatabaseSpecSchema>) {
|
constructor(options: CustomResourceOptions<typeof postgresDatabaseSpecSchema>) {
|
||||||
super(options);
|
super(options);
|
||||||
this.#clusterSecret = new ResourceReference();
|
this.#serverSecret = new ResourceReference();
|
||||||
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
const resourceService = this.services.get(ResourceService);
|
||||||
this.#databaseSecret = resourceService.get({
|
this.#databaseSecret = resourceService.get({
|
||||||
apiVersion: 'v1',
|
apiVersion: 'v1',
|
||||||
kind: 'Secret',
|
kind: 'Secret',
|
||||||
name: `${this.name}-postgres-database`,
|
name: `${this.name}-connection`,
|
||||||
namespace: this.namespace,
|
namespace: this.namespace,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#updateSecret();
|
this.#updateSecret();
|
||||||
this.#clusterSecret.on('changed', this.queueReconcile);
|
this.#serverSecret.on('changed', this.queueReconcile);
|
||||||
}
|
}
|
||||||
|
|
||||||
get #dbName() {
|
get #dbName() {
|
||||||
@@ -48,17 +60,17 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
|
|
||||||
#updateSecret = () => {
|
#updateSecret = () => {
|
||||||
const resourceService = this.services.get(ResourceService);
|
const resourceService = this.services.get(ResourceService);
|
||||||
const secretNames = getWithNamespace(this.spec.cluster, this.namespace);
|
const secretNames = getWithNamespace(this.spec.secretRef, this.namespace);
|
||||||
this.#clusterSecret.current = resourceService.get({
|
this.#serverSecret.current = resourceService.get({
|
||||||
apiVersion: 'v1',
|
apiVersion: 'v1',
|
||||||
kind: 'Secret',
|
kind: 'Secret',
|
||||||
name: `${secretNames.name}-postgres-cluster`,
|
name: secretNames.name,
|
||||||
namespace: secretNames.namespace,
|
namespace: secretNames.namespace,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
#reconcileSecret = async (): Promise<SubresourceResult> => {
|
#reconcileSecret = async (): Promise<SubresourceResult> => {
|
||||||
const serverSecret = this.#clusterSecret.current;
|
const serverSecret = this.#serverSecret.current;
|
||||||
const databaseSecret = this.#databaseSecret;
|
const databaseSecret = this.#databaseSecret;
|
||||||
|
|
||||||
if (!serverSecret?.exists || !serverSecret.data) {
|
if (!serverSecret?.exists || !serverSecret.data) {
|
||||||
@@ -68,7 +80,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
reason: 'MissingConnectionSecret',
|
reason: 'MissingConnectionSecret',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const serverSecretData = postgresClusterSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
const serverSecretData = postgresDatabaseSecretSchema.safeParse(decodeSecret(serverSecret.data));
|
||||||
if (!serverSecretData.success || !serverSecretData.data) {
|
if (!serverSecretData.success || !serverSecretData.data) {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -76,14 +88,13 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
reason: 'SecretMissing',
|
reason: 'SecretMissing',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const databaseSecretData = postgresClusterSecretSchema.safeParse(decodeSecret(databaseSecret.data));
|
const databaseSecretData = postgresDatabaseConnectionSecretSchema.safeParse(decodeSecret(databaseSecret.data));
|
||||||
const expectedSecret = {
|
const expectedSecret = {
|
||||||
password: crypto.randomUUID(),
|
password: crypto.randomUUID(),
|
||||||
host: serverSecretData.data.host,
|
host: serverSecretData.data.host,
|
||||||
port: serverSecretData.data.port,
|
port: serverSecretData.data.port,
|
||||||
user: this.#userName,
|
user: this.#userName,
|
||||||
database: this.#dbName,
|
database: this.#dbName,
|
||||||
...databaseSecretData.data,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isDeepSubset(databaseSecretData.data, expectedSecret)) {
|
if (!isDeepSubset(databaseSecretData.data, expectedSecret)) {
|
||||||
@@ -103,8 +114,8 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
};
|
};
|
||||||
|
|
||||||
#reconcileDatabase = async (): Promise<SubresourceResult> => {
|
#reconcileDatabase = async (): Promise<SubresourceResult> => {
|
||||||
const clusterSecret = this.#clusterSecret.current;
|
const connectionSecret = this.#serverSecret.current;
|
||||||
if (!clusterSecret?.exists || !clusterSecret.data) {
|
if (!connectionSecret?.exists || !connectionSecret.data) {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
failed: true,
|
failed: true,
|
||||||
@@ -112,7 +123,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionSecretData = postgresClusterSecretSchema.safeParse(decodeSecret(clusterSecret.data));
|
const connectionSecretData = postgresDatabaseSecretSchema.safeParse(decodeSecret(connectionSecret.data));
|
||||||
if (!connectionSecretData.success || !connectionSecretData.data) {
|
if (!connectionSecretData.success || !connectionSecretData.data) {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -121,7 +132,7 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretData = postgresClusterSecretSchema.safeParse(decodeSecret(this.#databaseSecret.data));
|
const secretData = postgresDatabaseConnectionSecretSchema.safeParse(decodeSecret(this.#serverSecret.current?.data));
|
||||||
if (!secretData.success || !secretData.data) {
|
if (!secretData.success || !secretData.data) {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -134,15 +145,14 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
const database = postgresService.get({
|
const database = postgresService.get({
|
||||||
...connectionSecretData.data,
|
...connectionSecretData.data,
|
||||||
port: connectionSecretData.data.port ? Number(connectionSecretData.data.port) : 5432,
|
port: connectionSecretData.data.port ? Number(connectionSecretData.data.port) : 5432,
|
||||||
database: connectionSecretData.data.database,
|
|
||||||
});
|
});
|
||||||
await database.upsertRole({
|
await database.upsertRole({
|
||||||
name: secretData.data.username,
|
name: secretData.data.user,
|
||||||
password: secretData.data.password,
|
password: secretData.data.password,
|
||||||
});
|
});
|
||||||
await database.upsertDatabase({
|
await database.upsertDatabase({
|
||||||
name: secretData.data.database,
|
name: secretData.data.database,
|
||||||
owner: secretData.data.username,
|
owner: secretData.data.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -156,8 +166,8 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
}
|
}
|
||||||
this.#updateSecret();
|
this.#updateSecret();
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
this.reconcileSubresource(DATABASE_READY_CONDITION, this.#reconcileDatabase),
|
await this.reconcileSubresource(DATABASE_READY_CONDITION, this.#reconcileDatabase),
|
||||||
this.reconcileSubresource(SECRET_READY_CONDITION, this.#reconcileSecret),
|
await this.reconcileSubresource(SECRET_READY_CONDITION, this.#reconcileSecret),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const secretReady = this.conditions.get(SECRET_READY_CONDITION)?.status === 'True';
|
const secretReady = this.conditions.get(SECRET_READY_CONDITION)?.status === 'True';
|
||||||
@@ -168,4 +178,4 @@ class PostgresDatabaseResource extends CustomResource<typeof postgresDatabaseSpe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PostgresDatabaseResource };
|
export { PostgresDatabaseResource, secretDataSchema as postgresDatabaseSecretSchema };
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { DeploymentInstance } from '../../instances/deployment.ts';
|
|
||||||
import { ServiceInstance } from '../../instances/service.ts';
|
|
||||||
import { CustomResource } from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import type { CustomResourceOptions } from '../../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceService } from '../../services/resources/resources.ts';
|
|
||||||
|
|
||||||
import type { redisServerSpecSchema } from './redis-server.schemas.ts';
|
|
||||||
|
|
||||||
class RedisServerController extends CustomResource<typeof redisServerSpecSchema> {
|
|
||||||
#deployment: DeploymentInstance;
|
|
||||||
#service: ServiceInstance;
|
|
||||||
|
|
||||||
constructor(options: CustomResourceOptions<typeof redisServerSpecSchema>) {
|
|
||||||
super(options);
|
|
||||||
const resourceService = this.services.get(ResourceService);
|
|
||||||
this.#deployment = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'apps/v1',
|
|
||||||
kind: 'Deployment',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
DeploymentInstance,
|
|
||||||
);
|
|
||||||
this.#service = resourceService.getInstance(
|
|
||||||
{
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Service',
|
|
||||||
name: this.name,
|
|
||||||
namespace: this.namespace,
|
|
||||||
},
|
|
||||||
ServiceInstance,
|
|
||||||
);
|
|
||||||
this.#deployment.on('changed', this.queueReconcile);
|
|
||||||
this.#service.on('changed', this.queueReconcile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reconcile = async () => {
|
|
||||||
await this.#deployment.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
replicas: 1,
|
|
||||||
selector: {
|
|
||||||
matchLabels: {
|
|
||||||
app: this.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
metadata: {
|
|
||||||
labels: {
|
|
||||||
app: this.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
containers: [
|
|
||||||
{
|
|
||||||
name: this.name,
|
|
||||||
image: 'redis:latest',
|
|
||||||
ports: [{ containerPort: 6379 }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.#service.ensure({
|
|
||||||
metadata: {
|
|
||||||
ownerReferences: [this.ref],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
selector: {
|
|
||||||
app: this.name,
|
|
||||||
},
|
|
||||||
ports: [{ port: 6379, targetPort: 6379 }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RedisServerController };
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const redisServerSpecSchema = z.object({});
|
|
||||||
|
|
||||||
export { redisServerSpecSchema };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createCustomResourceDefinition } from '../../services/custom-resources/custom-resources.ts';
|
|
||||||
import { GROUP } from '../../utils/consts.ts';
|
|
||||||
|
|
||||||
import { RedisServerController } from './redis-server.controller.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 RedisServerController(options),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { redisServerDefinition };
|
|
||||||
127
src/index.ts
127
src/index.ts
@@ -1,78 +1,37 @@
|
|||||||
import { BootstrapService } from './bootstrap/bootstrap.ts';
|
import 'dotenv/config';
|
||||||
import { customResources } from './custom-resouces/custom-resources.ts';
|
import { ApiException } from '@kubernetes/client-node';
|
||||||
|
|
||||||
|
import { Services } from './utils/service.ts';
|
||||||
import { CustomResourceService } from './services/custom-resources/custom-resources.ts';
|
import { CustomResourceService } from './services/custom-resources/custom-resources.ts';
|
||||||
import { WatcherService } from './services/watchers/watchers.ts';
|
import { WatcherService } from './services/watchers/watchers.ts';
|
||||||
|
import { customResources } from './custom-resouces/custom-resources.ts';
|
||||||
import { StorageProvider } from './storage-provider/storage-provider.ts';
|
import { StorageProvider } from './storage-provider/storage-provider.ts';
|
||||||
import { Services } from './utils/service.ts';
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.log('UNCAUGHT EXCEPTION');
|
||||||
|
if (error instanceof ApiException) {
|
||||||
|
return console.error(error.body);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (error) => {
|
||||||
|
console.log('UNHANDLED REJECTION');
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(error.stack);
|
||||||
|
}
|
||||||
|
if (error instanceof ApiException) {
|
||||||
|
return console.error(error.body);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
const services = new Services();
|
const services = new Services();
|
||||||
|
|
||||||
const watcherService = services.get(WatcherService);
|
const watcherService = services.get(WatcherService);
|
||||||
await watcherService.watchCustomGroup('source.toolkit.fluxcd.io', 'v1', ['helmrepositories', 'gitrepositories']);
|
const storageProvider = services.get(StorageProvider);
|
||||||
await watcherService.watchCustomGroup('helm.toolkit.fluxcd.io', 'v2', ['helmreleases']);
|
await storageProvider.start();
|
||||||
await watcherService.watchCustomGroup('cert-manager.io', 'v1', ['certificates']);
|
|
||||||
await watcherService.watchCustomGroup('networking.k8s.io', 'v1', ['gateways', 'virtualservices']);
|
|
||||||
|
|
||||||
await watcherService
|
|
||||||
.create({
|
|
||||||
path: '/api/v1/namespaces',
|
|
||||||
list: async (k8s) => {
|
|
||||||
return await k8s.api.listNamespace();
|
|
||||||
},
|
|
||||||
verbs: ['add', 'update', 'delete'],
|
|
||||||
transform: (manifest) => ({
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Namespace',
|
|
||||||
...manifest,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
|
|
||||||
await watcherService
|
|
||||||
.create({
|
|
||||||
path: '/api/v1/secrets',
|
|
||||||
list: async (k8s) => {
|
|
||||||
return await k8s.api.listSecretForAllNamespaces();
|
|
||||||
},
|
|
||||||
verbs: ['add', 'update', 'delete'],
|
|
||||||
transform: (manifest) => ({
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Secret',
|
|
||||||
...manifest,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
|
|
||||||
await watcherService
|
|
||||||
.create({
|
|
||||||
path: '/apis/apps/v1/statefulsets',
|
|
||||||
list: async (k8s) => {
|
|
||||||
return await k8s.apps.listStatefulSetForAllNamespaces({});
|
|
||||||
},
|
|
||||||
verbs: ['add', 'update', 'delete'],
|
|
||||||
transform: (manifest) => ({
|
|
||||||
apiVersion: 'apps/v1',
|
|
||||||
kind: 'StatefulSet',
|
|
||||||
...manifest,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
|
|
||||||
await watcherService
|
|
||||||
.create({
|
|
||||||
path: '/apis/apps/v1/deployments',
|
|
||||||
list: async (k8s) => {
|
|
||||||
return await k8s.apps.listDeploymentForAllNamespaces({});
|
|
||||||
},
|
|
||||||
verbs: ['add', 'update', 'delete'],
|
|
||||||
transform: (manifest) => ({
|
|
||||||
apiVersion: 'apps/v1',
|
|
||||||
kind: 'Deployment',
|
|
||||||
...manifest,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
|
|
||||||
await watcherService
|
await watcherService
|
||||||
.create({
|
.create({
|
||||||
path: '/apis/apiextensions.k8s.io/v1/customresourcedefinitions',
|
path: '/apis/apiextensions.k8s.io/v1/customresourcedefinitions',
|
||||||
@@ -89,27 +48,35 @@ await watcherService
|
|||||||
.start();
|
.start();
|
||||||
await watcherService
|
await watcherService
|
||||||
.create({
|
.create({
|
||||||
path: '/apis/storage.k8s.io/v1/storageclasses',
|
path: '/api/v1/secrets',
|
||||||
list: async (k8s) => {
|
list: async (k8s) => {
|
||||||
return await k8s.storageApi.listStorageClass();
|
return await k8s.api.listSecretForAllNamespaces();
|
||||||
},
|
},
|
||||||
verbs: ['add', 'update', 'delete'],
|
verbs: ['add', 'update', 'delete'],
|
||||||
transform: (manifest) => ({
|
transform: (manifest) => ({
|
||||||
apiVersion: 'storage.k8s.io/v1',
|
apiVersion: 'v1',
|
||||||
kind: 'StorageClass',
|
kind: 'Secret',
|
||||||
|
...manifest,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
await watcherService
|
||||||
|
.create({
|
||||||
|
path: '/apis/apps/v1/deployments',
|
||||||
|
list: async (k8s) => {
|
||||||
|
return await k8s.apps.listDeploymentForAllNamespaces({});
|
||||||
|
},
|
||||||
|
verbs: ['add', 'update', 'delete'],
|
||||||
|
transform: (manifest) => ({
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'Deployment',
|
||||||
...manifest,
|
...manifest,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.start();
|
.start();
|
||||||
|
|
||||||
const storageProvider = services.get(StorageProvider);
|
|
||||||
await storageProvider.start();
|
|
||||||
|
|
||||||
const bootstrap = services.get(BootstrapService);
|
|
||||||
await bootstrap.ensure();
|
|
||||||
|
|
||||||
const customResourceService = services.get(CustomResourceService);
|
const customResourceService = services.get(CustomResourceService);
|
||||||
customResourceService.register(...customResources);
|
customResourceService.register(...customResources);
|
||||||
|
|
||||||
await customResourceService.install(true);
|
await customResourceService.install(true);
|
||||||
await customResourceService.watch();
|
// await customResourceService.watch();
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { authentikServerSpecSchema } from '../custom-resouces/authentik-server/authentik-server.schemas.ts';
|
|
||||||
import type { CustomResourceObject } from '../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class AuthentikServerInstance extends ResourceInstance<CustomResourceObject<typeof authentikServerSpecSchema>> {}
|
|
||||||
|
|
||||||
export { AuthentikServerInstance };
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
import type { K8SCertificateV1 } from '../__generated__/resources/K8SCertificateV1.ts';
|
|
||||||
|
|
||||||
class CertificateInstance extends ResourceInstance<KubernetesObject & K8SCertificateV1> {}
|
|
||||||
|
|
||||||
export { CertificateInstance };
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import type { K8SClusterIssuerV1 } from '../__generated__/resources/K8SClusterIssuerV1.ts';
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class ClusterIssuerInstance extends ResourceInstance<KubernetesObject & K8SClusterIssuerV1> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ClusterIssuerInstance };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { V1CustomResourceDefinition } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class CustomDefinitionInstance extends ResourceInstance<V1CustomResourceDefinition> {}
|
|
||||||
|
|
||||||
export { CustomDefinitionInstance };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { V1Deployment } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.ts';
|
|
||||||
|
|
||||||
class DeploymentInstance extends ResourceInstance<V1Deployment> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists && this.status?.readyReplicas === this.status?.replicas;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { DeploymentInstance };
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
import type { K8SDestinationRuleV1 } from '../__generated__/resources/K8SDestinationRuleV1.ts';
|
|
||||||
|
|
||||||
class DestinationRuleInstance extends ResourceInstance<KubernetesObject & K8SDestinationRuleV1> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { DestinationRuleInstance };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { environmentSpecSchema } from '../custom-resouces/environment/environment.schemas.ts';
|
|
||||||
import type { CustomResourceObject } from '../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class EnvironmentInstance extends ResourceInstance<CustomResourceObject<typeof environmentSpecSchema>> {}
|
|
||||||
|
|
||||||
export { EnvironmentInstance };
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
import type { K8SGatewayV1 } from '../__generated__/resources/K8SGatewayV1.ts';
|
|
||||||
|
|
||||||
class GatewayInstance extends ResourceInstance<KubernetesObject & K8SGatewayV1> {}
|
|
||||||
|
|
||||||
export { GatewayInstance };
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.ts';
|
|
||||||
import type { K8SGitRepositoryV1 } from '../__generated__/resources/K8SGitRepositoryV1.ts';
|
|
||||||
|
|
||||||
class GitRepoInstance extends ResourceInstance<KubernetesObject & K8SGitRepositoryV1> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { GitRepoInstance };
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.ts';
|
|
||||||
import type { K8SHelmReleaseV2 } from '../__generated__/resources/K8SHelmReleaseV2.ts';
|
|
||||||
|
|
||||||
class HelmReleaseInstance extends ResourceInstance<KubernetesObject & K8SHelmReleaseV2> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { HelmReleaseInstance };
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.ts';
|
|
||||||
import type { K8SHelmRepositoryV1 } from '../__generated__/resources/K8SHelmRepositoryV1.ts';
|
|
||||||
|
|
||||||
class HelmRepoInstance extends ResourceInstance<KubernetesObject & K8SHelmRepositoryV1> {
|
|
||||||
public get ready() {
|
|
||||||
if (!this.exists) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const condition = this.getCondition('Ready');
|
|
||||||
return condition?.status === 'True';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { HelmRepoInstance };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { httpServiceSpecSchema } from '../custom-resouces/http-service/http-service.schemas.ts';
|
|
||||||
import type { CustomResourceObject } from '../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class HttpServiceInstance extends ResourceInstance<CustomResourceObject<typeof httpServiceSpecSchema>> {}
|
|
||||||
|
|
||||||
export { HttpServiceInstance };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { V1Namespace } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.ts';
|
|
||||||
|
|
||||||
class NamespaceInstance extends ResourceInstance<V1Namespace> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { NamespaceInstance };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { postgresClusterSpecSchema } from '../custom-resouces/postgres-cluster/postgres-cluster.schemas.ts';
|
|
||||||
import type { CustomResourceObject } from '../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class PostgresClusterInstance extends ResourceInstance<CustomResourceObject<typeof postgresClusterSpecSchema>> {}
|
|
||||||
|
|
||||||
export { PostgresClusterInstance };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { CustomResourceObject } from '../services/custom-resources/custom-resources.custom-resource.ts';
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
import type { redisServerSpecSchema } from '../custom-resouces/redis-server/redis-server.schemas.ts';
|
|
||||||
|
|
||||||
class RedisServerInstance extends ResourceInstance<CustomResourceObject<typeof redisServerSpecSchema>> {}
|
|
||||||
|
|
||||||
export { RedisServerInstance };
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import type { V1Secret } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
import { decodeSecret, encodeSecret } from '../utils/secrets.ts';
|
|
||||||
|
|
||||||
class SecretInstance extends ResourceInstance<V1Secret> {
|
|
||||||
public get values() {
|
|
||||||
return decodeSecret(this.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ensureData = async (values: Record<string, string>) => {
|
|
||||||
await this.ensure({
|
|
||||||
data: encodeSecret(values),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public readonly ready = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { SecretInstance };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { V1Service } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.ts';
|
|
||||||
|
|
||||||
class ServiceInstance extends ResourceInstance<V1Service> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ServiceInstance };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { V1StatefulSet } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class StatefulSetInstance extends ResourceInstance<V1StatefulSet> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists && this.manifest?.status?.readyReplicas === this.manifest?.status?.replicas;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { StatefulSetInstance };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { V1StorageClass } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
|
|
||||||
class StorageClassInstance extends ResourceInstance<V1StorageClass> {}
|
|
||||||
|
|
||||||
export { StorageClassInstance };
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { ResourceInstance } from '../services/resources/resources.instance.ts';
|
|
||||||
import type { K8SVirtualServiceV1 } from '../__generated__/resources/K8SVirtualServiceV1.ts';
|
|
||||||
|
|
||||||
class VirtualServiceInstance extends ResourceInstance<KubernetesObject & K8SVirtualServiceV1> {
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { VirtualServiceInstance };
|
|
||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
CustomObjectsApi,
|
CustomObjectsApi,
|
||||||
EventsV1Api,
|
EventsV1Api,
|
||||||
KubernetesObjectApi,
|
KubernetesObjectApi,
|
||||||
|
ApiException,
|
||||||
AppsV1Api,
|
AppsV1Api,
|
||||||
StorageV1Api,
|
|
||||||
} from '@kubernetes/client-node';
|
} from '@kubernetes/client-node';
|
||||||
|
|
||||||
class K8sService {
|
class K8sService {
|
||||||
@@ -17,7 +17,6 @@ class K8sService {
|
|||||||
#k8sEventsApi: EventsV1Api;
|
#k8sEventsApi: EventsV1Api;
|
||||||
#k8sObjectsApi: KubernetesObjectApi;
|
#k8sObjectsApi: KubernetesObjectApi;
|
||||||
#k8sAppsApi: AppsV1Api;
|
#k8sAppsApi: AppsV1Api;
|
||||||
#k8sStorageApi: StorageV1Api;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.#kc = new KubeConfig();
|
this.#kc = new KubeConfig();
|
||||||
@@ -28,7 +27,6 @@ class K8sService {
|
|||||||
this.#k8sEventsApi = this.#kc.makeApiClient(EventsV1Api);
|
this.#k8sEventsApi = this.#kc.makeApiClient(EventsV1Api);
|
||||||
this.#k8sObjectsApi = this.#kc.makeApiClient(KubernetesObjectApi);
|
this.#k8sObjectsApi = this.#kc.makeApiClient(KubernetesObjectApi);
|
||||||
this.#k8sAppsApi = this.#kc.makeApiClient(AppsV1Api);
|
this.#k8sAppsApi = this.#kc.makeApiClient(AppsV1Api);
|
||||||
this.#k8sStorageApi = this.#kc.makeApiClient(StorageV1Api);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get config() {
|
public get config() {
|
||||||
@@ -58,10 +56,6 @@ class K8sService {
|
|||||||
public get apps() {
|
public get apps() {
|
||||||
return this.#k8sAppsApi;
|
return this.#k8sAppsApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get storageApi() {
|
|
||||||
return this.#k8sStorageApi;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { K8sService };
|
export { K8sService };
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ type PostgresInstanceOptions = {
|
|||||||
services: Services;
|
services: Services;
|
||||||
host: string;
|
host: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
username: string;
|
user: string;
|
||||||
password: string;
|
password: string;
|
||||||
database?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class PostgresInstance {
|
class PostgresInstance {
|
||||||
@@ -21,10 +20,9 @@ class PostgresInstance {
|
|||||||
client: 'pg',
|
client: 'pg',
|
||||||
connection: {
|
connection: {
|
||||||
host: process.env.FORCE_PG_HOST ?? options.host,
|
host: process.env.FORCE_PG_HOST ?? options.host,
|
||||||
user: process.env.FORCE_PG_USER ?? options.username,
|
user: process.env.FORCE_PG_USER ?? options.user,
|
||||||
password: process.env.FORCE_PG_PASSWORD ?? options.password,
|
password: process.env.FORCE_PG_PASSWORD ?? options.password,
|
||||||
port: process.env.FORCE_PG_PORT ? parseInt(process.env.FORCE_PG_PORT) : options.port,
|
port: process.env.FORCE_PG_PORT ? parseInt(process.env.FORCE_PG_PORT) : options.port,
|
||||||
database: options.database,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { KubernetesObject } from '@kubernetes/client-node';
|
|
||||||
|
|
||||||
import { isDeepSubset } from '../../utils/objects.ts';
|
|
||||||
|
|
||||||
import { ResourceReference } from './resources.ref.ts';
|
|
||||||
|
|
||||||
abstract class ResourceInstance<T extends KubernetesObject> extends ResourceReference<T> {
|
|
||||||
public get resource() {
|
|
||||||
if (!this.current) {
|
|
||||||
throw new Error('Instance needs a resource');
|
|
||||||
}
|
|
||||||
return this.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get exists() {
|
|
||||||
return this.resource.exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get manifest() {
|
|
||||||
return this.resource.manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get apiVersion() {
|
|
||||||
return this.resource.apiVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get kind() {
|
|
||||||
return this.resource.kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get name() {
|
|
||||||
return this.resource.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get namespace() {
|
|
||||||
return this.resource.namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get metadata() {
|
|
||||||
return this.resource.metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get spec() {
|
|
||||||
return this.resource.spec;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get data() {
|
|
||||||
return this.resource.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get status() {
|
|
||||||
return this.resource.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public patch = this.resource.patch;
|
|
||||||
public reload = this.resource.load;
|
|
||||||
public delete = this.resource.delete;
|
|
||||||
|
|
||||||
public ensure = async (manifest: T) => {
|
|
||||||
if (isDeepSubset(this.manifest, manifest)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await this.patch(manifest);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
public get ready() {
|
|
||||||
return this.exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCondition = (
|
|
||||||
condition: string,
|
|
||||||
): T extends { status?: { conditions?: (infer U)[] } } ? U | undefined : undefined => {
|
|
||||||
const status = this.status as ExpectedAny;
|
|
||||||
return status?.conditions?.find((c: ExpectedAny) => c?.type === condition);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ResourceInstance };
|
|
||||||
@@ -163,13 +163,6 @@ class Resource<T extends KubernetesObject = UnknownResource> extends EventEmitte
|
|||||||
return undefined as ExpectedAny;
|
return undefined as ExpectedAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get status(): T extends { status?: infer K } ? K | undefined : never {
|
|
||||||
if (this.manifest && 'status' in this.manifest) {
|
|
||||||
return this.manifest.status as ExpectedAny;
|
|
||||||
}
|
|
||||||
return undefined as ExpectedAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get owners() {
|
public get owners() {
|
||||||
const { services } = this.#options;
|
const { services } = this.#options;
|
||||||
const references = this.metadata?.ownerReferences || [];
|
const references = this.metadata?.ownerReferences || [];
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { KubernetesObject } from '@kubernetes/client-node';
|
|||||||
import type { Services } from '../../utils/service.ts';
|
import type { Services } from '../../utils/service.ts';
|
||||||
|
|
||||||
import { Resource } from './resources.resource.ts';
|
import { Resource } from './resources.resource.ts';
|
||||||
import type { ResourceInstance } from './resources.instance.ts';
|
|
||||||
|
|
||||||
type ResourceGetOptions = {
|
type ResourceGetOptions = {
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
@@ -20,14 +19,6 @@ class ResourceService {
|
|||||||
this.#services = services;
|
this.#services = services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getInstance = <T extends KubernetesObject, I extends ResourceInstance<T>>(
|
|
||||||
options: ResourceGetOptions,
|
|
||||||
instance: new (resource: Resource<T>) => I,
|
|
||||||
) => {
|
|
||||||
const resource = this.get<T>(options);
|
|
||||||
return new instance(resource);
|
|
||||||
};
|
|
||||||
|
|
||||||
public get = <T extends KubernetesObject>(options: ResourceGetOptions) => {
|
public get = <T extends KubernetesObject>(options: ResourceGetOptions) => {
|
||||||
const { apiVersion, kind, name, namespace } = options;
|
const { apiVersion, kind, name, namespace } = options;
|
||||||
let resource = this.#cache.find(
|
let resource = this.#cache.find(
|
||||||
@@ -49,6 +40,5 @@ class ResourceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ResourceInstance } from './resources.instance.ts';
|
|
||||||
export { ResourceReference } from './resources.ref.ts';
|
export { ResourceReference } from './resources.ref.ts';
|
||||||
export { ResourceService, Resource };
|
export { ResourceService, Resource };
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class EnsuredSecret<T extends ZodObject> {
|
|||||||
return this.#options.namespace;
|
return this.#options.namespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get resource() {
|
public get resouce() {
|
||||||
return this.#resource;
|
return this.#resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class EnsuredSecret<T extends ZodObject> {
|
|||||||
if (deepEqual(patched, this.value)) {
|
if (deepEqual(patched, this.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.resource.patch({
|
await this.resouce.patch({
|
||||||
data: patched,
|
data: patched,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { V1Secret } from '@kubernetes/client-node';
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import deepEqual from 'deep-equal';
|
|
||||||
|
|
||||||
import { ResourceReference, ResourceService } from '../resources/resources.ts';
|
|
||||||
import type { Services } from '../../utils/service.ts';
|
|
||||||
import { getWithNamespace } from '../../utils/naming.ts';
|
|
||||||
import { decodeSecret } from '../../utils/secrets.ts';
|
|
||||||
|
|
||||||
const valueReferenceInfoSchema = z.object({
|
|
||||||
value: z.string().optional(),
|
|
||||||
secretRef: z.string().optional(),
|
|
||||||
key: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type ValueReferenceInfo = z.infer<typeof valueReferenceInfoSchema>;
|
|
||||||
|
|
||||||
type ValueRefOptions = {
|
|
||||||
services: Services;
|
|
||||||
namespace: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ValueReferenceEvents = {
|
|
||||||
changed: () => void;
|
|
||||||
};
|
|
||||||
class ValueReference extends EventEmitter<ValueReferenceEvents> {
|
|
||||||
#options: ValueRefOptions;
|
|
||||||
#ref?: ValueReferenceInfo;
|
|
||||||
#resource: ResourceReference;
|
|
||||||
|
|
||||||
constructor(options: ValueRefOptions) {
|
|
||||||
super();
|
|
||||||
this.#options = options;
|
|
||||||
this.#resource = new ResourceReference<V1Secret>();
|
|
||||||
this.#resource.on('changed', this.#handleChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get ref() {
|
|
||||||
return this.#ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set ref(ref: ValueReferenceInfo | undefined) {
|
|
||||||
if (deepEqual(this.#ref, ref)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ref?.secretRef && ref.key) {
|
|
||||||
const { services, namespace } = this.#options;
|
|
||||||
const resourceService = services.get(ResourceService);
|
|
||||||
const refNames = getWithNamespace(ref.secretRef, namespace);
|
|
||||||
this.#resource.current = resourceService.get({
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Secret',
|
|
||||||
name: refNames.name,
|
|
||||||
namespace: refNames.namespace,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.#resource.current = undefined;
|
|
||||||
}
|
|
||||||
this.#ref = ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get value() {
|
|
||||||
console.log('get', this.#ref);
|
|
||||||
if (!this.#ref) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (this.#ref.value) {
|
|
||||||
return this.#ref.value;
|
|
||||||
}
|
|
||||||
if (this.#resource.current && this.#ref.key) {
|
|
||||||
const decoded = decodeSecret(this.#resource.current.data);
|
|
||||||
return decoded?.[this.#ref.key];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
#handleChange = () => {
|
|
||||||
this.emit('changed');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ValueReference, valueReferenceInfoSchema, type ValueReferenceInfo };
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { Services } from '../../utils/service.ts';
|
|
||||||
|
|
||||||
import { ValueReference } from './value-reference.instance.ts';
|
|
||||||
|
|
||||||
class ValueReferenceService {
|
|
||||||
#services: Services;
|
|
||||||
|
|
||||||
constructor(services: Services) {
|
|
||||||
this.#services = services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get = (namespace: string) => {
|
|
||||||
return new ValueReference({
|
|
||||||
namespace,
|
|
||||||
services: this.#services,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './value-reference.instance.ts';
|
|
||||||
export { ValueReferenceService };
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { V1PersistentVolume, type V1PersistentVolumeClaim, CoreV1Event, V1StorageClass } from '@kubernetes/client-node';
|
import { mkdir } from 'fs/promises';
|
||||||
|
|
||||||
|
import { V1PersistentVolume, type V1PersistentVolumeClaim } from '@kubernetes/client-node';
|
||||||
|
|
||||||
import { Watcher, WatcherService } from '../services/watchers/watchers.ts';
|
import { Watcher, WatcherService } from '../services/watchers/watchers.ts';
|
||||||
import type { Services } from '../utils/service.ts';
|
import type { Services } from '../utils/service.ts';
|
||||||
import { ResourceService, type Resource } from '../services/resources/resources.ts';
|
import { ResourceService, type Resource } from '../services/resources/resources.ts';
|
||||||
|
|
||||||
const PROVISIONER = 'homelab-operator-local-path';
|
const PROVISIONER = 'reuse-local-path-provisioner';
|
||||||
|
|
||||||
class StorageProvider {
|
class StorageProvider {
|
||||||
#watcher: Watcher<V1PersistentVolumeClaim>;
|
#watcher: Watcher<V1PersistentVolumeClaim>;
|
||||||
@@ -30,128 +32,46 @@ class StorageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#handleChange = async (pvc: Resource<V1PersistentVolumeClaim>) => {
|
#handleChange = async (pvc: Resource<V1PersistentVolumeClaim>) => {
|
||||||
try {
|
if (pvc.metadata?.annotations?.['volume.kubernetes.io/storage-provisioner'] !== PROVISIONER) {
|
||||||
if (!pvc.exists || pvc.metadata?.deletionTimestamp) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageClassName = pvc.spec?.storageClassName;
|
|
||||||
if (!storageClassName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const resourceService = this.#services.get(ResourceService);
|
|
||||||
const storageClass = resourceService.get<V1StorageClass>({
|
|
||||||
apiVersion: 'storage.k8s.io/v1',
|
|
||||||
kind: 'StorageClass',
|
|
||||||
name: storageClassName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storageClass.exists || storageClass.manifest?.provisioner !== PROVISIONER) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pvc.status?.phase === 'Pending' && !pvc.spec?.volumeName) {
|
|
||||||
await this.#provisionVolume(pvc, storageClass);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error handling PVC ${pvc.namespace}/${pvc.name}:`, error);
|
|
||||||
await this.#createEvent(pvc, 'Warning', 'ProvisioningFailed', `Failed to provision volume: ${error}`);
|
|
||||||
}
|
}
|
||||||
};
|
const target = `/data/volumes/${pvc.namespace}/${pvc.name}`;
|
||||||
|
|
||||||
#provisionVolume = async (pvc: Resource<V1PersistentVolumeClaim>, storageClass: Resource<V1StorageClass>) => {
|
|
||||||
const pvName = `pv-${pvc.namespace}-${pvc.name}`;
|
|
||||||
const storageLocation = storageClass.manifest?.parameters?.storageLocation || '/data/volumes';
|
|
||||||
const target = `${storageLocation}/${pvc.namespace}/${pvc.name}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resourceService = this.#services.get(ResourceService);
|
await mkdir(target, { recursive: true });
|
||||||
const pv = resourceService.get<V1PersistentVolume>({
|
} catch (err) {
|
||||||
apiVersion: 'v1',
|
console.error(err);
|
||||||
kind: 'PersistentVolume',
|
|
||||||
name: pvName,
|
|
||||||
});
|
|
||||||
|
|
||||||
await pv.patch({
|
|
||||||
metadata: {
|
|
||||||
name: pvName,
|
|
||||||
labels: {
|
|
||||||
provisioner: PROVISIONER,
|
|
||||||
'pvc-namespace': pvc.namespace || 'default',
|
|
||||||
'pvc-name': pvc.name || 'unknown',
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
'pv.kubernetes.io/provisioned-by': PROVISIONER,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
hostPath: {
|
|
||||||
path: target,
|
|
||||||
type: 'DirectoryOrCreate',
|
|
||||||
},
|
|
||||||
capacity: {
|
|
||||||
storage: pvc.spec?.resources?.requests?.storage ?? '1Gi',
|
|
||||||
},
|
|
||||||
persistentVolumeReclaimPolicy: 'Retain',
|
|
||||||
accessModes: pvc.spec?.accessModes ?? ['ReadWriteOnce'],
|
|
||||||
storageClassName: pvc.spec?.storageClassName,
|
|
||||||
claimRef: {
|
|
||||||
uid: pvc.metadata?.uid,
|
|
||||||
resourceVersion: pvc.metadata?.resourceVersion,
|
|
||||||
apiVersion: pvc.apiVersion,
|
|
||||||
kind: 'PersistentVolumeClaim',
|
|
||||||
name: pvc.name,
|
|
||||||
namespace: pvc.namespace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#createEvent(pvc, 'Normal', 'Provisioning', `Successfully provisioned volume ${pvName}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to provision volume for PVC ${pvc.namespace}/${pvc.name}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
const resourceService = this.#services.get(ResourceService);
|
||||||
|
const pv = resourceService.get<V1PersistentVolume>({
|
||||||
#createEvent = async (pvc: Resource<V1PersistentVolumeClaim>, type: string, reason: string, message: string) => {
|
apiVersion: 'v1',
|
||||||
try {
|
kind: 'PersistentVolume',
|
||||||
const resourceService = this.#services.get(ResourceService);
|
name: `${pvc.namespace}-${pvc.name}`,
|
||||||
const event = resourceService.get<CoreV1Event>({
|
});
|
||||||
apiVersion: 'v1',
|
await pv.load();
|
||||||
kind: 'Event',
|
await pv.patch({
|
||||||
name: `${pvc.name}-${Date.now()}`,
|
metadata: {
|
||||||
namespace: pvc.namespace,
|
labels: {
|
||||||
});
|
provisioner: PROVISIONER,
|
||||||
|
|
||||||
if (!pvc.name || !pvc.namespace || !pvc.metadata?.uid) {
|
|
||||||
console.error('Missing required PVC metadata for event creation');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await event.patch({
|
|
||||||
metadata: {
|
|
||||||
namespace: pvc.namespace,
|
|
||||||
},
|
},
|
||||||
involvedObject: {
|
},
|
||||||
|
spec: {
|
||||||
|
hostPath: {
|
||||||
|
path: target,
|
||||||
|
},
|
||||||
|
capacity: {
|
||||||
|
storage: pvc.spec?.resources?.requests?.storage ?? '1Gi',
|
||||||
|
},
|
||||||
|
persistentVolumeReclaimPolicy: 'Retain',
|
||||||
|
accessModes: pvc.spec?.accessModes,
|
||||||
|
claimRef: {
|
||||||
|
uid: pvc.metadata?.uid,
|
||||||
|
resourceVersion: pvc.metadata?.resourceVersion,
|
||||||
apiVersion: pvc.apiVersion,
|
apiVersion: pvc.apiVersion,
|
||||||
kind: 'PersistentVolumeClaim',
|
|
||||||
name: pvc.name,
|
name: pvc.name,
|
||||||
namespace: pvc.namespace,
|
namespace: pvc.namespace,
|
||||||
uid: pvc.metadata.uid,
|
|
||||||
},
|
},
|
||||||
type,
|
},
|
||||||
reason,
|
});
|
||||||
message,
|
|
||||||
source: {
|
|
||||||
component: PROVISIONER,
|
|
||||||
},
|
|
||||||
firstTimestamp: new Date(),
|
|
||||||
lastTimestamp: new Date(),
|
|
||||||
count: 1,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to create event for PVC ${pvc.namespace}/${pvc.name}:`, error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public start = async () => {
|
public start = async () => {
|
||||||
@@ -159,4 +79,4 @@ class StorageProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { StorageProvider, PROVISIONER };
|
export { StorageProvider };
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ const FIELDS = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const NAMESPACE = 'homelab';
|
|
||||||
|
|
||||||
const CONTROLLED_LABEL = {
|
const CONTROLLED_LABEL = {
|
||||||
[`${GROUP}/controlled`]: 'true',
|
[`${GROUP}/controlled`]: 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTROLLED_LABEL_SELECTOR = `${GROUP}/controlled=true`;
|
const CONTROLLED_LABEL_SELECTOR = `${GROUP}/controlled=true`;
|
||||||
|
|
||||||
export { GROUP, FIELDS, CONTROLLED_LABEL, CONTROLLED_LABEL_SELECTOR, API_VERSION, NAMESPACE };
|
export { GROUP, FIELDS, CONTROLLED_LABEL, CONTROLLED_LABEL_SELECTOR, API_VERSION };
|
||||||
|
|||||||
Reference in New Issue
Block a user