diff --git a/.DS_Store b/.DS_Store
index 82727ea..14ddade 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml
deleted file mode 100644
index ee7c601..0000000
--- a/.gitea/workflows/build.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: Build EntKube
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: '10.0.x'
-
- - name: Restore dependencies
- run: dotnet restore EntKube.slnx
-
- - name: Build
- run: dotnet build EntKube.slnx --no-restore --configuration Release
-
- - name: Test
- run: dotnet test EntKube.slnx --no-build --configuration Release
diff --git a/.gitea/workflows/helm.yaml b/.gitea/workflows/helm.yaml
deleted file mode 100644
index 23ab44e..0000000
--- a/.gitea/workflows/helm.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: Package Helm Chart
-
-on:
- push:
- branches: [main]
- paths:
- - 'Charts/**'
- pull_request:
- branches: [main]
- paths:
- - 'Charts/**'
-
-jobs:
- lint:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Install Helm
- uses: azure/setup-helm@v4
- with:
- version: v3.16.0
-
- - name: Lint chart
- run: helm lint Charts/entkube
-
- package:
- runs-on: ubuntu-latest
- needs: lint
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Install Helm
- uses: azure/setup-helm@v4
- with:
- version: v3.16.0
-
- - name: Package chart
- run: helm package Charts/entkube --destination .helm-packages/
-
- - name: Login to Helm OCI registry
- run: echo "${{ secrets.REGISTRY_PASSWORD }}" | helm registry login ${{ vars.REGISTRY_HOST }} --username ${{ vars.REGISTRY_USER }} --password-stdin
-
- - name: Push chart to OCI registry
- run: helm push .helm-packages/entkube-*.tgz oci://${{ vars.REGISTRY_HOST }}/entkube
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 06509dd..0eeb980 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -2,10 +2,10 @@
"version": "0.2.0",
"configurations": [
{
- "name": "Web (BFF)",
+ "name": "EntKube.Web",
"type": "coreclr",
"request": "launch",
- "preLaunchTask": "build-web",
+ "preLaunchTask": "build",
"program": "${workspaceFolder}/src/EntKube.Web/bin/Debug/net10.0/EntKube.Web.dll",
"args": [],
"cwd": "${workspaceFolder}/src/EntKube.Web",
@@ -15,79 +15,8 @@
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "ASPNETCORE_URLS": "http://localhost:5000"
+ "ASPNETCORE_ENVIRONMENT": "Development"
}
- },
- {
- "name": "Clusters API",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "build-clusters",
- "program": "${workspaceFolder}/src/EntKube.Clusters/bin/Debug/net10.0/EntKube.Clusters.dll",
- "args": [],
- "cwd": "${workspaceFolder}/src/EntKube.Clusters",
- "stopAtEntry": false,
- "env": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "ASPNETCORE_URLS": "http://localhost:5010"
- }
- },
- {
- "name": "Identity API",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "build-identity",
- "program": "${workspaceFolder}/src/EntKube.Identity/bin/Debug/net10.0/EntKube.Identity.dll",
- "args": [],
- "cwd": "${workspaceFolder}/src/EntKube.Identity",
- "stopAtEntry": false,
- "env": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "ASPNETCORE_URLS": "http://localhost:5030"
- }
- },
- {
- "name": "Provisioning API",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "build-provisioning",
- "program": "${workspaceFolder}/src/EntKube.Provisioning/bin/Debug/net10.0/EntKube.Provisioning.dll",
- "args": [],
- "cwd": "${workspaceFolder}/src/EntKube.Provisioning",
- "stopAtEntry": false,
- "env": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "ASPNETCORE_URLS": "http://localhost:5020"
- }
- },
- {
- "name": "Secrets API",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "build-secrets",
- "program": "${workspaceFolder}/src/EntKube.Secrets/bin/Debug/net10.0/EntKube.Secrets.dll",
- "args": [],
- "cwd": "${workspaceFolder}/src/EntKube.Secrets",
- "stopAtEntry": false,
- "env": {
- "ASPNETCORE_ENVIRONMENT": "Development",
- "ASPNETCORE_URLS": "http://localhost:5040"
- }
- }
- ],
- "compounds": [
- {
- "name": "All Services",
- "configurations": [
- "Web (BFF)",
- "Clusters API",
- "Identity API",
- "Provisioning API",
- "Secrets API"
- ],
- "preLaunchTask": "build-all",
- "stopAll": true
}
]
}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index bfc5f33..651d77a 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -2,7 +2,7 @@
"version": "2.0.0",
"tasks": [
{
- "label": "build-web",
+ "label": "build",
"command": "dotnet",
"type": "process",
"args": [
@@ -14,83 +14,14 @@
"problemMatcher": "$msCompile"
},
{
- "label": "build-clusters",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/src/EntKube.Clusters/EntKube.Clusters.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary;ForceNoAlign"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "build-identity",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/src/EntKube.Identity/EntKube.Identity.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary;ForceNoAlign"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "build-provisioning",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/src/EntKube.Provisioning/EntKube.Provisioning.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary;ForceNoAlign"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "build-secrets",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/src/EntKube.Secrets/EntKube.Secrets.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary;ForceNoAlign"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "build-all",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/EntKube.slnx",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary;ForceNoAlign"
- ],
- "problemMatcher": "$msCompile",
- "group": {
- "kind": "build",
- "isDefault": true
- }
- },
- {
- "label": "test-all",
+ "label": "test",
"command": "dotnet",
"type": "process",
"args": [
"test",
- "${workspaceFolder}/EntKube.slnx",
- "--no-build"
+ "${workspaceFolder}/tests/EntKube.Web.Tests/EntKube.Web.Tests.csproj"
],
- "problemMatcher": "$msCompile",
- "group": {
- "kind": "test",
- "isDefault": true
- }
+ "problemMatcher": "$msCompile"
}
]
}
diff --git a/CMKS - Infra b/CMKS - Infra
new file mode 160000
index 0000000..ac8bf53
--- /dev/null
+++ b/CMKS - Infra
@@ -0,0 +1 @@
+Subproject commit ac8bf53d54136fd15e8e8e8c6bad8a7b2175eb6a000000000000000000000000
diff --git a/Charts/entkube/.helmignore b/Charts/entkube/.helmignore
deleted file mode 100644
index d3ce842..0000000
--- a/Charts/entkube/.helmignore
+++ /dev/null
@@ -1,4 +0,0 @@
-# Patterns to ignore when packaging the chart
-.DS_Store
-*.tgz
-.git/
diff --git a/Charts/entkube/Chart.yaml b/Charts/entkube/Chart.yaml
deleted file mode 100644
index a6a531b..0000000
--- a/Charts/entkube/Chart.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v2
-name: entkube
-description: A Helm chart for the EntKube multi-tenant Kubernetes management platform
-type: application
-version: 0.1.0
-appVersion: "1.0.0"
-keywords:
- - entkube
- - kubernetes
- - multi-tenant
- - blazor
-maintainers:
- - name: EntKube Team
diff --git a/Charts/entkube/templates/_helpers.tpl b/Charts/entkube/templates/_helpers.tpl
deleted file mode 100644
index af2bef4..0000000
--- a/Charts/entkube/templates/_helpers.tpl
+++ /dev/null
@@ -1,67 +0,0 @@
-{{/*
-Expand the name of the chart.
-*/}}
-{{- define "entkube.name" -}}
-{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
-{{- end }}
-
-{{/*
-Create a default fully qualified app name.
-*/}}
-{{- define "entkube.fullname" -}}
-{{- if .Values.fullnameOverride }}
-{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
-{{- else }}
-{{- $name := default .Chart.Name .Values.nameOverride }}
-{{- if contains $name .Release.Name }}
-{{- .Release.Name | trunc 63 | trimSuffix "-" }}
-{{- else }}
-{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
-{{- end }}
-{{- end }}
-{{- end }}
-
-{{/*
-Create chart name and version as used by the chart label.
-*/}}
-{{- define "entkube.chart" -}}
-{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
-{{- end }}
-
-{{/*
-Common labels
-*/}}
-{{- define "entkube.labels" -}}
-helm.sh/chart: {{ include "entkube.chart" . }}
-app.kubernetes.io/managed-by: {{ .Release.Service }}
-app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
-{{- end }}
-
-{{/*
-Selector labels for a specific component
-*/}}
-{{- define "entkube.selectorLabels" -}}
-app.kubernetes.io/name: {{ include "entkube.name" . }}
-app.kubernetes.io/instance: {{ .Release.Name }}
-{{- end }}
-
-{{/*
-Image reference helper
-*/}}
-{{- define "entkube.image" -}}
-{{- $registry := .root.Values.image.registry -}}
-{{- $repository := .component.image.repository -}}
-{{- $tag := default .root.Chart.AppVersion .root.Values.image.tag -}}
-{{- printf "%s/%s:%s" $registry $repository $tag -}}
-{{- end }}
-
-{{/*
-Service account name
-*/}}
-{{- define "entkube.serviceAccountName" -}}
-{{- if .Values.serviceAccount.create }}
-{{- default (include "entkube.fullname" .) .Values.serviceAccount.name }}
-{{- else }}
-{{- default "default" .Values.serviceAccount.name }}
-{{- end }}
-{{- end }}
diff --git a/Charts/entkube/templates/clusters-deployment.yaml b/Charts/entkube/templates/clusters-deployment.yaml
deleted file mode 100644
index 6eee296..0000000
--- a/Charts/entkube/templates/clusters-deployment.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-{{- if .Values.clusters.enabled }}
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: {{ include "entkube.fullname" . }}-clusters
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: clusters
-spec:
- replicas: {{ .Values.clusters.replicaCount }}
- selector:
- matchLabels:
- {{- include "entkube.selectorLabels" . | nindent 6 }}
- app.kubernetes.io/component: clusters
- template:
- metadata:
- labels:
- {{- include "entkube.selectorLabels" . | nindent 8 }}
- app.kubernetes.io/component: clusters
- spec:
- {{- with .Values.imagePullSecrets }}
- imagePullSecrets:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- serviceAccountName: {{ include "entkube.serviceAccountName" . }}
- containers:
- - name: clusters
- image: {{ include "entkube.image" (dict "root" . "component" .Values.clusters) }}
- imagePullPolicy: {{ .Values.image.pullPolicy }}
- ports:
- - name: http
- containerPort: {{ .Values.clusters.port }}
- protocol: TCP
- livenessProbe:
- httpGet:
- path: /health
- port: http
- initialDelaySeconds: 10
- periodSeconds: 30
- readinessProbe:
- httpGet:
- path: /health/ready
- port: http
- initialDelaySeconds: 5
- periodSeconds: 10
- resources:
- {{- toYaml .Values.clusters.resources | nindent 12 }}
- env:
- - name: ASPNETCORE_URLS
- value: "http://+:{{ .Values.clusters.port }}"
-{{- end }}
diff --git a/Charts/entkube/templates/clusters-service.yaml b/Charts/entkube/templates/clusters-service.yaml
deleted file mode 100644
index aa3a6d6..0000000
--- a/Charts/entkube/templates/clusters-service.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-{{- if .Values.clusters.enabled }}
-apiVersion: v1
-kind: Service
-metadata:
- name: {{ include "entkube.fullname" . }}-clusters
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: clusters
-spec:
- type: ClusterIP
- ports:
- - port: {{ .Values.clusters.port }}
- targetPort: http
- protocol: TCP
- name: http
- selector:
- {{- include "entkube.selectorLabels" . | nindent 4 }}
- app.kubernetes.io/component: clusters
-{{- end }}
diff --git a/Charts/entkube/templates/identity-deployment.yaml b/Charts/entkube/templates/identity-deployment.yaml
deleted file mode 100644
index fed23ab..0000000
--- a/Charts/entkube/templates/identity-deployment.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-{{- if .Values.identity.enabled }}
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: {{ include "entkube.fullname" . }}-identity
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: identity
-spec:
- replicas: {{ .Values.identity.replicaCount }}
- selector:
- matchLabels:
- {{- include "entkube.selectorLabels" . | nindent 6 }}
- app.kubernetes.io/component: identity
- template:
- metadata:
- labels:
- {{- include "entkube.selectorLabels" . | nindent 8 }}
- app.kubernetes.io/component: identity
- spec:
- {{- with .Values.imagePullSecrets }}
- imagePullSecrets:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- serviceAccountName: {{ include "entkube.serviceAccountName" . }}
- containers:
- - name: identity
- image: {{ include "entkube.image" (dict "root" . "component" .Values.identity) }}
- imagePullPolicy: {{ .Values.image.pullPolicy }}
- ports:
- - name: http
- containerPort: {{ .Values.identity.port }}
- protocol: TCP
- livenessProbe:
- httpGet:
- path: /health
- port: http
- initialDelaySeconds: 10
- periodSeconds: 30
- readinessProbe:
- httpGet:
- path: /health/ready
- port: http
- initialDelaySeconds: 5
- periodSeconds: 10
- resources:
- {{- toYaml .Values.identity.resources | nindent 12 }}
- env:
- - name: ASPNETCORE_URLS
- value: "http://+:{{ .Values.identity.port }}"
-{{- end }}
diff --git a/Charts/entkube/templates/identity-service.yaml b/Charts/entkube/templates/identity-service.yaml
deleted file mode 100644
index 7899e4c..0000000
--- a/Charts/entkube/templates/identity-service.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-{{- if .Values.identity.enabled }}
-apiVersion: v1
-kind: Service
-metadata:
- name: {{ include "entkube.fullname" . }}-identity
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: identity
-spec:
- type: ClusterIP
- ports:
- - port: {{ .Values.identity.port }}
- targetPort: http
- protocol: TCP
- name: http
- selector:
- {{- include "entkube.selectorLabels" . | nindent 4 }}
- app.kubernetes.io/component: identity
-{{- end }}
diff --git a/Charts/entkube/templates/ingress.yaml b/Charts/entkube/templates/ingress.yaml
deleted file mode 100644
index b096479..0000000
--- a/Charts/entkube/templates/ingress.yaml
+++ /dev/null
@@ -1,61 +0,0 @@
-{{- if .Values.ingress.enabled }}
-{{- if eq .Values.ingress.provider "traefik" }}
-apiVersion: traefik.io/v1alpha1
-kind: IngressRoute
-metadata:
- name: {{ include "entkube.fullname" . }}
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- {{- with .Values.ingress.annotations }}
- annotations:
- {{- toYaml . | nindent 4 }}
- {{- end }}
-spec:
- entryPoints:
- - websecure
- routes:
- - match: Host(`{{ .Values.ingress.host }}`)
- kind: Rule
- services:
- - name: {{ include "entkube.fullname" . }}-web
- port: {{ .Values.web.port }}
- {{- if .Values.ingress.tls.enabled }}
- tls:
- secretName: {{ .Values.ingress.tls.secretName }}
- {{- end }}
-{{- else if eq .Values.ingress.provider "gatewayapi" }}
-apiVersion: gateway.networking.k8s.io/v1
-kind: HTTPRoute
-metadata:
- name: {{ include "entkube.fullname" . }}
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
-spec:
- parentRefs:
- - name: {{ include "entkube.fullname" . }}-gateway
- hostnames:
- - {{ .Values.ingress.host }}
- rules:
- - backendRefs:
- - name: {{ include "entkube.fullname" . }}-web
- port: {{ .Values.web.port }}
-{{- else if eq .Values.ingress.provider "istio" }}
-apiVersion: networking.istio.io/v1
-kind: VirtualService
-metadata:
- name: {{ include "entkube.fullname" . }}
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
-spec:
- hosts:
- - {{ .Values.ingress.host }}
- gateways:
- - {{ include "entkube.fullname" . }}-gateway
- http:
- - route:
- - destination:
- host: {{ include "entkube.fullname" . }}-web
- port:
- number: {{ .Values.web.port }}
-{{- end }}
-{{- end }}
diff --git a/Charts/entkube/templates/provisioning-deployment.yaml b/Charts/entkube/templates/provisioning-deployment.yaml
deleted file mode 100644
index 5ea565c..0000000
--- a/Charts/entkube/templates/provisioning-deployment.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-{{- if .Values.provisioning.enabled }}
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: {{ include "entkube.fullname" . }}-provisioning
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: provisioning
-spec:
- replicas: {{ .Values.provisioning.replicaCount }}
- selector:
- matchLabels:
- {{- include "entkube.selectorLabels" . | nindent 6 }}
- app.kubernetes.io/component: provisioning
- template:
- metadata:
- labels:
- {{- include "entkube.selectorLabels" . | nindent 8 }}
- app.kubernetes.io/component: provisioning
- spec:
- {{- with .Values.imagePullSecrets }}
- imagePullSecrets:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- serviceAccountName: {{ include "entkube.serviceAccountName" . }}
- containers:
- - name: provisioning
- image: {{ include "entkube.image" (dict "root" . "component" .Values.provisioning) }}
- imagePullPolicy: {{ .Values.image.pullPolicy }}
- ports:
- - name: http
- containerPort: {{ .Values.provisioning.port }}
- protocol: TCP
- livenessProbe:
- httpGet:
- path: /health
- port: http
- initialDelaySeconds: 10
- periodSeconds: 30
- readinessProbe:
- httpGet:
- path: /health/ready
- port: http
- initialDelaySeconds: 5
- periodSeconds: 10
- resources:
- {{- toYaml .Values.provisioning.resources | nindent 12 }}
- env:
- - name: ASPNETCORE_URLS
- value: "http://+:{{ .Values.provisioning.port }}"
-{{- end }}
diff --git a/Charts/entkube/templates/provisioning-service.yaml b/Charts/entkube/templates/provisioning-service.yaml
deleted file mode 100644
index c5a8903..0000000
--- a/Charts/entkube/templates/provisioning-service.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-{{- if .Values.provisioning.enabled }}
-apiVersion: v1
-kind: Service
-metadata:
- name: {{ include "entkube.fullname" . }}-provisioning
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: provisioning
-spec:
- type: ClusterIP
- ports:
- - port: {{ .Values.provisioning.port }}
- targetPort: http
- protocol: TCP
- name: http
- selector:
- {{- include "entkube.selectorLabels" . | nindent 4 }}
- app.kubernetes.io/component: provisioning
-{{- end }}
diff --git a/Charts/entkube/templates/secrets-deployment.yaml b/Charts/entkube/templates/secrets-deployment.yaml
deleted file mode 100644
index 3e3a11c..0000000
--- a/Charts/entkube/templates/secrets-deployment.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-{{- if .Values.secrets.enabled }}
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: {{ include "entkube.fullname" . }}-secrets
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: secrets
-spec:
- replicas: {{ .Values.secrets.replicaCount }}
- selector:
- matchLabels:
- {{- include "entkube.selectorLabels" . | nindent 6 }}
- app.kubernetes.io/component: secrets
- template:
- metadata:
- labels:
- {{- include "entkube.selectorLabels" . | nindent 8 }}
- app.kubernetes.io/component: secrets
- spec:
- {{- with .Values.imagePullSecrets }}
- imagePullSecrets:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- serviceAccountName: {{ include "entkube.serviceAccountName" . }}
- containers:
- - name: secrets
- image: {{ include "entkube.image" (dict "root" . "component" .Values.secrets) }}
- imagePullPolicy: {{ .Values.image.pullPolicy }}
- ports:
- - name: http
- containerPort: {{ .Values.secrets.port }}
- protocol: TCP
- livenessProbe:
- httpGet:
- path: /health
- port: http
- initialDelaySeconds: 10
- periodSeconds: 30
- readinessProbe:
- httpGet:
- path: /health/ready
- port: http
- initialDelaySeconds: 5
- periodSeconds: 10
- resources:
- {{- toYaml .Values.secrets.resources | nindent 12 }}
- env:
- - name: ASPNETCORE_URLS
- value: "http://+:{{ .Values.secrets.port }}"
-{{- end }}
diff --git a/Charts/entkube/templates/secrets-service.yaml b/Charts/entkube/templates/secrets-service.yaml
deleted file mode 100644
index 4ab2715..0000000
--- a/Charts/entkube/templates/secrets-service.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-{{- if .Values.secrets.enabled }}
-apiVersion: v1
-kind: Service
-metadata:
- name: {{ include "entkube.fullname" . }}-secrets
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: secrets
-spec:
- type: ClusterIP
- ports:
- - port: {{ .Values.secrets.port }}
- targetPort: http
- protocol: TCP
- name: http
- selector:
- {{- include "entkube.selectorLabels" . | nindent 4 }}
- app.kubernetes.io/component: secrets
-{{- end }}
diff --git a/Charts/entkube/templates/serviceaccount.yaml b/Charts/entkube/templates/serviceaccount.yaml
deleted file mode 100644
index 7d4da49..0000000
--- a/Charts/entkube/templates/serviceaccount.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-{{- if .Values.serviceAccount.create }}
-apiVersion: v1
-kind: ServiceAccount
-metadata:
- name: {{ include "entkube.serviceAccountName" . }}
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- {{- with .Values.serviceAccount.annotations }}
- annotations:
- {{- toYaml . | nindent 4 }}
- {{- end }}
-{{- end }}
diff --git a/Charts/entkube/templates/web-deployment.yaml b/Charts/entkube/templates/web-deployment.yaml
deleted file mode 100644
index 23cc2b2..0000000
--- a/Charts/entkube/templates/web-deployment.yaml
+++ /dev/null
@@ -1,57 +0,0 @@
-{{- if .Values.web.enabled }}
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: {{ include "entkube.fullname" . }}-web
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: web
-spec:
- replicas: {{ .Values.web.replicaCount }}
- selector:
- matchLabels:
- {{- include "entkube.selectorLabels" . | nindent 6 }}
- app.kubernetes.io/component: web
- template:
- metadata:
- labels:
- {{- include "entkube.selectorLabels" . | nindent 8 }}
- app.kubernetes.io/component: web
- spec:
- {{- with .Values.imagePullSecrets }}
- imagePullSecrets:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- serviceAccountName: {{ include "entkube.serviceAccountName" . }}
- containers:
- - name: web
- image: {{ include "entkube.image" (dict "root" . "component" .Values.web) }}
- imagePullPolicy: {{ .Values.image.pullPolicy }}
- ports:
- - name: http
- containerPort: {{ .Values.web.port }}
- protocol: TCP
- livenessProbe:
- httpGet:
- path: /health
- port: http
- initialDelaySeconds: 10
- periodSeconds: 30
- readinessProbe:
- httpGet:
- path: /health/ready
- port: http
- initialDelaySeconds: 5
- periodSeconds: 10
- resources:
- {{- toYaml .Values.web.resources | nindent 12 }}
- env:
- - name: ASPNETCORE_URLS
- value: "http://+:{{ .Values.web.port }}"
- - name: Services__Clusters__BaseUrl
- value: "http://{{ include "entkube.fullname" . }}-clusters:{{ .Values.clusters.port }}"
- - name: Services__Provisioning__BaseUrl
- value: "http://{{ include "entkube.fullname" . }}-provisioning:{{ .Values.provisioning.port }}"
- - name: Services__Identity__BaseUrl
- value: "http://{{ include "entkube.fullname" . }}-identity:{{ .Values.identity.port }}"
-{{- end }}
diff --git a/Charts/entkube/templates/web-service.yaml b/Charts/entkube/templates/web-service.yaml
deleted file mode 100644
index b96ac7e..0000000
--- a/Charts/entkube/templates/web-service.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-{{- if .Values.web.enabled }}
-apiVersion: v1
-kind: Service
-metadata:
- name: {{ include "entkube.fullname" . }}-web
- labels:
- {{- include "entkube.labels" . | nindent 4 }}
- app.kubernetes.io/component: web
-spec:
- type: ClusterIP
- ports:
- - port: {{ .Values.web.port }}
- targetPort: http
- protocol: TCP
- name: http
- selector:
- {{- include "entkube.selectorLabels" . | nindent 4 }}
- app.kubernetes.io/component: web
-{{- end }}
diff --git a/Charts/entkube/values-dev.yaml b/Charts/entkube/values-dev.yaml
deleted file mode 100644
index fb073d2..0000000
--- a/Charts/entkube/values-dev.yaml
+++ /dev/null
@@ -1,91 +0,0 @@
-# Default values override for local development (k3d/kind/minikube)
-
-replicaCount: 1
-
-image:
- registry: localhost:5050
- pullPolicy: Always
- tag: "dev"
-
-web:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/web
- port: 5000
- resources:
- requests:
- cpu: 50m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 512Mi
-
-clusters:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/clusters
- port: 5010
- resources:
- requests:
- cpu: 50m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-provisioning:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/provisioning
- port: 5020
- resources:
- requests:
- cpu: 50m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-identity:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/identity
- port: 5030
- resources:
- requests:
- cpu: 50m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-secrets:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/secrets
- port: 5040
- resources:
- requests:
- cpu: 50m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-ingress:
- enabled: true
- provider: traefik
- host: entkube.localhost
- tls:
- enabled: false
- secretName: ""
-
-serviceAccount:
- create: true
- name: ""
- annotations: {}
diff --git a/Charts/entkube/values.yaml b/Charts/entkube/values.yaml
deleted file mode 100644
index b515e19..0000000
--- a/Charts/entkube/values.yaml
+++ /dev/null
@@ -1,98 +0,0 @@
-# Default values for EntKube Helm chart
-
-replicaCount: 1
-
-image:
- registry: gitea.example.com
- pullPolicy: IfNotPresent
- tag: "" # Defaults to appVersion
-
-imagePullSecrets: []
-nameOverride: ""
-fullnameOverride: ""
-
-# Service configurations per microservice
-web:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/web
- port: 5000
- resources:
- requests:
- cpu: 100m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 512Mi
-
-clusters:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/clusters
- port: 5010
- resources:
- requests:
- cpu: 100m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-provisioning:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/provisioning
- port: 5020
- resources:
- requests:
- cpu: 100m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-identity:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/identity
- port: 5030
- resources:
- requests:
- cpu: 100m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-secrets:
- enabled: true
- replicaCount: 1
- image:
- repository: entkube/secrets
- port: 5040
- resources:
- requests:
- cpu: 100m
- memory: 128Mi
- limits:
- cpu: 500m
- memory: 256Mi
-
-# Ingress configuration — select your strategy
-ingress:
- enabled: true
- provider: traefik # Options: traefik | istio | gatewayapi
- host: entkube.example.com
- tls:
- enabled: true
- secretName: entkube-tls
- annotations: {}
-
-serviceAccount:
- create: true
- name: ""
- annotations: {}
diff --git a/Directory.Build.props b/Directory.Build.props
index 569df69..0d4aef4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,5 @@
- false
false
- false
diff --git a/EntKube.slnx b/EntKube.slnx
index cbd8e1f..b21d15f 100644
--- a/EntKube.slnx
+++ b/EntKube.slnx
@@ -1,16 +1,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Tiltfile b/Tiltfile
deleted file mode 100644
index ce0ceff..0000000
--- a/Tiltfile
+++ /dev/null
@@ -1,92 +0,0 @@
-# -*- mode: Python -*-
-
-# EntKube Tiltfile — Helm-based local Kubernetes development
-#
-# Prerequisites:
-# - Docker Desktop or Rancher Desktop
-# - k3d (recommended): k3d cluster create entkube --registry-create entkube-registry:0.0.0.0:5050 -p "8080:80@loadbalancer"
-# - Tilt: https://docs.tilt.dev/install.html
-# - Helm 3
-#
-# Usage:
-# tilt up
-#
-# This watches source code, rebuilds Docker images, and redeploys via Helm.
-
-# Allow the k3d local registry
-allow_k8s_contexts(['k3d-entkube'])
-
-# Use the local registry created by k3d
-default_registry('localhost:5050')
-
-# Docker builds for each service — context is the repo root so COPY paths work
-docker_build(
- 'entkube/web',
- '.',
- dockerfile='src/EntKube.Web/Dockerfile',
- live_update=[
- sync('src/EntKube.Web/', '/src/src/EntKube.Web/'),
- sync('src/EntKube.Web.Client/', '/src/src/EntKube.Web.Client/'),
- sync('src/EntKube.SharedKernel/', '/src/src/EntKube.SharedKernel/'),
- run('cd /src/src/EntKube.Web && dotnet build -c Release -o /app', trigger=['src/EntKube.Web/', 'src/EntKube.Web.Client/', 'src/EntKube.SharedKernel/']),
- ]
-)
-
-docker_build(
- 'entkube/clusters',
- '.',
- dockerfile='src/EntKube.Clusters/Dockerfile',
- live_update=[
- sync('src/EntKube.Clusters/', '/src/src/EntKube.Clusters/'),
- sync('src/EntKube.SharedKernel/', '/src/src/EntKube.SharedKernel/'),
- run('cd /src/src/EntKube.Clusters && dotnet build -c Release -o /app', trigger=['src/EntKube.Clusters/', 'src/EntKube.SharedKernel/']),
- ]
-)
-
-docker_build(
- 'entkube/identity',
- '.',
- dockerfile='src/EntKube.Identity/Dockerfile',
- live_update=[
- sync('src/EntKube.Identity/', '/src/src/EntKube.Identity/'),
- sync('src/EntKube.SharedKernel/', '/src/src/EntKube.SharedKernel/'),
- run('cd /src/src/EntKube.Identity && dotnet build -c Release -o /app', trigger=['src/EntKube.Identity/', 'src/EntKube.SharedKernel/']),
- ]
-)
-
-docker_build(
- 'entkube/provisioning',
- '.',
- dockerfile='src/EntKube.Provisioning/Dockerfile',
- live_update=[
- sync('src/EntKube.Provisioning/', '/src/src/EntKube.Provisioning/'),
- sync('src/EntKube.SharedKernel/', '/src/src/EntKube.SharedKernel/'),
- run('cd /src/src/EntKube.Provisioning && dotnet build -c Release -o /app', trigger=['src/EntKube.Provisioning/', 'src/EntKube.SharedKernel/']),
- ]
-)
-
-docker_build(
- 'entkube/secrets',
- '.',
- dockerfile='src/EntKube.Secrets/Dockerfile',
- live_update=[
- sync('src/EntKube.Secrets/', '/src/src/EntKube.Secrets/'),
- sync('src/EntKube.SharedKernel/', '/src/src/EntKube.SharedKernel/'),
- run('cd /src/src/EntKube.Secrets && dotnet build -c Release -o /app', trigger=['src/EntKube.Secrets/', 'src/EntKube.SharedKernel/']),
- ]
-)
-
-# Deploy via Helm chart with dev values
-k8s_yaml(helm(
- 'Charts/entkube',
- name='entkube',
- namespace='default',
- values=['Charts/entkube/values-dev.yaml'],
-))
-
-# Resource grouping for the Tilt UI
-k8s_resource('entkube-web', port_forwards='5000:5000', labels=['entkube'])
-k8s_resource('entkube-clusters', port_forwards='5010:5010', labels=['entkube'])
-k8s_resource('entkube-provisioning', port_forwards='5020:5020', labels=['entkube'])
-k8s_resource('entkube-identity', port_forwards='5030:5030', labels=['entkube'])
-k8s_resource('entkube-secrets', port_forwards='5040:5040', labels=['entkube'])
diff --git a/src/EntKube.Clusters/Dockerfile b/src/EntKube.Clusters/Dockerfile
deleted file mode 100644
index 0ec1f22..0000000
--- a/src/EntKube.Clusters/Dockerfile
+++ /dev/null
@@ -1,21 +0,0 @@
-FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
-WORKDIR /app
-EXPOSE 5010
-
-FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
-WORKDIR /src
-COPY ["src/EntKube.Clusters/EntKube.Clusters.csproj", "src/EntKube.Clusters/"]
-COPY ["src/EntKube.SharedKernel/EntKube.SharedKernel.csproj", "src/EntKube.SharedKernel/"]
-RUN dotnet restore "src/EntKube.Clusters/EntKube.Clusters.csproj"
-COPY . .
-WORKDIR "/src/src/EntKube.Clusters"
-RUN dotnet build -c Release -o /app/build
-
-FROM build AS publish
-RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENV ASPNETCORE_URLS=http://+:5010
-ENTRYPOINT ["dotnet", "EntKube.Clusters.dll"]
diff --git a/src/EntKube.Clusters/Domain/ClusterComponent.cs b/src/EntKube.Clusters/Domain/ClusterComponent.cs
deleted file mode 100644
index f3637b3..0000000
--- a/src/EntKube.Clusters/Domain/ClusterComponent.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using EntKube.Clusters.Features.AdoptCluster;
-
-namespace EntKube.Clusters.Domain;
-
-///
-/// Represents a tracked component on a cluster. Once detected during an adoption
-/// scan, the component is persisted so subsequent page loads can display state
-/// without hitting the cluster API again. Configuration changes (via deploy or
-/// configure operations) update this record in-place to reflect current state.
-///
-public class ClusterComponent
-{
- public string ComponentName { get; private set; } = string.Empty;
- public ComponentStatus Status { get; private set; }
- public string? Version { get; private set; }
- public string? Namespace { get; private set; }
- public string? HelmReleaseName { get; private set; }
- public Dictionary Configuration { get; private set; } = new();
- public DateTimeOffset LastCheckedAt { get; private set; }
-
- private ClusterComponent() { }
-
- ///
- /// Creates a component record from an adoption check result. This captures
- /// a snapshot of what the scan discovered — status, version, namespace,
- /// Helm release, and running configuration values.
- ///
- public static ClusterComponent FromCheckResult(ComponentCheckResult result)
- {
- return new ClusterComponent
- {
- ComponentName = result.ComponentName,
- Status = result.Status,
- Version = result.Configuration?.Version,
- Namespace = result.Configuration?.Namespace,
- HelmReleaseName = result.Configuration?.HelmReleaseName,
- Configuration = result.Configuration?.Values ?? new Dictionary(),
- LastCheckedAt = DateTimeOffset.UtcNow
- };
- }
-
- ///
- /// Merges new configuration values into the existing configuration.
- /// Existing keys are overwritten, new keys are added, keys not in the
- /// update are left unchanged.
- ///
- public void MergeConfiguration(Dictionary values)
- {
- foreach (KeyValuePair kvp in values)
- {
- Configuration[kvp.Key] = kvp.Value;
- }
- }
-
- ///
- /// After a successful deployment, mark the component as Installed.
- ///
- public void MarkInstalled()
- {
- Status = ComponentStatus.Installed;
- LastCheckedAt = DateTimeOffset.UtcNow;
- }
-
- ///
- /// Updates the tracked version after a successful upgrade. The version
- /// string comes from Helm or the installer and reflects what's now
- /// running on the cluster.
- ///
- public void UpdateVersion(string version)
- {
- Version = version;
- LastCheckedAt = DateTimeOffset.UtcNow;
- }
-
- ///
- /// After a successful uninstall, mark the component as NotInstalled.
- ///
- public void MarkUninstalled()
- {
- Status = ComponentStatus.NotInstalled;
- LastCheckedAt = DateTimeOffset.UtcNow;
- }
-}
diff --git a/src/EntKube.Clusters/Domain/IClusterRepository.cs b/src/EntKube.Clusters/Domain/IClusterRepository.cs
deleted file mode 100644
index c5a5501..0000000
--- a/src/EntKube.Clusters/Domain/IClusterRepository.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace EntKube.Clusters.Domain;
-
-///
-/// Defines how the cluster service persists and retrieves cluster aggregates.
-/// The implementation lives in the infrastructure layer — the domain only
-/// knows about this contract, keeping it persistence-agnostic.
-///
-public interface IClusterRepository
-{
- Task GetByIdAsync(Guid id, CancellationToken ct = default);
- Task> GetAllAsync(CancellationToken ct = default);
- Task AddAsync(KubernetesCluster cluster, CancellationToken ct = default);
- Task UpdateAsync(KubernetesCluster cluster, CancellationToken ct = default);
- Task DeleteAsync(Guid id, CancellationToken ct = default);
-}
diff --git a/src/EntKube.Clusters/Domain/KubernetesCluster.cs b/src/EntKube.Clusters/Domain/KubernetesCluster.cs
deleted file mode 100644
index bca26ae..0000000
--- a/src/EntKube.Clusters/Domain/KubernetesCluster.cs
+++ /dev/null
@@ -1,274 +0,0 @@
-using EntKube.Clusters.Features.AdoptCluster;
-
-namespace EntKube.Clusters.Domain;
-
-///
-/// A KubernetesCluster represents a registered cluster that EntKube manages.
-/// It holds the connection details, health state, and metadata needed to
-/// interact with the cluster's API server. Every cluster belongs to exactly
-/// one tenant — no cluster can exist without an owning tenant. The kubeconfig
-/// is stored as the credential material for API access.
-///
-public class KubernetesCluster
-{
- public Guid Id { get; private set; }
- public Guid TenantId { get; private set; }
- public Guid EnvironmentId { get; private set; }
- public string Name { get; private set; } = string.Empty;
- public string ApiServerUrl { get; private set; } = string.Empty;
- public string ContextName { get; private set; } = string.Empty;
- public ClusterStatus Status { get; private set; }
- public CloudProvider Provider { get; private set; }
- public ProviderCredentials? ProviderCredentials { get; private set; }
- public string KubeConfig { get; private set; } = string.Empty;
- public DateTimeOffset RegisteredAt { get; private set; }
- public DateTimeOffset? LastHealthCheckAt { get; private set; }
- public IReadOnlyList PrometheusEndpoints => prometheusEndpoints.AsReadOnly();
- public IReadOnlyList Components => components.AsReadOnly();
- public IReadOnlyList StorageBuckets => storageBuckets.AsReadOnly();
-
- private List prometheusEndpoints = new();
- private List components = new();
- private List storageBuckets = new();
-
- private KubernetesCluster() { }
-
- ///
- /// Registers a new cluster in the platform. The caller must provide a tenant,
- /// cluster name, API server URL, kubeconfig, and the environment it belongs to.
- /// The cluster starts in a Pending state until a health check confirms it's reachable.
- /// Every cluster must be assigned to an environment — no orphan clusters allowed.
- ///
- public static KubernetesCluster Register(string name, string apiServerUrl, string kubeConfig, Guid tenantId, string contextName, Guid environmentId)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("Cluster name is required.", nameof(name));
- }
-
- if (string.IsNullOrWhiteSpace(apiServerUrl))
- {
- throw new ArgumentException("API server URL is required.", nameof(apiServerUrl));
- }
-
- if (string.IsNullOrWhiteSpace(kubeConfig))
- {
- throw new ArgumentException("KubeConfig is required.", nameof(kubeConfig));
- }
-
- if (tenantId == Guid.Empty)
- {
- throw new ArgumentException("A tenant must be specified.", nameof(tenantId));
- }
-
- if (string.IsNullOrWhiteSpace(contextName))
- {
- throw new ArgumentException("A context name must be specified.", nameof(contextName));
- }
-
- if (environmentId == Guid.Empty)
- {
- throw new ArgumentException("An environment must be specified.", nameof(environmentId));
- }
-
- return new KubernetesCluster
- {
- Id = Guid.NewGuid(),
- TenantId = tenantId,
- EnvironmentId = environmentId,
- Name = name,
- ApiServerUrl = apiServerUrl,
- ContextName = contextName,
- KubeConfig = kubeConfig,
- Status = ClusterStatus.Pending,
- RegisteredAt = DateTimeOffset.UtcNow
- };
- }
-
- ///
- /// After a successful health check, we mark the cluster as connected.
- /// This means the platform can now schedule work against this cluster.
- ///
- public void MarkConnected()
- {
- Status = ClusterStatus.Connected;
- LastHealthCheckAt = DateTimeOffset.UtcNow;
- }
-
- ///
- /// When a health check fails, we mark the cluster as unreachable.
- /// Existing workloads keep running, but no new provisioning can happen
- /// until connectivity is restored.
- ///
- public void MarkUnreachable()
- {
- Status = ClusterStatus.Unreachable;
- LastHealthCheckAt = DateTimeOffset.UtcNow;
- }
-
- ///
- /// Assigns (or reassigns) this cluster to an environment. A cluster lives
- /// in exactly one environment at a time — for example dev01 belongs to "dev",
- /// test01 and test02 belong to "test", prod01 belongs to "prod".
- ///
- public void AssignToEnvironment(Guid environmentId)
- {
- if (environmentId == Guid.Empty)
- {
- throw new ArgumentException("Environment ID is required.", nameof(environmentId));
- }
-
- EnvironmentId = environmentId;
- }
-
- ///
- /// Replaces all discovered Prometheus endpoints with a fresh scan result.
- /// Called after the platform scans the cluster for Prometheus services.
- /// If multiple instances are found, they are all stored so metrics can
- /// be aggregated from all of them.
- ///
- public void SetPrometheusEndpoints(List endpoints)
- {
- prometheusEndpoints = endpoints ?? new List();
- }
-
- ///
- /// Replaces all tracked components with a fresh scan result from the adoption
- /// check. Each component check result is converted into a ClusterComponent
- /// that persists the detected state — status, version, namespace, config.
- /// This is called after every adoption scan so stored state stays current.
- ///
- public void UpdateComponents(List checkResults)
- {
- components = checkResults
- .Select(ClusterComponent.FromCheckResult)
- .ToList();
- }
-
- ///
- /// Merges new configuration values into an already-tracked component.
- /// Called after a successful configure operation so the persisted config
- /// reflects what's actually deployed. If the component hasn't been
- /// tracked yet (no prior scan), this is a no-op.
- ///
- public void UpdateComponentConfiguration(string componentName, Dictionary values)
- {
- ClusterComponent? component = components.FirstOrDefault(
- c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
-
- component?.MergeConfiguration(values);
- }
-
- ///
- /// Marks a tracked component as Installed after a successful deployment.
- /// If the component hasn't been tracked yet, this is a no-op.
- ///
- public void MarkComponentInstalled(string componentName)
- {
- ClusterComponent? component = components.FirstOrDefault(
- c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
-
- component?.MarkInstalled();
- }
-
- ///
- /// Updates the version of a tracked component after a successful upgrade.
- /// If the component hasn't been tracked yet, this is a no-op.
- ///
- public void UpdateComponentVersion(string componentName, string version)
- {
- ClusterComponent? component = components.FirstOrDefault(
- c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
-
- component?.UpdateVersion(version);
- }
-
- ///
- /// Marks a tracked component as NotInstalled after a successful uninstall.
- /// If the component hasn't been tracked yet, this is a no-op.
- ///
- public void MarkComponentUninstalled(string componentName)
- {
- ClusterComponent? component = components.FirstOrDefault(
- c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
-
- component?.MarkUninstalled();
- }
-
- ///
- /// Assigns a cloud provider to this cluster. Providers like Cleura offer
- /// additional APIs (OpenStack, Gardener) that EntKube can leverage for
- /// provisioning, billing, and resource management. Credentials are required
- /// for any provider other than None.
- ///
- public void SetProvider(CloudProvider provider, ProviderCredentials? credentials)
- {
- if (provider != CloudProvider.None && credentials is null)
- {
- throw new ArgumentException(
- $"Credentials are required when setting provider to {provider}.",
- nameof(credentials));
- }
-
- Provider = provider;
- ProviderCredentials = provider == CloudProvider.None ? null : credentials;
- }
-
- ///
- /// Registers a newly created storage bucket on this cluster. The bucket
- /// has already been created on the S3 backend — this persists the metadata
- /// so the platform knows about it and components can reference it.
- ///
- public void AddStorageBucket(StorageBucket bucket)
- {
- storageBuckets.Add(bucket);
- }
-
- ///
- /// Removes a storage bucket from this cluster's tracked buckets.
- /// Called after the bucket has been deleted from the S3 backend.
- ///
- public void RemoveStorageBucket(Guid bucketId)
- {
- storageBuckets.RemoveAll(b => b.Id == bucketId);
- }
-}
-
-///
-/// The cloud provider hosting a Kubernetes cluster. Determines which additional
-/// APIs are available for management (OpenStack, Gardener, billing, etc.).
-///
-public enum CloudProvider
-{
- None,
- Cleura,
- Aws,
- Azure,
- Gcp
-}
-
-///
-/// Credentials for authenticating with a cluster's cloud provider APIs.
-/// Cleura exposes two separate APIs:
-/// 1. Cleura Cloud REST API (rest.cleura.cloud) — uses Cleura login credentials
-/// for account management, Gardener K8s cluster ops, billing, domain listing.
-/// 2. OpenStack API (e.g. sto2.citycloud.com:5000) — uses OpenStack/Keystone
-/// credentials for infrastructure: compute, networking, storage, load balancing.
-/// Both credential sets are stored here; either may be null if not configured.
-///
-public record ProviderCredentials(
- string Username,
- string Password,
- string Region,
- string? OpenStackAuthUrl = null,
- string? OpenStackProjectId = null,
- string? OpenStackUsername = null,
- string? OpenStackPassword = null,
- string? OpenStackUserDomainName = null);
-
-public enum ClusterStatus
-{
- Pending,
- Connected,
- Unreachable
-}
diff --git a/src/EntKube.Clusters/Domain/PrometheusEndpoint.cs b/src/EntKube.Clusters/Domain/PrometheusEndpoint.cs
deleted file mode 100644
index 6fd85f0..0000000
--- a/src/EntKube.Clusters/Domain/PrometheusEndpoint.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace EntKube.Clusters.Domain;
-
-///
-/// A discovered Prometheus instance running inside a Kubernetes cluster.
-/// Stores the connection URL, the namespace where it runs, and the service name.
-/// Multiple Prometheus endpoints may exist per cluster — metrics are aggregated
-/// from all of them when queried.
-///
-public record PrometheusEndpoint(string Url, string Namespace, string ServiceName);
diff --git a/src/EntKube.Clusters/Domain/StorageBucket.cs b/src/EntKube.Clusters/Domain/StorageBucket.cs
deleted file mode 100644
index bfd7901..0000000
--- a/src/EntKube.Clusters/Domain/StorageBucket.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-namespace EntKube.Clusters.Domain;
-
-///
-/// A managed S3-compatible storage bucket provisioned through the platform.
-/// Tracks which credentials own the bucket, its encryption setting, and
-/// connection information so components can wire up to it as a storage backend.
-///
-public class StorageBucket
-{
- public Guid Id { get; private set; }
- public string Name { get; private set; } = string.Empty;
- public string AccessKey { get; private set; } = string.Empty;
- public string SecretKey { get; private set; } = string.Empty;
- public string Endpoint { get; private set; } = string.Empty;
- public string Region { get; private set; } = string.Empty;
- public bool Encrypted { get; private set; }
- public DateTimeOffset CreatedAt { get; private set; }
-
- private StorageBucket() { }
-
- ///
- /// Creates a new storage bucket record after successfully provisioning
- /// the bucket on the S3-compatible backend. The access key and secret key
- /// are the credentials that own and can access this bucket.
- ///
- public static StorageBucket Create(
- string name,
- string accessKey,
- string secretKey,
- string endpoint,
- string region,
- bool encrypted)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("Bucket name is required.", nameof(name));
- }
-
- return new StorageBucket
- {
- Id = Guid.NewGuid(),
- Name = name,
- AccessKey = accessKey,
- SecretKey = secretKey,
- Endpoint = endpoint,
- Region = region,
- Encrypted = encrypted,
- CreatedAt = DateTimeOffset.UtcNow
- };
- }
-}
diff --git a/src/EntKube.Clusters/EntKube.Clusters.csproj b/src/EntKube.Clusters/EntKube.Clusters.csproj
deleted file mode 100644
index 6e18374..0000000
--- a/src/EntKube.Clusters/EntKube.Clusters.csproj
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- net10.0
- enable
- enable
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/EntKube.Clusters/EntKube.Clusters.http b/src/EntKube.Clusters/EntKube.Clusters.http
deleted file mode 100644
index b431c29..0000000
--- a/src/EntKube.Clusters/EntKube.Clusters.http
+++ /dev/null
@@ -1,6 +0,0 @@
-@EntKube.Clusters_HostAddress = http://localhost:5243
-
-GET {{EntKube.Clusters_HostAddress}}/weatherforecast/
-Accept: application/json
-
-###
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/AdoptClusterEndpoint.cs b/src/EntKube.Clusters/Features/AdoptCluster/AdoptClusterEndpoint.cs
deleted file mode 100644
index 5b8393e..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/AdoptClusterEndpoint.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using EntKube.SharedKernel.Contracts;
-using EntKube.SharedKernel.Domain;
-
-namespace EntKube.Clusters.Features.AdoptCluster;
-
-///
-/// Maps the adoption endpoints:
-/// - POST /api/clusters/{id}/adopt — runs all checks, returns aggregate report
-/// - GET /api/clusters/{id}/components — lists available components and their status
-///
-public static class AdoptClusterEndpoint
-{
- public static void Map(IEndpointRouteBuilder app)
- {
- app.MapPost("/api/clusters/{id:guid}/adopt", async (
- Guid id,
- AdoptClusterHandler handler,
- CancellationToken ct) =>
- {
- Result result = await handler.HandleAsync(id, ct);
-
- if (result.IsFailure)
- {
- return Results.BadRequest(ApiResponse.Fail(result.Error!));
- }
-
- return Results.Ok(ApiResponse.Ok(result.Value!));
- });
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/AdoptClusterHandler.cs b/src/EntKube.Clusters/Features/AdoptCluster/AdoptClusterHandler.cs
deleted file mode 100644
index c8ae965..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/AdoptClusterHandler.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using EntKube.Clusters.Domain;
-using EntKube.SharedKernel.Domain;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster;
-
-///
-/// Orchestrates the full adoption check by running all registered component checks
-/// against the cluster. Each check runs independently — a failure in one doesn't
-/// prevent the others from reporting their status. The results are aggregated into
-/// a single AdoptionReport that tells the caller exactly what's installed, degraded,
-/// or missing.
-///
-/// Think of this as "terraform plan" for a cluster: it inspects current state
-/// and reports the delta between what exists and what's required.
-///
-public class AdoptClusterHandler
-{
- private readonly IClusterRepository repository;
- private readonly IEnumerable checks;
- private readonly ILogger logger;
-
- public AdoptClusterHandler(IClusterRepository repository, IEnumerable checks, ILogger logger)
- {
- this.repository = repository;
- this.checks = checks;
- this.logger = logger;
- }
-
- public async Task> HandleAsync(Guid clusterId, CancellationToken ct = default)
- {
- logger.LogWarning("[DIAG] AdoptClusterHandler.HandleAsync called for cluster {ClusterId} with {CheckCount} checks registered", clusterId, checks.Count());
-
- // Find the cluster — can't adopt something that doesn't exist.
-
- KubernetesCluster? cluster = await repository.GetByIdAsync(clusterId, ct);
-
- if (cluster is null)
- {
- return Result.Failure($"Cluster with ID '{clusterId}' was not found.");
- }
-
- // The cluster must be connected (reachable) so we can inspect it.
- // A Pending or Unreachable cluster can't be checked.
-
- if (cluster.Status != ClusterStatus.Connected)
- {
- return Result.Failure(
- "Cluster must be connected before adoption. Run a health check to verify connectivity.");
- }
-
- // Run all component checks in parallel — they're independent of each other.
- // Each check connects to the cluster API and inspects its own area.
-
- List> checkTasks = checks
- .Select(check => check.CheckAsync(cluster, ct))
- .ToList();
-
- ComponentCheckResult[] results = await Task.WhenAll(checkTasks);
-
- // Log what each check returned for diagnostics.
-
- foreach (ComponentCheckResult result in results)
- {
- logger.LogWarning("[DIAG] Check result: {Component} = {Status}, missing=[{Missing}], details=[{Details}]",
- result.ComponentName, result.Status,
- string.Join("; ", result.MissingItems),
- string.Join("; ", result.Details));
- }
-
- // Persist the detected components on the cluster so we don't need to
- // re-scan next time. The cluster aggregate stores a snapshot of each
- // component's status and configuration as last seen.
-
- cluster.UpdateComponents(results.ToList());
- await repository.UpdateAsync(cluster, ct);
-
- // Build the aggregate report from all individual results.
-
- AdoptionReport report = AdoptionReport.FromChecks(results.ToList());
-
- return Result.Success(report);
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/AdoptionReport.cs b/src/EntKube.Clusters/Features/AdoptCluster/AdoptionReport.cs
deleted file mode 100644
index 3949819..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/AdoptionReport.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace EntKube.Clusters.Features.AdoptCluster;
-
-///
-/// Aggregates all component check results into a single adoption report.
-/// Each component has its own line item, and the overall status is derived
-/// from the worst individual result — if any component is NotInstalled,
-/// the cluster is NotReady; if some are Degraded, it's PartiallyReady.
-///
-public class AdoptionReport
-{
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public AdoptionReadiness Status { get; }
- public List Components { get; }
-
- public AdoptionReport(AdoptionReadiness status, List components)
- {
- Status = status;
- Components = components;
- }
-
- ///
- /// Builds the aggregate report from individual component results.
- /// The overall readiness is the worst status across all components.
- /// Components are always sorted alphabetically by name for consistent display.
- ///
- public static AdoptionReport FromChecks(List results)
- {
- // Sort components alphabetically by name so the report is always
- // presented in a consistent, predictable order.
-
- List sorted = results
- .OrderBy(r => r.ComponentName, StringComparer.OrdinalIgnoreCase)
- .ToList();
-
- // If any component is entirely missing, the cluster is not ready for adoption.
-
- if (sorted.Any(r => r.Status == ComponentStatus.NotInstalled))
- {
- return new AdoptionReport(AdoptionReadiness.NotReady, sorted);
- }
-
- // If all are installed but some are degraded (unhealthy or incomplete),
- // the cluster is partially ready — it can operate but needs attention.
-
- if (sorted.Any(r => r.Status == ComponentStatus.Degraded))
- {
- return new AdoptionReport(AdoptionReadiness.PartiallyReady, sorted);
- }
-
- // Everything is present and healthy — cluster is ready for full adoption.
-
- return new AdoptionReport(AdoptionReadiness.Ready, sorted);
- }
-}
-
-///
-/// Overall cluster adoption readiness, derived from the aggregate of all
-/// component checks:
-/// - Ready: all components installed and healthy
-/// - PartiallyReady: some components degraded but present
-/// - NotReady: one or more components entirely missing
-///
-public enum AdoptionReadiness
-{
- Ready,
- PartiallyReady,
- NotReady
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/ComponentSchemas.cs b/src/EntKube.Clusters/Features/AdoptCluster/ComponentSchemas.cs
deleted file mode 100644
index d203c97..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/ComponentSchemas.cs
+++ /dev/null
@@ -1,876 +0,0 @@
-using EntKube.Clusters.Domain;
-
-namespace EntKube.Clusters.Features.AdoptCluster;
-
-///
-/// Central registry of configuration schemas for all managed components.
-/// Each schema declares what parameters a component accepts, their types,
-/// validation constraints, defaults, and UI groupings. The UI reads these
-/// to render proper typed forms with validation instead of raw key/value editors.
-///
-public static class ComponentSchemas
-{
- private static readonly Dictionary schemas = new(StringComparer.OrdinalIgnoreCase)
- {
- ["kyverno"] = new(
- ComponentName: "kyverno",
- DisplayName: "Kyverno",
- Description: "Policy engine for Kubernetes — validates, mutates, and generates resources",
- Parameters: new List
- {
- new(Key: "admissionReplicas", DisplayName: "Admission Controller Replicas", Description: "Number of admission controller pod replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 10),
- new(Key: "backgroundReplicas", DisplayName: "Background Controller Replicas", Description: "Number of background controller pod replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 10)
- }),
-
- ["security-policies"] = new(
- ComponentName: "security-policies",
- DisplayName: "Security Policies",
- Description: "Kyverno ClusterPolicies for security enforcement (image registries, load balancers, seccomp, privilege escalation)",
- Parameters: new List
- {
- new(Key: "validationAction", DisplayName: "Validation Action", Description: "Whether policies audit or enforce violations",
- Type: ParameterType.Select, Group: "Enforcement", DefaultValue: "Audit", IsRequired: true,
- AllowedValues: new List { "Audit", "Enforce" }),
- new(Key: "revertPatches", DisplayName: "Revert Seccomp & ReadOnly Patches", Description: "Deletes the seccomp and readOnlyRootFilesystem policies and reverts workload patches applied during adoption. Use this if workloads are failing due to these security constraints.",
- Type: ParameterType.Boolean, Group: "Emergency", DefaultValue: "false")
- }),
-
- ["cert-manager"] = new(
- ComponentName: "cert-manager",
- DisplayName: "cert-manager",
- Description: "Certificate lifecycle management for Kubernetes — automates TLS certificate issuance and renewal",
- Parameters: new List
- {
- new(Key: "replicas", DisplayName: "Controller Replicas", Description: "Number of cert-manager controller replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 5)
- }),
-
- ["trust-manager"] = new(
- ComponentName: "trust-manager",
- DisplayName: "trust-manager",
- Description: "Distributes trust bundles (CA certificates) across namespaces",
- Parameters: new List
- {
- new(Key: "bundleName", DisplayName: "Bundle Name", Description: "Name of the trust Bundle resource",
- Type: ParameterType.String, Group: "General", DefaultValue: "entkube-trust-bundle"),
- new(Key: "addCertificate", DisplayName: "Add Certificate", Description: "Add a new CA certificate to the trust bundle",
- Type: ParameterType.Boolean, Group: "Certificates", DefaultValue: "false"),
- new(Key: "certificateName", DisplayName: "Certificate Name", Description: "Name identifier for the certificate being added",
- Type: ParameterType.String, Group: "Certificates"),
- new(Key: "certificateData", DisplayName: "Certificate PEM Data", Description: "PEM-encoded CA certificate to add",
- Type: ParameterType.String, Group: "Certificates"),
- new(Key: "removeCertificate", DisplayName: "Remove Certificate", Description: "Name of certificate to remove from the bundle",
- Type: ParameterType.String, Group: "Certificates")
- }),
-
- ["internal-ca"] = new(
- ComponentName: "internal-ca",
- DisplayName: "Internal CA",
- Description: "Self-signed Certificate Authority for internal service-to-service TLS",
- Parameters: new List
- {
- new(Key: "caName", DisplayName: "CA Issuer Name", Description: "Name of the CA Issuer resource",
- Type: ParameterType.String, Group: "General", DefaultValue: "internal-ca"),
- new(Key: "bundleName", DisplayName: "Trust Bundle Name", Description: "Bundle to inject the CA certificate into",
- Type: ParameterType.String, Group: "General", DefaultValue: "entkube-trust-bundle"),
- new(Key: "caDurationDays", DisplayName: "CA Duration (days)", Description: "Lifetime of the CA certificate in days",
- Type: ParameterType.Integer, Group: "Certificate", DefaultValue: "3650", Min: 30, Max: 36500),
- new(Key: "caOrganization", DisplayName: "Organization", Description: "Organization name in the CA certificate subject",
- Type: ParameterType.String, Group: "Certificate", DefaultValue: "EntKube")
- }),
-
- ["domain-ca"] = new(
- ComponentName: "domain-ca",
- DisplayName: "Domain CA",
- Description: "Certificate Authority for domain-scoped TLS (e.g., *.tenant.example.com)",
- Parameters: new List
- {
- new(Key: "caName", DisplayName: "CA Issuer Name", Description: "Name of the domain CA Issuer resource",
- Type: ParameterType.String, Group: "General", DefaultValue: "domain-ca"),
- new(Key: "domains", DisplayName: "Domains", Description: "Comma-separated list of domains this CA can issue for",
- Type: ParameterType.String, Group: "General", IsRequired: true),
- new(Key: "bundleName", DisplayName: "Trust Bundle Name", Description: "Bundle to inject the CA certificate into",
- Type: ParameterType.String, Group: "General", DefaultValue: "entkube-trust-bundle")
- }),
-
- ["istio"] = new(
- ComponentName: "istio",
- DisplayName: "Istio",
- Description: "Service mesh with mTLS, traffic management, and observability",
- Parameters: new List
- {
- new(Key: "pilotReplicas", DisplayName: "Pilot Replicas", Description: "Number of istiod (pilot) replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 10),
- new(Key: "enableGatewayApi", DisplayName: "Enable Gateway API", Description: "Enable Kubernetes Gateway API support in Istio",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true")
- }),
-
- ["minio"] = new(
- ComponentName: "minio",
- DisplayName: "MinIO Operator",
- Description: "Object storage operator — manages MinIO tenants for S3-compatible storage",
- Parameters: new List
- {
- new(Key: "operatorReplicas", DisplayName: "Operator Replicas", Description: "Number of MinIO Operator pod replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "1", Min: 1, Max: 5)
- }),
-
- ["mongodb-community"] = new(
- ComponentName: "mongodb-community",
- DisplayName: "MongoDB Community Operator",
- Description: "MongoDB operator — manages MongoDB replica sets with SCRAM authentication, scaling, and upgrades",
- Parameters: new List
- {
- new(Key: "operatorReplicas", DisplayName: "Operator Replicas", Description: "Number of MongoDB operator pod replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "1", Min: 1, Max: 5),
- new(Key: "watchNamespace", DisplayName: "Watch Namespace", Description: "Namespace to watch for MongoDBCommunity resources (* = all namespaces)",
- Type: ParameterType.String, Group: "General", DefaultValue: "*")
- }),
-
- ["monitoring"] = new(
- ComponentName: "monitoring",
- DisplayName: "Monitoring (kube-prometheus-stack)",
- Description: "Full observability stack — Prometheus, Alertmanager, Grafana, kube-state-metrics, node-exporter",
- Parameters: new List
- {
- new(Key: "retention", DisplayName: "Retention Period", Description: "How long Prometheus keeps metric data (e.g., 30d, 90d)",
- Type: ParameterType.String, Group: "Prometheus", DefaultValue: "30d", IsRequired: true),
- new(Key: "retentionSize", DisplayName: "Retention Size", Description: "Maximum total size of stored metrics (e.g., 45GB)",
- Type: ParameterType.String, Group: "Prometheus", DefaultValue: "45GB"),
- new(Key: "replicas", DisplayName: "Prometheus Replicas", Description: "Number of Prometheus server replicas",
- Type: ParameterType.Integer, Group: "Prometheus", DefaultValue: "2", Min: 1, Max: 10),
- new(Key: "storageSize", DisplayName: "Storage Size", Description: "PVC size for each Prometheus replica (e.g., 50Gi)",
- Type: ParameterType.String, Group: "Prometheus", DefaultValue: "50Gi"),
- new(Key: "grafanaEnabled", DisplayName: "Enable Grafana", Description: "Deploy Grafana alongside Prometheus",
- Type: ParameterType.Boolean, Group: "Grafana", DefaultValue: "true"),
- new(Key: "alertmanagerEnabled", DisplayName: "Enable Alertmanager", Description: "Deploy Alertmanager for alert routing",
- Type: ParameterType.Boolean, Group: "Alertmanager", DefaultValue: "true"),
- new(Key: "alertmanagerReplicas", DisplayName: "Alertmanager Replicas", Description: "Number of Alertmanager replicas",
- Type: ParameterType.Integer, Group: "Alertmanager", DefaultValue: "2", Min: 1, Max: 5),
-
- // ─── Ingress / Gateway API ───────────────────────────────
- new(Key: "ingressEnabled", DisplayName: "Enable Ingress", Description: "Publish monitoring services via ingress/Gateway API",
- Type: ParameterType.Boolean, Group: "Ingress", DefaultValue: "false"),
- new(Key: "ingressProvider", DisplayName: "Ingress Provider", Description: "How to expose monitoring services externally",
- Type: ParameterType.Select, Group: "Ingress", DefaultValue: "gatewayapi",
- AllowedValues: new List { "gatewayapi", "traefik", "istio" },
- DependsOnKey: "ingressEnabled", DependsOnValues: new List { "true" }),
- new(Key: "grafanaHostname", DisplayName: "Grafana Hostname", Description: "External hostname for Grafana (e.g., grafana.prod01.example.com)",
- Type: ParameterType.String, Group: "Ingress",
- DependsOnKey: "ingressEnabled", DependsOnValues: new List { "true" }),
- new(Key: "prometheusHostname", DisplayName: "Prometheus Hostname", Description: "External hostname for Prometheus (optional)",
- Type: ParameterType.String, Group: "Ingress",
- DependsOnKey: "ingressEnabled", DependsOnValues: new List { "true" }),
- new(Key: "alertmanagerHostname", DisplayName: "Alertmanager Hostname", Description: "External hostname for Alertmanager (optional)",
- Type: ParameterType.String, Group: "Ingress",
- DependsOnKey: "ingressEnabled", DependsOnValues: new List { "true" }),
- new(Key: "tlsEnabled", DisplayName: "TLS Enabled", Description: "Enable TLS for ingress routes",
- Type: ParameterType.Boolean, Group: "Ingress", DefaultValue: "true",
- DependsOnKey: "ingressEnabled", DependsOnValues: new List { "true" }),
- new(Key: "tlsSecretName", DisplayName: "TLS Secret Name", Description: "Kubernetes Secret containing the TLS certificate",
- Type: ParameterType.String, Group: "Ingress",
- DependsOnKey: "tlsEnabled", DependsOnValues: new List { "true" })
- }),
-
- ["grafana"] = new(
- ComponentName: "grafana",
- DisplayName: "Grafana",
- Description: "Visualization and dashboarding — deployed as part of the monitoring stack",
- Parameters: new List
- {
- new(Key: "enabled", DisplayName: "Enabled", Description: "Whether Grafana is deployed",
- Type: ParameterType.Boolean, Group: "General", DefaultValue: "true"),
- new(Key: "persistence", DisplayName: "Persistence", Description: "Enable persistent storage for Grafana data",
- Type: ParameterType.Boolean, Group: "Storage", DefaultValue: "false"),
- new(Key: "persistenceSize", DisplayName: "Storage Size", Description: "PVC size for Grafana persistence (e.g., 10Gi)",
- Type: ParameterType.String, Group: "Storage", DefaultValue: "10Gi"),
-
- // ─── OIDC Authentication ─────────────────────────────────
- new(Key: "oidcEnabled", DisplayName: "Enable OIDC", Description: "Authenticate users via OpenID Connect (e.g., Entra ID, Keycloak)",
- Type: ParameterType.Boolean, Group: "OIDC", DefaultValue: "false"),
- new(Key: "oidcProvider", DisplayName: "OIDC Provider", Description: "OAuth provider type (generic_oauth for most IdPs)",
- Type: ParameterType.Select, Group: "OIDC", DefaultValue: "generic_oauth",
- AllowedValues: new List { "generic_oauth", "azuread", "github", "google" },
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcClientId", DisplayName: "OIDC Client ID", Description: "Client ID from the identity provider",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcClientSecretRef", DisplayName: "Client Secret (Vault Reference)", Description: "Reference to client secret in the secrets service (e.g., vault://monitoring/grafana-oidc-secret)",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcAuthUrl", DisplayName: "Authorization URL", Description: "OIDC authorization endpoint",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcTokenUrl", DisplayName: "Token URL", Description: "OIDC token endpoint",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcApiUrl", DisplayName: "UserInfo URL", Description: "OIDC userinfo endpoint",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcScopes", DisplayName: "Scopes", Description: "OIDC scopes to request (e.g., openid profile email)",
- Type: ParameterType.String, Group: "OIDC", DefaultValue: "openid profile email",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcRoleAttributePath", DisplayName: "Role Attribute Path", Description: "JMESPath expression to map IdP claims to Grafana roles",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcAutoLogin", DisplayName: "Auto Login", Description: "Skip the Grafana login form and redirect directly to the IdP",
- Type: ParameterType.Boolean, Group: "OIDC", DefaultValue: "true",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" })
- }),
-
- ["harbor"] = new(
- ComponentName: "harbor",
- DisplayName: "Harbor",
- Description: "Container registry with vulnerability scanning, image signing, and proxy cache",
- Parameters: new List
- {
- new(Key: "harborDomain", DisplayName: "Harbor Domain", Description: "Domain name for Harbor registry access",
- Type: ParameterType.String, Group: "General", IsRequired: true),
- new(Key: "cnpgClusterName", DisplayName: "CNPG Cluster", Description: "CloudNativePG cluster to create the Harbor database on. When set, Harbor uses external PostgreSQL instead of embedded",
- Type: ParameterType.PostgresCluster, Group: "Database"),
- new(Key: "cnpgNamespace", DisplayName: "CNPG Namespace", Description: "Namespace where the CNPG cluster is running",
- Type: ParameterType.String, Group: "Database"),
- new(Key: "storageBucketId", DisplayName: "Registry Storage Bucket", Description: "Use a pre-created S3 bucket for registry image storage instead of PVC",
- Type: ParameterType.StorageBucket, Group: "Storage"),
- new(Key: "adminPassword", DisplayName: "Admin Password", Description: "Harbor admin account password",
- Type: ParameterType.String, Group: "General"),
- new(Key: "wireAllServices", DisplayName: "Wire All Services", Description: "Automatically configure all namespaces to use Harbor as image source",
- Type: ParameterType.Boolean, Group: "Integration", DefaultValue: "false"),
- new(Key: "addProxyCache", DisplayName: "Add Proxy Cache", Description: "Upstream registry URL to create a proxy cache for (e.g., docker.io)",
- Type: ParameterType.String, Group: "Proxy Cache"),
- new(Key: "proxyCacheProjectName", DisplayName: "Proxy Cache Project", Description: "Harbor project name for the proxy cache",
- Type: ParameterType.String, Group: "Proxy Cache"),
- new(Key: "setDefaultCache", DisplayName: "Set Default Cache", Description: "Set proxy cache as default for docker.io pulls",
- Type: ParameterType.Boolean, Group: "Proxy Cache", DefaultValue: "false"),
- new(Key: "createProject", DisplayName: "Create Project", Description: "Name of a new Harbor project to create",
- Type: ParameterType.String, Group: "Projects"),
- new(Key: "projectVisibility", DisplayName: "Project Visibility", Description: "Visibility for newly created projects",
- Type: ParameterType.Select, Group: "Projects", DefaultValue: "private",
- AllowedValues: new List { "public", "private" }),
- new(Key: "enableScanOnPush", DisplayName: "Scan on Push", Description: "Automatically scan images when pushed",
- Type: ParameterType.Boolean, Group: "Security", DefaultValue: "true"),
- new(Key: "severityThreshold", DisplayName: "Severity Threshold", Description: "Minimum vulnerability severity to block deployment",
- Type: ParameterType.Select, Group: "Security",
- AllowedValues: new List { "none", "low", "medium", "high", "critical" }),
- new(Key: "scanSchedule", DisplayName: "Scan Schedule", Description: "Cron expression for periodic vulnerability scanning",
- Type: ParameterType.String, Group: "Security", DefaultValue: "0 2 * * *"),
- new(Key: "createPullSecret", DisplayName: "Create Pull Secret", Description: "Create an image pull secret for Harbor access",
- Type: ParameterType.Boolean, Group: "Integration", DefaultValue: "false"),
- new(Key: "targetNamespace", DisplayName: "Target Namespace", Description: "Namespace to create the pull secret in",
- Type: ParameterType.String, Group: "Integration")
- }),
-
- ["traefik"] = new(
- ComponentName: "traefik",
- DisplayName: "Traefik Ingress Controller",
- Description: "High-performance ingress controller with automatic TLS, middleware support, and dashboard",
- Parameters: new List
- {
- new(Key: "replicas", DisplayName: "Replicas", Description: "Number of Traefik ingress controller replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 20),
- new(Key: "dashboardEnabled", DisplayName: "Dashboard", Description: "Expose the Traefik management dashboard",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "false"),
- new(Key: "serviceType", DisplayName: "Service Type", Description: "Kubernetes Service type for the Traefik LoadBalancer",
- Type: ParameterType.Select, Group: "Networking", DefaultValue: "LoadBalancer",
- AllowedValues: new List { "LoadBalancer", "NodePort", "ClusterIP" }),
- new(Key: "internalLoadBalancer", DisplayName: "Internal Load Balancer", Description: "Use cloud-provider internal LB annotations (private LB)",
- Type: ParameterType.Boolean, Group: "Networking", DefaultValue: "false")
- }),
-
- ["ingress"] = new(
- ComponentName: "ingress",
- DisplayName: "Ingress Gateway Infrastructure",
- Description: "Gateway infrastructure layer — auto-detects Istio or Traefik and deploys the appropriate load balancer resources",
- Parameters: new List
- {
- new(Key: "enable_external", DisplayName: "External Gateway", Description: "Deploy an external-facing gateway (Istio only — Traefik uses a single LB)",
- Type: ParameterType.Boolean, Group: "General", DefaultValue: "false")
- }),
-
- ["keycloak"] = new(
- ComponentName: "keycloak",
- DisplayName: "Keycloak",
- Description: "Identity and access management — SSO, user federation, social login, OIDC/SAML",
- Parameters: new List
- {
- new(Key: "keycloakDomain", DisplayName: "Domain", Description: "External domain for Keycloak access",
- Type: ParameterType.String, Group: "General", IsRequired: true),
- new(Key: "cnpgClusterName", DisplayName: "CNPG Cluster", Description: "CloudNativePG cluster to create the Keycloak database on",
- Type: ParameterType.PostgresCluster, Group: "Database"),
- new(Key: "cnpgNamespace", DisplayName: "CNPG Namespace", Description: "Namespace where the CNPG cluster is running",
- Type: ParameterType.String, Group: "Database"),
- new(Key: "createRealm", DisplayName: "Create Realm", Description: "Name of a new realm to create",
- Type: ParameterType.String, Group: "Realms"),
- new(Key: "realmDisplayName", DisplayName: "Realm Display Name", Description: "Human-readable name for the realm",
- Type: ParameterType.String, Group: "Realms"),
- new(Key: "addProvider", DisplayName: "Add Identity Provider", Description: "Type of identity provider to add",
- Type: ParameterType.Select, Group: "Identity Providers",
- AllowedValues: new List { "bankid", "oidc", "saml" }),
- new(Key: "realmName", DisplayName: "Target Realm", Description: "Realm to add the identity provider to",
- Type: ParameterType.String, Group: "Identity Providers"),
- new(Key: "bankIdEnvironment", DisplayName: "BankID Environment", Description: "BankID environment (test or production)",
- Type: ParameterType.Select, Group: "BankID",
- AllowedValues: new List { "test", "production" }),
- new(Key: "bankIdDisplayName", DisplayName: "BankID Display Name", Description: "Display name for BankID login button",
- Type: ParameterType.String, Group: "BankID", DefaultValue: "BankID"),
- new(Key: "bankIdCertificateP12", DisplayName: "BankID Certificate (Base64)", Description: "Base64-encoded PKCS#12 certificate",
- Type: ParameterType.String, Group: "BankID"),
- new(Key: "bankIdCertificatePassword", DisplayName: "Certificate Password", Description: "Password for the PKCS#12 certificate",
- Type: ParameterType.String, Group: "BankID"),
- new(Key: "oidcDiscoveryUrl", DisplayName: "OIDC Discovery URL", Description: "OpenID Connect discovery endpoint URL",
- Type: ParameterType.String, Group: "OIDC"),
- new(Key: "oidcClientId", DisplayName: "OIDC Client ID", Description: "Client ID for the OIDC provider",
- Type: ParameterType.String, Group: "OIDC"),
- new(Key: "oidcClientSecret", DisplayName: "OIDC Client Secret", Description: "Client secret for the OIDC provider",
- Type: ParameterType.String, Group: "OIDC"),
- new(Key: "samlEntityId", DisplayName: "SAML Entity ID", Description: "Entity ID of the SAML identity provider",
- Type: ParameterType.String, Group: "SAML"),
- new(Key: "samlSsoUrl", DisplayName: "SAML SSO URL", Description: "Single Sign-On URL of the SAML provider",
- Type: ParameterType.String, Group: "SAML"),
- new(Key: "samlCertificate", DisplayName: "SAML Certificate", Description: "PEM certificate for SAML signature verification",
- Type: ParameterType.String, Group: "SAML"),
- new(Key: "updateTheme", DisplayName: "Update Theme", Description: "Whether to apply theme customization",
- Type: ParameterType.Boolean, Group: "Theming", DefaultValue: "false"),
- new(Key: "themePrimaryColor", DisplayName: "Primary Color", Description: "Primary brand color (hex, e.g. #1a73e8)",
- Type: ParameterType.String, Group: "Theming"),
- new(Key: "themeSecondaryColor", DisplayName: "Secondary Color", Description: "Secondary brand color (hex)",
- Type: ParameterType.String, Group: "Theming"),
- new(Key: "themeLogo", DisplayName: "Logo URL", Description: "URL to the logo image",
- Type: ParameterType.String, Group: "Theming"),
- new(Key: "themeBackground", DisplayName: "Background URL", Description: "URL to the background image",
- Type: ParameterType.String, Group: "Theming")
- }),
-
- ["letsencrypt"] = new(
- ComponentName: "letsencrypt",
- DisplayName: "Let's Encrypt",
- Description: "Automated public TLS certificates via Let's Encrypt ACME protocol",
- Parameters: new List
- {
- new(Key: "email", DisplayName: "Registration Email", Description: "Email address for Let's Encrypt account registration",
- Type: ParameterType.String, Group: "General", IsRequired: true),
- new(Key: "httpSolverEnabled", DisplayName: "HTTP-01 Solver", Description: "Enable HTTP-01 challenge solver",
- Type: ParameterType.Boolean, Group: "HTTP-01 Solver", DefaultValue: "true"),
- new(Key: "httpSolverMode", DisplayName: "HTTP-01 Mode", Description: "How cert-manager serves HTTP-01 challenges: via traditional Ingress or Gateway API HTTPRoute",
- Type: ParameterType.Select, Group: "HTTP-01 Solver", DefaultValue: "ingress",
- AllowedValues: new List { "ingress", "gatewayHTTPRoute" },
- DependsOnKey: "httpSolverEnabled", DependsOnValues: new List { "true" }),
- new(Key: "httpSolverIngressClass", DisplayName: "Ingress Class", Description: "Ingress class for HTTP-01 challenge pods",
- Type: ParameterType.String, Group: "HTTP-01 Solver", DefaultValue: "traefik",
- DependsOnKey: "httpSolverMode", DependsOnValues: new List { "ingress" }),
- new(Key: "httpSolverGatewayName", DisplayName: "Gateway Name", Description: "Name of the Gateway resource for HTTP-01 challenges (e.g., 'internal')",
- Type: ParameterType.String, Group: "HTTP-01 Solver", DefaultValue: "internal",
- DependsOnKey: "httpSolverMode", DependsOnValues: new List { "gatewayHTTPRoute" }),
- new(Key: "httpSolverGatewayNamespace", DisplayName: "Gateway Namespace", Description: "Namespace of the Gateway resource (e.g., 'internal-ingress')",
- Type: ParameterType.String, Group: "HTTP-01 Solver",
- DependsOnKey: "httpSolverMode", DependsOnValues: new List { "gatewayHTTPRoute" }),
- new(Key: "dnsSolverEnabled", DisplayName: "DNS-01 Solver", Description: "Enable DNS-01 challenge solver for wildcard certificates and private networks",
- Type: ParameterType.Boolean, Group: "DNS-01 Solver", DefaultValue: "false"),
- new(Key: "dnsSolverProvider", DisplayName: "DNS Provider", Description: "Cloud DNS provider for DNS-01 challenges",
- Type: ParameterType.Select, Group: "DNS-01 Solver",
- AllowedValues: new List { "cloudflare", "route53", "azuredns", "clouddns" },
- DependsOnKey: "dnsSolverEnabled", DependsOnValues: new List { "true" }),
- new(Key: "dnsZones", DisplayName: "DNS Zones", Description: "Comma-separated DNS zones scoped to the DNS-01 solver (e.g., example.com, internal.example.com)",
- Type: ParameterType.String, Group: "DNS-01 Solver",
- DependsOnKey: "dnsSolverEnabled", DependsOnValues: new List { "true" }),
-
- // ─── Cloudflare-specific fields ──────────────────────────────
- new(Key: "dnsSolverSecretName", DisplayName: "API Token Secret", Description: "Kubernetes Secret containing the Cloudflare API token (key: api-token)",
- Type: ParameterType.String, Group: "Cloudflare DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "cloudflare" }),
-
- // ─── Route53-specific fields ─────────────────────────────────
- new(Key: "dnsSolverHostedZone", DisplayName: "Hosted Zone ID", Description: "AWS Route53 hosted zone ID (e.g., Z2ABCDEF123456)",
- Type: ParameterType.String, Group: "AWS Route53",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "route53" }),
-
- // ─── Azure DNS-specific fields ───────────────────────────────
- new(Key: "dnsSolverClientId", DisplayName: "Client ID", Description: "Azure AD application (service principal) client ID for DNS zone access",
- Type: ParameterType.String, Group: "Azure DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "azuredns" }),
- new(Key: "dnsSolverSubscriptionId", DisplayName: "Subscription ID", Description: "Azure subscription containing the DNS zone",
- Type: ParameterType.String, Group: "Azure DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "azuredns" }),
- new(Key: "dnsSolverTenantId", DisplayName: "Tenant ID", Description: "Azure AD tenant ID for the service principal",
- Type: ParameterType.String, Group: "Azure DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "azuredns" }),
- new(Key: "dnsSolverResourceGroup", DisplayName: "Resource Group", Description: "Azure resource group containing the DNS zone",
- Type: ParameterType.String, Group: "Azure DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "azuredns" }),
- new(Key: "dnsSolverSecretNameAzure", DisplayName: "Client Secret Name", Description: "Kubernetes Secret containing the Azure service principal client secret (key: client-secret)",
- Type: ParameterType.String, Group: "Azure DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "azuredns" }),
-
- // ─── Google Cloud DNS-specific fields ────────────────────────
- new(Key: "dnsSolverProject", DisplayName: "GCP Project", Description: "Google Cloud project ID containing the DNS zone",
- Type: ParameterType.String, Group: "Google Cloud DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "clouddns" }),
- new(Key: "dnsSolverSecretNameGcp", DisplayName: "Service Account Secret", Description: "Kubernetes Secret containing the GCP service account JSON key",
- Type: ParameterType.String, Group: "Google Cloud DNS",
- DependsOnKey: "dnsSolverProvider", DependsOnValues: new List { "clouddns" }),
-
- // ─── Management ──────────────────────────────────────────────
- new(Key: "deleteStaging", DisplayName: "Delete Staging Issuer", Description: "Remove the staging ClusterIssuer",
- Type: ParameterType.Boolean, Group: "Management", DefaultValue: "false"),
- new(Key: "deleteProduction", DisplayName: "Delete Production Issuer", Description: "Remove the production ClusterIssuer",
- Type: ParameterType.Boolean, Group: "Management", DefaultValue: "false")
- }),
-
- ["network-policies"] = new(
- ComponentName: "network-policies",
- DisplayName: "Network Policies",
- Description: "Namespace-scoped network segmentation — controls which pods can communicate",
- Parameters: new List()),
-
- ["cnpg"] = new(
- ComponentName: "cnpg",
- DisplayName: "CloudNativePG",
- Description: "PostgreSQL operator — manages highly available PostgreSQL clusters on Kubernetes",
- Parameters: new List
- {
- new(Key: "operatorReplicas", DisplayName: "Operator Replicas", Description: "Number of CNPG operator pod replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "1", Min: 1, Max: 5),
- new(Key: "monitoringEnabled", DisplayName: "Monitoring", Description: "Enable Prometheus metrics for the operator",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true"),
- new(Key: "walBucketId", DisplayName: "WAL/Backup Bucket", Description: "Use a pre-created S3 bucket for WAL archiving and base backups instead of MinIO",
- Type: ParameterType.StorageBucket, Group: "Backup Storage")
- }),
-
- ["redis"] = new(
- ComponentName: "redis",
- DisplayName: "Redis Operator",
- Description: "In-memory data store operator — manages Redis clusters with Sentinel HA",
- Parameters: new List
- {
- new(Key: "replicaCount", DisplayName: "Replica Count", Description: "Number of Redis replica pods",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "3", Min: 1, Max: 10),
- new(Key: "sentinelEnabled", DisplayName: "Sentinel HA", Description: "Enable Redis Sentinel for automatic failover",
- Type: ParameterType.Boolean, Group: "High Availability", DefaultValue: "true"),
- new(Key: "sentinelReplicas", DisplayName: "Sentinel Replicas", Description: "Number of Sentinel pods",
- Type: ParameterType.Integer, Group: "High Availability", DefaultValue: "3", Min: 1, Max: 5),
- new(Key: "maxMemory", DisplayName: "Max Memory", Description: "Maximum memory per Redis instance (e.g., 256mb, 1gb)",
- Type: ParameterType.String, Group: "Resources", DefaultValue: "256mb"),
- new(Key: "maxMemoryPolicy", DisplayName: "Eviction Policy", Description: "What happens when memory limit is reached",
- Type: ParameterType.Select, Group: "Resources", DefaultValue: "allkeys-lru",
- AllowedValues: new List { "noeviction", "allkeys-lru", "volatile-lru", "allkeys-random", "volatile-random", "volatile-ttl" }),
- new(Key: "persistence", DisplayName: "Persistence", Description: "Enable AOF persistence",
- Type: ParameterType.Boolean, Group: "Storage", DefaultValue: "true"),
- new(Key: "persistenceSize", DisplayName: "Persistence Size", Description: "PVC size for Redis persistence",
- Type: ParameterType.String, Group: "Storage", DefaultValue: "8Gi")
- }),
-
- ["external-secrets"] = new(
- ComponentName: "external-secrets",
- DisplayName: "External Secrets Operator",
- Description: "Synchronizes secrets from external providers (AWS SSM, Azure KeyVault, GCP Secret Manager, Vault)",
- Parameters: new List
- {
- new(Key: "replicas", DisplayName: "Operator Replicas", Description: "Number of ESO controller replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 5),
- new(Key: "webhookReplicas", DisplayName: "Webhook Replicas", Description: "Number of webhook server replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 5),
- new(Key: "certControllerReplicas", DisplayName: "Cert Controller Replicas", Description: "Number of cert-controller replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 1, Max: 5),
- new(Key: "storeProvider", DisplayName: "Secret Store Provider", Description: "External secret provider type",
- Type: ParameterType.Select, Group: "Secret Store",
- AllowedValues: new List { "aws", "azure", "gcp", "vault" }),
- new(Key: "storeEndpoint", DisplayName: "Store Endpoint", Description: "Provider endpoint URL (Vault server URL, etc.)",
- Type: ParameterType.String, Group: "Secret Store"),
- new(Key: "storeSecretName", DisplayName: "Auth Secret", Description: "Kubernetes secret with provider credentials",
- Type: ParameterType.String, Group: "Secret Store"),
- new(Key: "storeRegion", DisplayName: "Region", Description: "Cloud region for the secret store (AWS/Azure/GCP)",
- Type: ParameterType.String, Group: "Secret Store")
- }),
-
- ["rabbitmq"] = new(
- ComponentName: "rabbitmq",
- DisplayName: "RabbitMQ Cluster Operator",
- Description: "Message broker operator — manages RabbitMQ clusters with topology management",
- Parameters: new List
- {
- new(Key: "operatorReplicas", DisplayName: "Operator Replicas", Description: "Number of RabbitMQ operator pod replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "1", Min: 1, Max: 5),
- new(Key: "topologyOperator", DisplayName: "Topology Operator", Description: "Deploy the topology operator for declarative queue/exchange management",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true"),
- new(Key: "monitoringEnabled", DisplayName: "Monitoring", Description: "Enable Prometheus ServiceMonitor for the operator",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true")
- }),
-
- ["custom-dns"] = new(
- ComponentName: "custom-dns",
- DisplayName: "Custom DNS",
- Description: "ExternalDNS controller — automatically manages DNS records from Kubernetes resources",
- Parameters: new List
- {
- new(Key: "provider", DisplayName: "DNS Provider", Description: "Cloud DNS provider",
- Type: ParameterType.Select, Group: "General", IsRequired: true,
- AllowedValues: new List { "cloudflare", "route53", "azure", "google", "digitalocean" }),
- new(Key: "domainFilter", DisplayName: "Domain Filter", Description: "Only manage records in these domains (comma-separated)",
- Type: ParameterType.String, Group: "General"),
- new(Key: "txtOwnerId", DisplayName: "TXT Owner ID", Description: "Unique identifier for this ExternalDNS instance",
- Type: ParameterType.String, Group: "General", DefaultValue: "entkube"),
- new(Key: "policy", DisplayName: "Record Policy", Description: "How to handle existing DNS records",
- Type: ParameterType.Select, Group: "General", DefaultValue: "upsert-only",
- AllowedValues: new List { "upsert-only", "sync" }),
- new(Key: "secretName", DisplayName: "Credentials Secret", Description: "Kubernetes secret with DNS provider credentials",
- Type: ParameterType.String, Group: "Authentication")
- }),
-
- ["gitea"] = new(
- ComponentName: "gitea",
- DisplayName: "Gitea",
- Description: "Self-hosted Git service with built-in CI/CD (Gitea Actions), package registry, and container registry",
- Parameters: new List
- {
- // — Version —
- new(Key: "version", DisplayName: "Gitea Version", Description: "Helm chart version to install or upgrade to",
- Type: ParameterType.String, Group: "Version", DefaultValue: "10.6.0"),
-
- // — Scaling —
- new(Key: "replicas", DisplayName: "Replicas", Description: "Number of Gitea application replicas",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "1", Min: 1, Max: 5),
- new(Key: "runnerReplicas", DisplayName: "Runner Replicas", Description: "Number of Gitea Actions runner pods",
- Type: ParameterType.Integer, Group: "Scaling", DefaultValue: "2", Min: 0, Max: 20),
- new(Key: "runnerStorage", DisplayName: "Runner Storage", Description: "PVC size for each runner pod cache",
- Type: ParameterType.String, Group: "Scaling", DefaultValue: "2Gi"),
-
- // — Features —
- new(Key: "actionsEnabled", DisplayName: "Gitea Actions", Description: "Enable built-in CI/CD with Gitea Actions",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true"),
- new(Key: "projectsEnabled", DisplayName: "Projects", Description: "Enable project boards for issue/task management",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true"),
- new(Key: "sshEnabled", DisplayName: "SSH Access", Description: "Enable Git over SSH",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true"),
- new(Key: "sshPort", DisplayName: "SSH Port", Description: "SSH service port number",
- Type: ParameterType.Integer, Group: "Features", DefaultValue: "22", Min: 1, Max: 65535,
- DependsOnKey: "sshEnabled", DependsOnValues: new List { "true" }),
- new(Key: "metricsEnabled", DisplayName: "Metrics", Description: "Enable Prometheus metrics endpoint and ServiceMonitor",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true"),
- new(Key: "disableRegistration", DisplayName: "Disable Registration", Description: "Prevent self-registration (use OIDC instead)",
- Type: ParameterType.Boolean, Group: "Features", DefaultValue: "true"),
-
- // — Storage —
- new(Key: "persistenceSize", DisplayName: "Storage Size", Description: "PVC size for Git repository storage",
- Type: ParameterType.String, Group: "Storage", DefaultValue: "50Gi"),
-
- // — Ingress (selectors, not textboxes) —
- new(Key: "ingressHost", DisplayName: "Ingress Host", Description: "Hostname for Gitea web UI (leave empty to skip ingress)",
- Type: ParameterType.String, Group: "Ingress"),
- new(Key: "ingressProvider", DisplayName: "Ingress Provider", Description: "Ingress controller type",
- Type: ParameterType.Select, Group: "Ingress", DefaultValue: "traefik",
- AllowedValues: new List { "traefik", "istio", "gatewayapi" }),
-
- // — Database (CNPG cluster selector — hides manual fields when selected) —
- new(Key: "cnpgClusterName", DisplayName: "CNPG Cluster", Description: "CloudNativePG cluster to create the Gitea database on. Credentials are auto-generated and stored in the secrets vault",
- Type: ParameterType.PostgresCluster, Group: "Database"),
- new(Key: "cnpgNamespace", DisplayName: "CNPG Namespace", Description: "Namespace where the CNPG cluster is running (auto-filled when CNPG cluster is selected)",
- Type: ParameterType.String, Group: "Database"),
- new(Key: "dbName", DisplayName: "Database Name", Description: "PostgreSQL database name to create on the CNPG cluster",
- Type: ParameterType.String, Group: "Database", DefaultValue: "gitea"),
-
- // — Redis (cluster selector) —
- new(Key: "redisClusterName", DisplayName: "Redis Cluster", Description: "Redis cluster for cache, sessions, and queues. Falls back to in-memory if not selected",
- Type: ParameterType.RedisCluster, Group: "Cache"),
-
- // — OIDC —
- new(Key: "oidcEnabled", DisplayName: "OIDC Authentication", Description: "Enable OpenID Connect authentication (e.g. via Keycloak)",
- Type: ParameterType.Boolean, Group: "OIDC", DefaultValue: "false"),
- new(Key: "oidcProviderName", DisplayName: "Provider Name", Description: "Display name shown on the login button (e.g. 'Keycloak', 'Azure AD')",
- Type: ParameterType.String, Group: "OIDC", DefaultValue: "Keycloak",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcDiscoveryUrl", DisplayName: "Discovery URL", Description: "OpenID Connect discovery endpoint (e.g. https://keycloak.example.com/realms/myrealm)",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcClientId", DisplayName: "Client ID", Description: "OIDC client ID registered in the identity provider",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcClientSecret", DisplayName: "Client Secret", Description: "OIDC client secret",
- Type: ParameterType.String, Group: "OIDC",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcScopes", DisplayName: "Scopes", Description: "OIDC scopes to request",
- Type: ParameterType.String, Group: "OIDC", DefaultValue: "openid profile email",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
- new(Key: "oidcAutoDiscoverUrl", DisplayName: "Auto Discovery", Description: "Use the discovery URL to auto-configure endpoints",
- Type: ParameterType.Boolean, Group: "OIDC", DefaultValue: "true",
- DependsOnKey: "oidcEnabled", DependsOnValues: new List { "true" }),
-
- // — Integration (Harbor auto-detect) —
- new(Key: "harborRegistryUrl", DisplayName: "Harbor Registry URL", Description: "Harbor registry URL for act-runner Docker mirror. Auto-detected from installed Harbor if available",
- Type: ParameterType.String, Group: "Integration"),
-
- // — Object Storage (cascading — selecting a bucket hides manual S3/Azure fields) —
- new(Key: "objectStorageType", DisplayName: "Object Storage", Description: "How Gitea stores LFS objects, packages, and attachments",
- Type: ParameterType.Select, Group: "Object Storage", DefaultValue: "none",
- AllowedValues: new List { "none", "bucket", "s3", "azure" }),
- new(Key: "storageBucketId", DisplayName: "Storage Bucket", Description: "Use a pre-created storage bucket from the Storage tab",
- Type: ParameterType.StorageBucket, Group: "Object Storage",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "bucket" }),
- new(Key: "s3Endpoint", DisplayName: "S3 Endpoint", Description: "S3-compatible endpoint for object storage",
- Type: ParameterType.String, Group: "Object Storage",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "s3" }),
- new(Key: "s3AccessKey", DisplayName: "S3 Access Key", Description: "S3 access key ID",
- Type: ParameterType.String, Group: "Object Storage",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "s3" }),
- new(Key: "s3SecretKey", DisplayName: "S3 Secret Key", Description: "S3 secret access key",
- Type: ParameterType.String, Group: "Object Storage",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "s3" }),
- new(Key: "s3Bucket", DisplayName: "S3 Bucket", Description: "S3 bucket name for Gitea storage",
- Type: ParameterType.String, Group: "Object Storage", DefaultValue: "gitea",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "s3" }),
- new(Key: "s3Region", DisplayName: "S3 Region", Description: "S3 region",
- Type: ParameterType.String, Group: "Object Storage",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "s3" }),
- new(Key: "azureAccountName", DisplayName: "Azure Account Name", Description: "Azure Storage account name",
- Type: ParameterType.String, Group: "Object Storage",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "azure" }),
- new(Key: "azureAccountKey", DisplayName: "Azure Account Key", Description: "Azure Storage account key",
- Type: ParameterType.String, Group: "Object Storage",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "azure" }),
- new(Key: "azureContainer", DisplayName: "Azure Container", Description: "Azure Blob container name",
- Type: ParameterType.String, Group: "Object Storage", DefaultValue: "gitea",
- DependsOnKey: "objectStorageType", DependsOnValues: new List { "azure" }),
-
- // — Administration —
- new(Key: "adminUsername", DisplayName: "Admin Username", Description: "Initial admin user (only used on first install)",
- Type: ParameterType.String, Group: "Administration", DefaultValue: "gitea_admin"),
- new(Key: "adminEmail", DisplayName: "Admin Email", Description: "Admin user email address",
- Type: ParameterType.String, Group: "Administration")
- })
- };
-
- ///
- /// Retrieves the configuration schema for a component by name. Returns an
- /// empty schema if the component has no registered parameters.
- ///
- public static ConfigurationSchema GetSchema(string componentName)
- {
- if (schemas.TryGetValue(componentName, out ConfigurationSchema? schema))
- {
- return schema;
- }
-
- return new ConfigurationSchema(
- ComponentName: componentName,
- DisplayName: componentName,
- Description: $"No configuration schema defined for '{componentName}'",
- Parameters: new List());
- }
-
- ///
- /// Returns all registered schemas for use in API responses.
- ///
- public static IReadOnlyDictionary GetAll() => schemas;
-
- ///
- /// Returns a schema enriched with cluster-contextual defaults. Uses the
- /// cluster's discovered component state (from adoption scans) to adjust
- /// defaults so the configuration form pre-selects the right options.
- ///
- /// For Let's Encrypt:
- /// - Detects the ingress provider (Istio vs Traefik) from the ingress component
- /// - Sets httpSolverMode default to "gatewayHTTPRoute" when Istio is detected
- /// - Sets httpSolverMode default to "ingress" when Traefik is detected
- /// - Pre-fills gateway name/namespace from discovered gateways
- /// - Converts gateway name to a Select with discovered options when available
- ///
- public static ConfigurationSchema EnrichForCluster(
- string componentName, IReadOnlyList clusterComponents)
- {
- ConfigurationSchema baseSchema = GetSchema(componentName);
-
- if (componentName.Equals("ingress", StringComparison.OrdinalIgnoreCase))
- {
- return EnrichIngressSchema(baseSchema, clusterComponents);
- }
-
- if (componentName.Equals("letsencrypt", StringComparison.OrdinalIgnoreCase))
- {
- return EnrichLetsEncryptSchema(baseSchema, clusterComponents);
- }
-
- return baseSchema;
- }
-
- ///
- /// Enriches the ingress schema based on the cluster's discovered state.
- /// When the cluster scan found an external gateway, the enable_external
- /// parameter defaults to true so the toggle reflects reality.
- ///
- private static ConfigurationSchema EnrichIngressSchema(
- ConfigurationSchema baseSchema, IReadOnlyList clusterComponents)
- {
- // Look up the ingress component's discovered configuration to see
- // whether an external gateway was found during the cluster scan.
-
- ClusterComponent? ingressComponent = clusterComponents.FirstOrDefault(
- c => c.ComponentName.Equals("ingress", StringComparison.OrdinalIgnoreCase));
-
- bool hasExternalGateway = ingressComponent?.Configuration is not null
- && ingressComponent.Configuration.TryGetValue("hasExternalGateway", out string? extGw)
- && extGw.Equals("True", StringComparison.OrdinalIgnoreCase);
-
- if (!hasExternalGateway)
- {
- return baseSchema;
- }
-
- // The cluster already has an external gateway — flip the default so
- // the UI toggle shows "Enabled" instead of contradicting the current state.
-
- List enrichedParams = baseSchema.Parameters
- .Select(p => p.Key == "enable_external" ? p with { DefaultValue = "true" } : p)
- .ToList();
-
- return baseSchema with { Parameters = enrichedParams };
- }
-
- ///
- /// Enriches the Let's Encrypt schema based on the cluster's ingress provider
- /// and discovered gateways. When Istio is detected, the HTTP-01 solver form
- /// defaults to Gateway API mode. When Traefik is detected, it defaults to
- /// ingress mode with the traefik class pre-selected.
- ///
- private static ConfigurationSchema EnrichLetsEncryptSchema(
- ConfigurationSchema baseSchema, IReadOnlyList clusterComponents)
- {
- // Find the ingress component to determine the provider type.
-
- ClusterComponent? ingressComponent = clusterComponents.FirstOrDefault(
- c => c.ComponentName.Equals("ingress", StringComparison.OrdinalIgnoreCase));
-
- string? ingressProvider = null;
- bool hasInternalGateway = false;
- bool hasExternalGateway = false;
-
- if (ingressComponent?.Configuration is not null)
- {
- ingressComponent.Configuration.TryGetValue("provider", out ingressProvider);
-
- hasInternalGateway = ingressComponent.Configuration.TryGetValue("hasInternalGateway", out string? intGw)
- && intGw.Equals("True", StringComparison.OrdinalIgnoreCase);
-
- hasExternalGateway = ingressComponent.Configuration.TryGetValue("hasExternalGateway", out string? extGw)
- && extGw.Equals("True", StringComparison.OrdinalIgnoreCase);
- }
-
- // Find discovered gateways from the letsencrypt component's config.
-
- ClusterComponent? letsEncryptComponent = clusterComponents.FirstOrDefault(
- c => c.ComponentName.Equals("letsencrypt", StringComparison.OrdinalIgnoreCase));
-
- List discoveredGateways = ParseDiscoveredGateways(letsEncryptComponent);
-
- // If no gateways from the letsencrypt check, build from ingress info.
-
- if (discoveredGateways.Count == 0)
- {
- if (hasInternalGateway)
- {
- discoveredGateways.Add(new DiscoveredGateway("internal", "internal-ingress"));
- }
-
- if (hasExternalGateway)
- {
- discoveredGateways.Add(new DiscoveredGateway("external", "external-ingress"));
- }
- }
-
- // Build enriched parameters with adjusted defaults.
-
- bool isIstio = string.Equals(ingressProvider, "istio", StringComparison.OrdinalIgnoreCase);
- bool isTraefik = string.Equals(ingressProvider, "traefik", StringComparison.OrdinalIgnoreCase);
-
- List enrichedParams = baseSchema.Parameters
- .Select(p => EnrichLetsEncryptParameter(p, isIstio, isTraefik, discoveredGateways))
- .ToList();
-
- return baseSchema with { Parameters = enrichedParams };
- }
-
- ///
- /// Adjusts a single Let's Encrypt parameter based on the detected provider.
- /// Sets appropriate defaults for Istio (gateway mode) or Traefik (ingress mode)
- /// and converts gateway name to a Select when gateways are discovered.
- ///
- private static ConfigurationParameter EnrichLetsEncryptParameter(
- ConfigurationParameter param, bool isIstio, bool isTraefik,
- List discoveredGateways)
- {
- return param.Key switch
- {
- // When Istio is detected, default to Gateway API mode.
-
- "httpSolverMode" when isIstio => param with { DefaultValue = "gatewayHTTPRoute" },
-
- // When Istio and gateways are discovered, show them as Select options.
-
- "httpSolverGatewayName" when isIstio && discoveredGateways.Count > 0 =>
- param with
- {
- Type = ParameterType.Select,
- DefaultValue = discoveredGateways[0].Name,
- AllowedValues = discoveredGateways.Select(g => g.Name).ToList()
- },
-
- // Default the gateway namespace to the first discovered gateway's namespace.
-
- "httpSolverGatewayNamespace" when isIstio && discoveredGateways.Count > 0 =>
- param with { DefaultValue = discoveredGateways[0].Namespace },
-
- // When Traefik, ensure ingress class defaults to "traefik".
-
- "httpSolverIngressClass" when isTraefik => param with { DefaultValue = "traefik" },
-
- _ => param
- };
- }
-
- ///
- /// Parses the availableGateways JSON from the letsencrypt component's
- /// discovered configuration. The JSON is an array of {name, namespace} objects
- /// stored by the LetsEncryptCheck during gateway discovery.
- ///
- private static List ParseDiscoveredGateways(ClusterComponent? component)
- {
- List gateways = new();
-
- if (component?.Configuration is null)
- {
- return gateways;
- }
-
- if (!component.Configuration.TryGetValue("availableGateways", out string? json)
- || string.IsNullOrEmpty(json))
- {
- return gateways;
- }
-
- try
- {
- using System.Text.Json.JsonDocument doc = System.Text.Json.JsonDocument.Parse(json);
-
- foreach (System.Text.Json.JsonElement element in doc.RootElement.EnumerateArray())
- {
- string? name = element.TryGetProperty("name", out System.Text.Json.JsonElement nameEl)
- ? nameEl.GetString() : null;
- string? ns = element.TryGetProperty("namespace", out System.Text.Json.JsonElement nsEl)
- ? nsEl.GetString() : null;
-
- if (!string.IsNullOrEmpty(name))
- {
- gateways.Add(new DiscoveredGateway(name, ns ?? "default"));
- }
- }
- }
- catch
- {
- // Malformed JSON — ignore and return empty list.
- }
-
- return gateways;
- }
-
- private record DiscoveredGateway(string Name, string Namespace);
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/CertManager/CertManagerCheck.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/CertManager/CertManagerCheck.cs
deleted file mode 100644
index 882c479..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/CertManager/CertManagerCheck.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-using System.Text;
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.CertManager;
-
-///
-/// Checks whether cert-manager is installed and healthy on the cluster.
-/// Cert-manager is a prerequisite for internal CA management, TLS certificate
-/// issuance, and trust bundle distribution. The Terraform bootstrap module
-/// deploys it as one of the first components.
-///
-public class CertManagerCheck : IAdoptionCheck
-{
- private readonly ILogger logger;
-
- public string ComponentName => "cert-manager";
- public string DisplayName => "cert-manager (Certificate Management)";
-
- public CertManagerCheck(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task CheckAsync(KubernetesCluster cluster, CancellationToken ct = default)
- {
- Kubernetes client = BuildClient(cluster);
- List details = new();
- List missing = new();
-
- // Look for cert-manager pods in the standard namespace.
-
- List runningPods = await FindCertManagerPods(client, ct);
-
- if (runningPods.Count == 0)
- {
- missing.Add("No cert-manager pods found in cert-manager namespace");
- return new ComponentCheckResult(ComponentName, ComponentStatus.NotInstalled, details, missing);
- }
-
- details.AddRange(runningPods.Select(p => $"Pod running: {p}"));
-
- // Check for the key components: controller, webhook, cainjector.
-
- bool hasController = runningPods.Any(p => p.Contains("cert-manager") && !p.Contains("webhook") && !p.Contains("cainjector"));
- bool hasWebhook = runningPods.Any(p => p.Contains("webhook"));
- bool hasCainjector = runningPods.Any(p => p.Contains("cainjector"));
-
- if (!hasController)
- {
- missing.Add("cert-manager controller pod not found");
- }
-
- if (!hasWebhook)
- {
- missing.Add("cert-manager webhook pod not found");
- }
-
- if (!hasCainjector)
- {
- missing.Add("cert-manager cainjector pod not found");
- }
-
- // Also check if the Certificate CRD exists (confirms CRDs are installed).
-
- bool crdExists = await CertificateCrdExists(client, ct);
-
- if (!crdExists)
- {
- missing.Add("Certificate CRD (certificates.cert-manager.io) not found");
- }
- else
- {
- details.Add("Certificate CRD present");
- }
-
- // Build discovered configuration from what we found on the cluster.
-
- string? version = await GetVersionFromPods(client, ct);
-
- DiscoveredConfiguration config = new(
- Version: version,
- Namespace: "cert-manager",
- HelmReleaseName: "cert-manager",
- Values: new Dictionary
- {
- ["controllerReplicas"] = runningPods.Count(p => p.Contains("cert-manager") && !p.Contains("webhook") && !p.Contains("cainjector")).ToString(),
- ["hasWebhook"] = hasWebhook.ToString(),
- ["hasCainjector"] = hasCainjector.ToString()
- });
-
- if (missing.Count > 0)
- {
- return new ComponentCheckResult(ComponentName, ComponentStatus.Degraded, details, missing, config);
- }
-
- return new ComponentCheckResult(ComponentName, ComponentStatus.Installed, details, missing, config);
- }
-
- private async Task GetVersionFromPods(Kubernetes client, CancellationToken ct)
- {
- try
- {
- // List all pods in the cert-manager namespace — the namespace is
- // dedicated to cert-manager, so no label filtering needed. This
- // catches all components (controller, webhook, cainjector) regardless
- // of Helm release name or label configuration.
-
- V1PodList podList = await client.CoreV1.ListNamespacedPodAsync(
- "cert-manager",
- cancellationToken: ct);
-
- foreach (V1Pod pod in podList.Items)
- {
- if (pod.Spec?.Containers?.Count > 0)
- {
- string? image = pod.Spec.Containers[0].Image;
-
- if (image?.Contains(':') == true)
- {
- return image.Split(':').Last().TrimStart('v');
- }
- }
- }
- }
- catch { }
-
- return null;
- }
-
- private async Task> FindCertManagerPods(Kubernetes client, CancellationToken ct)
- {
- List pods = new();
-
- try
- {
- // List all running pods in the cert-manager namespace. Since this
- // namespace is dedicated to cert-manager, we don't need to filter
- // by labels — any running pod here is a cert-manager component.
-
- V1PodList podList = await client.CoreV1.ListNamespacedPodAsync(
- "cert-manager",
- cancellationToken: ct);
-
- foreach (V1Pod pod in podList.Items)
- {
- if (pod.Status?.Phase == "Running")
- {
- pods.Add(pod.Metadata.Name);
- }
- }
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- logger.LogDebug("cert-manager namespace not found on cluster");
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Error checking for cert-manager pods");
- }
-
- return pods;
- }
-
- private async Task CertificateCrdExists(Kubernetes client, CancellationToken ct)
- {
- try
- {
- await client.ApiextensionsV1.ReadCustomResourceDefinitionAsync(
- "certificates.cert-manager.io", cancellationToken: ct);
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private static Kubernetes BuildClient(KubernetesCluster cluster)
- {
- byte[] kubeConfigBytes = Encoding.UTF8.GetBytes(cluster.KubeConfig);
- using MemoryStream stream = new(kubeConfigBytes);
- KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream, cluster.ContextName);
- return new Kubernetes(config);
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/CertManager/CertManagerInstaller.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/CertManager/CertManagerInstaller.cs
deleted file mode 100644
index 260d7e0..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/CertManager/CertManagerInstaller.cs
+++ /dev/null
@@ -1,270 +0,0 @@
-using System.Text;
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.CertManager;
-
-///
-/// Installs cert-manager via Helm. cert-manager provides the CRD runtime for
-/// Certificate, Issuer, and ClusterIssuer resources. Let's Encrypt ClusterIssuer
-/// configuration is handled separately by the LetsEncryptInstaller.
-///
-/// What gets deployed:
-/// 1. cert-manager namespace with controller, webhook, and cainjector
-/// 2. CRDs for Certificate, Issuer, ClusterIssuer, etc.
-///
-/// Configuration keys:
-/// - "replicas": cert-manager controller replicas (default: 1)
-///
-public class CertManagerInstaller : IComponentInstaller
-{
- private readonly ILogger logger;
-
- private const string DefaultVersion = "1.16.3";
- private const string DefaultNamespace = "cert-manager";
- private const string HelmRepo = "https://charts.jetstack.io";
-
- public string ComponentName => "cert-manager";
-
- public CertManagerInstaller(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task InstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
- {
- string version = options.Version ?? DefaultVersion;
- string targetNamespace = options.Namespace ?? DefaultNamespace;
- List actions = new();
-
- string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-certmgr-{Guid.NewGuid()}.kubeconfig");
-
- try
- {
- await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
-
- // Ensure the namespace exists.
-
- Kubernetes client = BuildClient(cluster);
- await EnsureNamespace(client, targetNamespace, ct);
- actions.Add($"Ensured namespace '{targetNamespace}'");
-
- // Install cert-manager with CRDs via Helm.
-
- await RunCommand("helm",
- $"upgrade --install cert-manager cert-manager " +
- $"--repo {HelmRepo} --version v{version} " +
- $"--namespace {targetNamespace} " +
- $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
- $"--set crds.enabled=true " +
- $"--set replicaCount=1 " +
- $"--set webhook.replicaCount=1 " +
- $"--set cainjector.replicaCount=1 " +
- $"--set resources.requests.cpu=50m " +
- $"--set resources.requests.memory=128Mi " +
- $"--set resources.limits.cpu=200m " +
- $"--set resources.limits.memory=256Mi " +
- $"--set prometheus.enabled=true " +
- $"--set prometheus.servicemonitor.enabled=false " +
- $"--wait --timeout 10m",
- ct);
- actions.Add($"Helm install cert-manager v{version} with CRDs");
-
- logger.LogInformation("cert-manager {Version} installed on cluster {Cluster}",
- version, cluster.Name);
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: $"cert-manager {version} installed",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to install cert-manager on cluster {Cluster}", cluster.Name);
- actions.Add($"Error: {ex.Message}");
-
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to install cert-manager: {ex.Message}",
- Actions: actions);
- }
- finally
- {
- if (File.Exists(tempKubeConfig))
- {
- File.Delete(tempKubeConfig);
- }
- }
- }
-
- ///
- /// Reconfigures cert-manager. Supports:
- /// - "replicas": controller replicas
- ///
- public async Task ConfigureAsync(KubernetesCluster cluster, ComponentConfiguration configuration, CancellationToken ct = default)
- {
- string targetNamespace = configuration.Namespace ?? DefaultNamespace;
- List actions = new();
-
- string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-certmgr-cfg-{Guid.NewGuid()}.kubeconfig");
-
- try
- {
- await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
-
- // Handle Helm-level configuration (replicas, resources).
-
- List setFlags = new();
-
- if (configuration.Values.TryGetValue("replicas", out string? replicas))
- {
- setFlags.Add($"--set replicaCount={replicas}");
- }
-
- if (setFlags.Count > 0)
- {
- await RunCommand("helm",
- $"upgrade cert-manager cert-manager --repo {HelmRepo} " +
- $"--namespace {targetNamespace} " +
- $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
- $"--reuse-values {string.Join(" ", setFlags)} --wait --timeout 5m",
- ct);
- actions.Add($"Updated cert-manager Helm release: {string.Join(", ", setFlags)}");
- }
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: "cert-manager reconfigured",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to reconfigure cert-manager on cluster {Cluster}", cluster.Name);
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to reconfigure cert-manager: {ex.Message}",
- Actions: new List { $"Error: {ex.Message}" });
- }
- finally
- {
- if (File.Exists(tempKubeConfig))
- {
- File.Delete(tempKubeConfig);
- }
- }
- }
-
- ///
- /// Uninstalls cert-manager from the cluster by removing the Helm release.
- /// Warning: this removes all CRDs too — any Certificate, Issuer, and
- /// ClusterIssuer resources on the cluster will be deleted.
- ///
- public async Task UninstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
- {
- string targetNamespace = options.Namespace ?? DefaultNamespace;
- List actions = new();
-
- string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-certmgr-rm-{Guid.NewGuid()}.kubeconfig");
-
- try
- {
- await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
-
- await RunCommand("helm",
- $"uninstall cert-manager --namespace {targetNamespace} " +
- $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
- $"--wait --timeout 5m",
- ct);
- actions.Add("Helm uninstall cert-manager");
-
- logger.LogInformation("cert-manager uninstalled from cluster {Cluster}", cluster.Name);
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: "cert-manager uninstalled successfully",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to uninstall cert-manager from cluster {Cluster}", cluster.Name);
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to uninstall cert-manager: {ex.Message}",
- Actions: new List { $"Error: {ex.Message}" });
- }
- finally
- {
- if (File.Exists(tempKubeConfig))
- {
- File.Delete(tempKubeConfig);
- }
- }
- }
-
- private async Task EnsureNamespace(Kubernetes client, string namespaceName, CancellationToken ct)
- {
- try
- {
- await client.CoreV1.ReadNamespaceAsync(namespaceName, cancellationToken: ct);
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- V1Namespace ns = new()
- {
- Metadata = new V1ObjectMeta
- {
- Name = namespaceName,
- Labels = new Dictionary
- {
- ["app.kubernetes.io/managed-by"] = "entkube"
- }
- }
- };
- await client.CoreV1.CreateNamespaceAsync(ns, cancellationToken: ct);
- }
- }
-
- private async Task RunCommand(string fileName, string arguments, CancellationToken ct)
- {
- System.Diagnostics.ProcessStartInfo psi = new(fileName, arguments)
- {
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false
- };
-
- using System.Diagnostics.Process? process = System.Diagnostics.Process.Start(psi);
-
- if (process is null)
- {
- throw new InvalidOperationException($"Failed to start {fileName}");
- }
-
- string output = await process.StandardOutput.ReadToEndAsync(ct);
- string error = await process.StandardError.ReadToEndAsync(ct);
- await process.WaitForExitAsync(ct);
-
- if (process.ExitCode != 0)
- {
- throw new InvalidOperationException($"{fileName} failed (exit {process.ExitCode}): {error}");
- }
-
- return output;
- }
-
- private static Kubernetes BuildClient(KubernetesCluster cluster)
- {
- byte[] kubeConfigBytes = Encoding.UTF8.GetBytes(cluster.KubeConfig);
- using MemoryStream stream = new(kubeConfigBytes);
- KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream, cluster.ContextName);
- return new Kubernetes(config);
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/CloudNativePG/CloudNativePGCheck.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/CloudNativePG/CloudNativePGCheck.cs
deleted file mode 100644
index 36793a1..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/CloudNativePG/CloudNativePGCheck.cs
+++ /dev/null
@@ -1,201 +0,0 @@
-using System.Text;
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.CloudNativePG;
-
-///
-/// Checks whether CloudNativePG (CNPG) operator is installed and healthy on the cluster.
-/// CNPG is the Kubernetes operator for managing PostgreSQL databases natively.
-/// It handles provisioning, failover, backup, and point-in-time recovery.
-///
-/// This check verifies:
-/// 1. CNPG operator pods running in cnpg-system namespace
-/// 2. Cluster CRD (clusters.postgresql.cnpg.io) is registered
-/// 3. Any existing PostgreSQL clusters managed by the operator
-///
-public class CloudNativePGCheck : IAdoptionCheck
-{
- private readonly ILogger logger;
-
- public string ComponentName => "cnpg";
- public string DisplayName => "CloudNativePG (PostgreSQL Operator)";
-
- public CloudNativePGCheck(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task CheckAsync(KubernetesCluster cluster, CancellationToken ct = default)
- {
- Kubernetes client = BuildClient(cluster);
- List details = new();
- List missing = new();
-
- // Check 1: CNPG operator pods in cnpg-system namespace.
-
- List operatorPods = await FindOperatorPods(client, ct);
-
- if (operatorPods.Count > 0)
- {
- details.AddRange(operatorPods.Select(p => $"Operator pod: {p}"));
- }
- else
- {
- missing.Add("No CNPG operator pods found in cnpg-system namespace");
- return new ComponentCheckResult(ComponentName, ComponentStatus.NotInstalled, details, missing);
- }
-
- // Check 2: Cluster CRD exists (confirms operator CRDs installed).
-
- bool crdExists = await ClusterCrdExists(client, ct);
-
- if (crdExists)
- {
- details.Add("Cluster CRD (clusters.postgresql.cnpg.io) present");
- }
- else
- {
- missing.Add("Cluster CRD (clusters.postgresql.cnpg.io) not found");
- }
-
- // Check 3: Discover existing PostgreSQL clusters across all namespaces.
-
- int clusterCount = await CountPostgresClusters(client, ct);
- details.Add($"Managed PostgreSQL clusters: {clusterCount}");
-
- // Discover operator version from the controller pod image tag.
-
- string? version = await GetVersionFromPods(client, ct);
-
- DiscoveredConfiguration config = new(
- Version: version,
- Namespace: "cnpg-system",
- HelmReleaseName: "cnpg",
- Values: new Dictionary
- {
- ["operatorReplicas"] = operatorPods.Count.ToString(),
- ["managedClusters"] = clusterCount.ToString(),
- ["crdInstalled"] = crdExists.ToString()
- });
-
- if (missing.Count > 0)
- {
- return new ComponentCheckResult(ComponentName, ComponentStatus.Degraded, details, missing, config);
- }
-
- return new ComponentCheckResult(ComponentName, ComponentStatus.Installed, details, missing, config);
- }
-
- private async Task GetVersionFromPods(Kubernetes client, CancellationToken ct)
- {
- try
- {
- V1PodList podList = await client.CoreV1.ListNamespacedPodAsync(
- "cnpg-system",
- labelSelector: "app.kubernetes.io/name=cloudnative-pg",
- cancellationToken: ct);
-
- foreach (V1Pod pod in podList.Items)
- {
- if (pod.Spec?.Containers?.Count > 0)
- {
- string? image = pod.Spec.Containers[0].Image;
-
- if (image?.Contains(':') == true)
- {
- return image.Split(':').Last().TrimStart('v');
- }
- }
- }
- }
- catch { }
-
- return null;
- }
-
- private async Task> FindOperatorPods(Kubernetes client, CancellationToken ct)
- {
- List pods = new();
-
- try
- {
- V1PodList podList = await client.CoreV1.ListNamespacedPodAsync(
- "cnpg-system",
- labelSelector: "app.kubernetes.io/name=cloudnative-pg",
- cancellationToken: ct);
-
- foreach (V1Pod pod in podList.Items)
- {
- if (pod.Status?.Phase == "Running")
- {
- pods.Add(pod.Metadata.Name);
- }
- }
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- logger.LogDebug("cnpg-system namespace not found on cluster");
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Error checking for CNPG operator pods");
- }
-
- return pods;
- }
-
- private async Task ClusterCrdExists(Kubernetes client, CancellationToken ct)
- {
- try
- {
- await client.ApiextensionsV1.ReadCustomResourceDefinitionAsync(
- "clusters.postgresql.cnpg.io", cancellationToken: ct);
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private async Task CountPostgresClusters(Kubernetes client, CancellationToken ct)
- {
- try
- {
- object result = await client.CustomObjects.ListClusterCustomObjectAsync(
- group: "postgresql.cnpg.io",
- version: "v1",
- plural: "clusters",
- cancellationToken: ct);
-
- // Serialize then parse — the k8s client can return different runtime
- // types (JsonElement, JObject, Dictionary) depending on configuration.
-
- string json = System.Text.Json.JsonSerializer.Serialize(result);
-
- using System.Text.Json.JsonDocument doc = System.Text.Json.JsonDocument.Parse(json);
-
- if (doc.RootElement.TryGetProperty("items", out System.Text.Json.JsonElement items))
- {
- return items.GetArrayLength();
- }
-
- return 0;
- }
- catch
- {
- return 0;
- }
- }
-
- private static Kubernetes BuildClient(KubernetesCluster cluster)
- {
- byte[] kubeConfigBytes = Encoding.UTF8.GetBytes(cluster.KubeConfig);
- using MemoryStream stream = new(kubeConfigBytes);
- KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream, cluster.ContextName);
- return new Kubernetes(config);
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/CloudNativePG/CloudNativePGInstaller.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/CloudNativePG/CloudNativePGInstaller.cs
deleted file mode 100644
index d2b8099..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/CloudNativePG/CloudNativePGInstaller.cs
+++ /dev/null
@@ -1,353 +0,0 @@
-using System.Text;
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.CloudNativePG;
-
-///
-/// Installs CloudNativePG (CNPG) operator via Helm. The operator manages the full
-/// lifecycle of PostgreSQL databases on Kubernetes — provisioning, HA failover,
-/// automated backups, point-in-time recovery, and rolling updates.
-///
-/// What gets deployed:
-/// 1. cnpg-system namespace with the CNPG operator controller
-/// 2. All required CRDs (Cluster, Backup, ScheduledBackup, Pooler, etc.)
-/// 3. Webhook configuration for validating/mutating PostgreSQL clusters
-///
-/// Configuration keys:
-/// - "operatorReplicas": Number of operator controller replicas (default: 1)
-/// - "monitoringEnabled": Whether to create ServiceMonitor for Prometheus scraping
-///
-/// Note: This installs the OPERATOR only. Actual PostgreSQL clusters are provisioned
-/// separately via the Provisioning service (per-tenant databases).
-///
-public class CloudNativePGInstaller : IComponentInstaller
-{
- private readonly ILogger logger;
-
- private const string DefaultVersion = "0.22.0";
- private const string DefaultNamespace = "cnpg-system";
- private const string HelmRepo = "https://cloudnative-pg.github.io/charts";
-
- public string ComponentName => "cnpg";
-
- public CloudNativePGInstaller(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task InstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
- {
- string version = options.Version ?? DefaultVersion;
- string targetNamespace = options.Namespace ?? DefaultNamespace;
- bool monitoringEnabled = options.Parameters?.TryGetValue("monitoringEnabled", out string? monVal) == true
- && monVal == "true";
- List actions = new();
-
- string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-cnpg-{Guid.NewGuid()}.kubeconfig");
-
- try
- {
- await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
-
- // Ensure the namespace exists with management labels.
-
- Kubernetes client = BuildClient(cluster);
- await EnsureNamespaceWithLabels(client, targetNamespace, ct);
- actions.Add($"Ensured namespace '{targetNamespace}' with management labels");
-
- // Install CNPG operator via Helm. The chart includes all CRDs,
- // the controller deployment, webhook configuration, and RBAC.
-
- string monitoringFlag = monitoringEnabled
- ? "--set monitoring.podMonitorEnabled=true --set monitoring.grafanaDashboard.create=true"
- : "--set monitoring.podMonitorEnabled=false";
-
- await RunCommand("helm",
- $"upgrade --install cnpg cloudnative-pg " +
- $"--repo {HelmRepo} --version {version} " +
- $"--namespace {targetNamespace} " +
- $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
- $"--set replicaCount=1 " +
- $"--set resources.requests.cpu=100m " +
- $"--set resources.requests.memory=256Mi " +
- $"--set resources.limits.cpu=500m " +
- $"--set resources.limits.memory=512Mi " +
- $"{monitoringFlag} " +
- $"--wait --timeout 10m",
- ct);
- actions.Add($"Helm install cnpg operator v{version}");
-
- // If the user selected a storage bucket for WAL/backup, create a
- // Kubernetes secret with the bucket credentials so CNPG clusters can
- // reference it for Barman object store (S3-based WAL archiving).
-
- if (options.Parameters?.TryGetValue("walBucketId", out string? walBucketIdVal) == true
- && !string.IsNullOrEmpty(walBucketIdVal) && Guid.TryParse(walBucketIdVal, out Guid walBucketId))
- {
- StorageBucket? walBucket = cluster.StorageBuckets.FirstOrDefault(b => b.Id == walBucketId);
-
- if (walBucket is not null)
- {
- await EnsureBackupStorageSecret(client, targetNamespace, walBucket, ct);
- actions.Add($"Created backup storage secret 'cnpg-backup-creds' with bucket '{walBucket.Name}'");
- actions.Add($"CNPG clusters can use: endpointURL={walBucket.Endpoint}, bucket={walBucket.Name}, secret=cnpg-backup-creds");
- logger.LogInformation("Created CNPG backup storage secret for bucket '{BucketName}'", walBucket.Name);
- }
- }
-
- logger.LogInformation("CloudNativePG Operator {Version} installed on cluster {Cluster}",
- version, cluster.Name);
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: $"CloudNativePG Operator {version} installed",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to install CNPG on cluster {Cluster}", cluster.Name);
- actions.Add($"Error: {ex.Message}");
-
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to install CloudNativePG: {ex.Message}",
- Actions: actions);
- }
- finally
- {
- if (File.Exists(tempKubeConfig))
- {
- File.Delete(tempKubeConfig);
- }
- }
- }
-
- ///
- /// Reconfigures CNPG operator. Supports:
- /// - "operatorReplicas": number of operator replicas
- /// - "monitoringEnabled": enable/disable Prometheus monitoring
- ///
- public async Task ConfigureAsync(KubernetesCluster cluster, ComponentConfiguration configuration, CancellationToken ct = default)
- {
- string targetNamespace = configuration.Namespace ?? DefaultNamespace;
- List actions = new();
-
- string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-cnpg-cfg-{Guid.NewGuid()}.kubeconfig");
-
- try
- {
- await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
-
- List setFlags = new();
-
- if (configuration.Values.TryGetValue("operatorReplicas", out string? replicas))
- {
- setFlags.Add($"--set replicaCount={replicas}");
- }
-
- if (configuration.Values.TryGetValue("monitoringEnabled", out string? monitoring))
- {
- setFlags.Add($"--set monitoring.podMonitorEnabled={monitoring}");
- setFlags.Add($"--set monitoring.grafanaDashboard.create={monitoring}");
- }
-
- await RunCommand("helm",
- $"upgrade cnpg cloudnative-pg --repo {HelmRepo} " +
- $"--namespace {targetNamespace} " +
- $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
- $"--reuse-values {string.Join(" ", setFlags)} --wait --timeout 5m",
- ct);
-
- actions.Add($"Reconfigured CNPG: {string.Join(", ", configuration.Values.Select(kv => $"{kv.Key}={kv.Value}"))}");
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: "CloudNativePG operator reconfigured",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to reconfigure CNPG on cluster {Cluster}", cluster.Name);
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to reconfigure CloudNativePG: {ex.Message}",
- Actions: new List { $"Error: {ex.Message}" });
- }
- finally
- {
- if (File.Exists(tempKubeConfig))
- {
- File.Delete(tempKubeConfig);
- }
- }
- }
-
- ///
- /// Uninstalls CloudNativePG operator from the cluster. Warning: existing
- /// PostgreSQL Cluster resources will become unmanaged.
- ///
- public async Task UninstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
- {
- string targetNamespace = options.Namespace ?? DefaultNamespace;
- List actions = new();
-
- string tempKubeConfig = Path.Combine(Path.GetTempPath(), $"entkube-cnpg-rm-{Guid.NewGuid()}.kubeconfig");
-
- try
- {
- await File.WriteAllTextAsync(tempKubeConfig, cluster.KubeConfig, ct);
-
- await RunCommand("helm",
- $"uninstall cnpg --namespace {targetNamespace} " +
- $"--kubeconfig \"{tempKubeConfig}\" --kube-context \"{cluster.ContextName}\" " +
- $"--wait --timeout 5m",
- ct);
- actions.Add("Helm uninstall cnpg");
-
- logger.LogInformation("CloudNativePG uninstalled from cluster {Cluster}", cluster.Name);
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: "CloudNativePG operator uninstalled successfully",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to uninstall CloudNativePG from cluster {Cluster}", cluster.Name);
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to uninstall CloudNativePG: {ex.Message}",
- Actions: new List { $"Error: {ex.Message}" });
- }
- finally
- {
- if (File.Exists(tempKubeConfig))
- {
- File.Delete(tempKubeConfig);
- }
- }
- }
-
- private async Task EnsureNamespaceWithLabels(Kubernetes client, string namespaceName, CancellationToken ct)
- {
- try
- {
- await client.CoreV1.ReadNamespaceAsync(namespaceName, cancellationToken: ct);
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- V1Namespace ns = new()
- {
- Metadata = new V1ObjectMeta
- {
- Name = namespaceName,
- Labels = new Dictionary
- {
- ["app.kubernetes.io/managed-by"] = "entkube",
- ["app.kubernetes.io/part-of"] = "cnpg-platform"
- }
- }
- };
- await client.CoreV1.CreateNamespaceAsync(ns, cancellationToken: ct);
- }
- }
-
- private async Task RunCommand(string fileName, string arguments, CancellationToken ct)
- {
- System.Diagnostics.ProcessStartInfo psi = new(fileName, arguments)
- {
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false
- };
-
- using System.Diagnostics.Process? process = System.Diagnostics.Process.Start(psi);
-
- if (process is null)
- {
- throw new InvalidOperationException($"Failed to start {fileName}");
- }
-
- string output = await process.StandardOutput.ReadToEndAsync(ct);
- string error = await process.StandardError.ReadToEndAsync(ct);
- await process.WaitForExitAsync(ct);
-
- if (process.ExitCode != 0)
- {
- throw new InvalidOperationException($"{fileName} failed (exit {process.ExitCode}): {error}");
- }
-
- return output;
- }
-
- private static Kubernetes BuildClient(KubernetesCluster cluster)
- {
- byte[] kubeConfigBytes = Encoding.UTF8.GetBytes(cluster.KubeConfig);
- using MemoryStream stream = new(kubeConfigBytes);
- KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream, cluster.ContextName);
- return new Kubernetes(config);
- }
-
- ///
- /// Creates or updates a Kubernetes Secret with S3 bucket credentials that
- /// CNPG clusters can reference for Barman object store (WAL archiving and
- /// base backups). The secret keys match what CNPG expects:
- /// - ACCESS_KEY_ID: S3 access key
- /// - ACCESS_SECRET_KEY: S3 secret key
- /// - BUCKET_NAME: target bucket name
- /// - ENDPOINT_URL: S3 endpoint URL
- ///
- private async Task EnsureBackupStorageSecret(
- Kubernetes client, string namespaceName, StorageBucket bucket, CancellationToken ct)
- {
- Dictionary secretData = new()
- {
- ["ACCESS_KEY_ID"] = Encoding.UTF8.GetBytes(bucket.AccessKey),
- ["ACCESS_SECRET_KEY"] = Encoding.UTF8.GetBytes(bucket.SecretKey),
- ["BUCKET_NAME"] = Encoding.UTF8.GetBytes(bucket.Name),
- ["ENDPOINT_URL"] = Encoding.UTF8.GetBytes(bucket.Endpoint)
- };
-
- // Include the S3 region when available. External S3 providers like
- // Cleura require a region for AWS Signature V4 authentication in
- // barman-cloud. Without it, WAL archiving fails silently.
-
- if (!string.IsNullOrWhiteSpace(bucket.Region))
- {
- secretData["AWS_DEFAULT_REGION"] = Encoding.UTF8.GetBytes(bucket.Region);
- }
-
- V1Secret secret = new()
- {
- Metadata = new V1ObjectMeta
- {
- Name = "cnpg-backup-creds",
- NamespaceProperty = namespaceName,
- Labels = new Dictionary
- {
- ["app.kubernetes.io/managed-by"] = "entkube",
- ["app.kubernetes.io/component"] = "cnpg-backup"
- }
- },
- Data = secretData
- };
-
- try
- {
- await client.CoreV1.ReplaceNamespacedSecretAsync(secret, "cnpg-backup-creds", namespaceName, cancellationToken: ct);
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- await client.CoreV1.CreateNamespacedSecretAsync(secret, namespaceName, cancellationToken: ct);
- }
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/CnpgDatabaseProvisioner.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/CnpgDatabaseProvisioner.cs
deleted file mode 100644
index a8eba3d..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/CnpgDatabaseProvisioner.cs
+++ /dev/null
@@ -1,139 +0,0 @@
-using k8s.Models;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components;
-
-///
-/// Creates databases on an existing CNPG cluster by building Kubernetes Job
-/// and Secret manifests. The installers (Gitea, Harbor, Keycloak) use these
-/// manifests to provision databases on the shared CNPG PostgreSQL cluster.
-/// Pure manifest builders — no I/O, fully unit-testable.
-///
-public static class CnpgDatabaseProvisioner
-{
- ///
- /// Builds the CNPG primary service FQDN from cluster name and namespace.
- /// The convention is {clusterName}-rw.{namespace}.svc.cluster.local.
- ///
- public static string GetPrimaryHost(string clusterName, string clusterNamespace)
- {
- return $"{clusterName}-rw.{clusterNamespace}.svc.cluster.local";
- }
-
- ///
- /// Builds the Kubernetes Job manifest that creates a database and owner role
- /// on a CNPG cluster. The Job runs psql against the cluster's read-write
- /// primary service ({clusterName}-rw).
- ///
- public static V1Job BuildCreateDatabaseJob(
- string clusterName,
- string clusterNamespace,
- string databaseName,
- string ownerRole,
- string ownerPassword)
- {
- // The CNPG primary service follows the naming convention {clusterName}-rw.
-
- string primaryService = $"{clusterName}-rw";
- string rawJobName = $"create-db-{databaseName}-{Guid.NewGuid():N}";
- string jobName = rawJobName.Length > 63 ? rawJobName[..63] : rawJobName;
-
- // Build a SQL script that creates the role (if not exists) and database.
- // We use CREATE ROLE ... LOGIN so the service can authenticate directly.
- // The IF NOT EXISTS pattern prevents failures on re-runs.
-
- string sqlScript =
- $"SELECT 'CREATE ROLE {ownerRole} LOGIN PASSWORD ''{ownerPassword}''' " +
- $"WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{ownerRole}')\\gexec\n" +
- $"SELECT 'CREATE DATABASE {databaseName} OWNER {ownerRole}' " +
- $"WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{databaseName}')\\gexec";
-
- return new V1Job
- {
- Metadata = new V1ObjectMeta
- {
- Name = jobName,
- NamespaceProperty = clusterNamespace,
- Labels = new Dictionary
- {
- ["app.kubernetes.io/managed-by"] = "entkube",
- ["entkube.io/operation"] = "create-database"
- }
- },
- Spec = new V1JobSpec
- {
- BackoffLimit = 3,
- TtlSecondsAfterFinished = 300,
- Template = new V1PodTemplateSpec
- {
- Spec = new V1PodSpec
- {
- RestartPolicy = "Never",
- Containers = new List
- {
- new()
- {
- Name = "create-db",
- Image = "postgres:16-alpine",
- Command = new List { "sh", "-c" },
- Args = new List
- {
- $"echo \"{sqlScript}\" | PGPASSWORD=$POSTGRES_PASSWORD psql -h {primaryService} -U postgres"
- },
- Env = new List
- {
- new()
- {
- Name = "POSTGRES_PASSWORD",
- ValueFrom = new V1EnvVarSource
- {
- SecretKeyRef = new V1SecretKeySelector
- {
- Name = $"{clusterName}-superuser",
- Key = "password"
- }
- }
- }
- }
- }
- }
- }
- }
- }
- };
- }
-
- ///
- /// Builds a Kubernetes Secret containing database credentials so the
- /// consuming service (Gitea, Harbor, Keycloak) can reference them.
- ///
- public static V1Secret BuildCredentialsSecret(
- string secretName,
- string targetNamespace,
- string host,
- string databaseName,
- string username,
- string password)
- {
- return new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = targetNamespace,
- Labels = new Dictionary
- {
- ["app.kubernetes.io/managed-by"] = "entkube",
- ["entkube.io/purpose"] = "database-credentials"
- }
- },
- Type = "Opaque",
- StringData = new Dictionary
- {
- ["host"] = host,
- ["database"] = databaseName,
- ["username"] = username,
- ["password"] = password
- }
- };
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/CustomDNS/CustomDNSCheck.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/CustomDNS/CustomDNSCheck.cs
deleted file mode 100644
index 7bfa8cf..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/CustomDNS/CustomDNSCheck.cs
+++ /dev/null
@@ -1,310 +0,0 @@
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.CustomDNS;
-
-///
-/// Checks the CoreDNS configuration on the cluster for customizations.
-/// CoreDNS is the default DNS provider in Kubernetes and its behaviour is
-/// controlled by the "coredns" ConfigMap in kube-system. This check looks for:
-///
-/// 1. CoreDNS pods running in kube-system
-/// 2. Custom stub zones (forwarding specific domains to external resolvers)
-/// 3. Custom Corefile entries (additional plugins, rewrites, logging)
-/// 4. Custom DNS records via the "coredns-custom" ConfigMap (static A/CNAME)
-///
-/// When customizations are detected the discovered configuration includes the
-/// raw Corefile content and any custom ConfigMap data so the platform knows
-/// exactly how DNS is configured on this cluster.
-///
-public class CustomDNSCheck : IAdoptionCheck
-{
- private readonly ILogger logger;
-
- public string ComponentName => "custom-dns";
- public string DisplayName => "Custom DNS (CoreDNS)";
-
- public CustomDNSCheck(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task CheckAsync(KubernetesCluster cluster, CancellationToken ct = default)
- {
- Kubernetes client = BuildClient(cluster);
- List details = new();
- List missing = new();
-
- // Check 1: CoreDNS pods running in kube-system.
- // Every Kubernetes cluster should have CoreDNS running — if not,
- // the cluster DNS is in a broken state.
-
- List coreDnsPods = await FindCoreDnsPods(client, ct);
-
- if (coreDnsPods.Count > 0)
- {
- details.AddRange(coreDnsPods.Select(p => $"CoreDNS pod: {p}"));
- }
- else
- {
- missing.Add("No CoreDNS pods found in kube-system namespace");
- return new ComponentCheckResult(ComponentName, ComponentStatus.NotInstalled, details, missing);
- }
-
- // Check 2: Read the main Corefile from the "coredns" ConfigMap.
- // This tells us what plugins and zones CoreDNS is configured with.
-
- string? corefile = await GetCorefile(client, ct);
- bool hasStubZones = false;
- bool hasCustomEntries = false;
-
- if (corefile is not null)
- {
- details.Add("Corefile ConfigMap present");
- hasStubZones = ContainsStubZones(corefile);
- hasCustomEntries = ContainsCustomEntries(corefile);
-
- if (hasStubZones)
- {
- details.Add("Stub zones detected in Corefile");
- }
-
- if (hasCustomEntries)
- {
- details.Add("Custom Corefile entries detected");
- }
- }
- else
- {
- missing.Add("coredns ConfigMap not found in kube-system");
- }
-
- // Check 3: Look for "coredns-custom" ConfigMap which holds additional
- // server blocks, static records, or override entries that get imported
- // into the main Corefile via the import directive.
-
- Dictionary? customRecords = await GetCustomRecordsConfigMap(client, ct);
- bool hasCustomRecords = customRecords is not null && customRecords.Count > 0;
-
- if (hasCustomRecords)
- {
- details.Add($"coredns-custom ConfigMap found with {customRecords!.Count} entries");
- }
-
- // Discover the CoreDNS version from the pod image tag.
-
- string? version = await GetVersionFromPods(client, ct);
-
- // Build discovered configuration based on what we found.
-
- Dictionary values = new()
- {
- ["replicas"] = coreDnsPods.Count.ToString(),
- ["hasStubZones"] = hasStubZones.ToString(),
- ["hasCustomEntries"] = hasCustomEntries.ToString(),
- ["hasCustomRecords"] = hasCustomRecords.ToString()
- };
-
- if (corefile is not null)
- {
- values["corefile"] = corefile;
- }
-
- DiscoveredConfiguration config = new(
- Version: version,
- Namespace: "kube-system",
- HelmReleaseName: null,
- Values: values);
-
- // If CoreDNS is running but has no customizations, report as Installed
- // but without custom configuration. If it has customizations, still Installed.
-
- ComponentStatus status = coreDnsPods.Count > 0
- ? ComponentStatus.Installed
- : ComponentStatus.NotInstalled;
-
- if (missing.Count > 0)
- {
- status = ComponentStatus.Degraded;
- }
-
- return new ComponentCheckResult(ComponentName, status, details, missing, config);
- }
-
- private async Task> FindCoreDnsPods(Kubernetes client, CancellationToken ct)
- {
- try
- {
- V1PodList podList = await client.CoreV1.ListNamespacedPodAsync(
- "kube-system",
- labelSelector: "k8s-app=kube-dns",
- cancellationToken: ct);
-
- return podList.Items
- .Where(p => p.Status?.Phase == "Running")
- .Select(p => p.Metadata.Name)
- .ToList();
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Failed to query CoreDNS pods");
- return new();
- }
- }
-
- private async Task GetCorefile(Kubernetes client, CancellationToken ct)
- {
- try
- {
- V1ConfigMap configMap = await client.CoreV1.ReadNamespacedConfigMapAsync(
- "coredns", "kube-system", cancellationToken: ct);
-
- if (configMap.Data?.TryGetValue("Corefile", out string? corefile) == true)
- {
- return corefile;
- }
-
- return null;
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Failed to read coredns ConfigMap");
- return null;
- }
- }
-
- private async Task?> GetCustomRecordsConfigMap(Kubernetes client, CancellationToken ct)
- {
- try
- {
- V1ConfigMap configMap = await client.CoreV1.ReadNamespacedConfigMapAsync(
- "coredns-custom", "kube-system", cancellationToken: ct);
-
- return configMap.Data?.ToDictionary(k => k.Key, v => v.Value);
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- // coredns-custom ConfigMap doesn't exist — that's fine, no custom records.
- return null;
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Failed to read coredns-custom ConfigMap");
- return null;
- }
- }
-
- ///
- /// Stub zones are identified by additional server blocks that forward
- /// specific domains to upstream DNS servers, e.g.:
- /// corp.internal:53 { forward . 10.0.0.53 }
- ///
- private static bool ContainsStubZones(string corefile)
- {
- // A stub zone typically appears as a separate server block with a forward directive
- // pointing to a non-standard upstream (not /etc/resolv.conf or well-known public DNS).
- string[] lines = corefile.Split('\n');
- bool insideNonDefaultBlock = false;
-
- foreach (string line in lines)
- {
- string trimmed = line.Trim();
-
- // A server block for a specific domain (not the catch-all ".") indicates a stub zone.
- if (trimmed.Contains(':') && trimmed.EndsWith('{') && !trimmed.StartsWith(".:"))
- {
- insideNonDefaultBlock = true;
- }
-
- if (insideNonDefaultBlock && trimmed.StartsWith("forward"))
- {
- return true;
- }
-
- if (trimmed == "}" && insideNonDefaultBlock)
- {
- insideNonDefaultBlock = false;
- }
- }
-
- return false;
- }
-
- ///
- /// Custom entries are non-default plugins like rewrite, log (when explicitly added),
- /// template, or import directives pointing to custom configuration.
- ///
- private static bool ContainsCustomEntries(string corefile)
- {
- string[] customPlugins = { "rewrite", "template", "import", "hosts", "file" };
-
- string[] lines = corefile.Split('\n');
-
- foreach (string line in lines)
- {
- string trimmed = line.Trim();
-
- foreach (string plugin in customPlugins)
- {
- if (trimmed.StartsWith(plugin + " ") || trimmed == plugin)
- {
- return true;
- }
- }
- }
-
- return false;
- }
-
- private async Task GetVersionFromPods(Kubernetes client, CancellationToken ct)
- {
- try
- {
- V1PodList podList = await client.CoreV1.ListNamespacedPodAsync(
- "kube-system",
- labelSelector: "k8s-app=kube-dns",
- cancellationToken: ct);
-
- V1Pod? firstPod = podList.Items.FirstOrDefault();
-
- if (firstPod?.Spec?.Containers is null)
- {
- return null;
- }
-
- string? image = firstPod.Spec.Containers
- .FirstOrDefault(c => c.Name == "coredns")?.Image;
-
- if (image is null)
- {
- return null;
- }
-
- // Image format: registry/coredns:1.11.1 or coredns/coredns:1.11.1
- int tagIndex = image.LastIndexOf(':');
-
- if (tagIndex > 0 && tagIndex < image.Length - 1)
- {
- return image[(tagIndex + 1)..];
- }
-
- return null;
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Failed to determine CoreDNS version");
- return null;
- }
- }
-
- private static Kubernetes BuildClient(KubernetesCluster cluster)
- {
- KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(
- new MemoryStream(System.Text.Encoding.UTF8.GetBytes(cluster.KubeConfig)),
- currentContext: cluster.ContextName);
-
- return new Kubernetes(config);
- }
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/CustomDNS/CustomDNSInstaller.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/CustomDNS/CustomDNSInstaller.cs
deleted file mode 100644
index 71722a6..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/CustomDNS/CustomDNSInstaller.cs
+++ /dev/null
@@ -1,438 +0,0 @@
-using System.Text;
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.CustomDNS;
-
-///
-/// Configures CoreDNS on the cluster by patching the "coredns" and "coredns-custom"
-/// ConfigMaps in kube-system. Unlike most other components that deploy via Helm,
-/// CoreDNS is already running on every Kubernetes cluster — we just customize it.
-///
-/// This installer supports three types of customization:
-///
-/// 1. **Stub zones** — Forward queries for specific domains to internal DNS servers.
-/// Format: "domain=ip1,ip2;domain2=ip3" → generates server blocks with forward directives.
-/// Example: "corp.internal=10.0.0.53,10.0.0.54;db.internal=10.1.0.53"
-///
-/// 2. **Custom Corefile entries** — Additional plugin directives injected into the
-/// main server block (e.g., rewrite rules, logging, template records).
-/// Format: Raw Corefile plugin text (newline-separated).
-///
-/// 3. **Custom DNS records** — Static A records stored in the "coredns-custom" ConfigMap
-/// and imported via the hosts plugin.
-/// Format: "hostname=ip;hostname2=ip2" → generates hosts-format entries.
-///
-/// Configuration keys:
-/// - "stubZones": Semicolon-separated zone definitions (zone=ip1,ip2)
-/// - "customCorefileEntries": Raw plugin text to inject into the main server block
-/// - "customRecords": Semicolon-separated static A records (hostname=ip)
-///
-/// After patching the ConfigMap, CoreDNS pods automatically reload the configuration
-/// (CoreDNS watches ConfigMap changes and hot-reloads within ~30s).
-///
-public class CustomDNSInstaller : IComponentInstaller
-{
- private readonly ILogger logger;
-
- private const string Namespace = "kube-system";
- private const string CoreDnsConfigMap = "coredns";
- private const string CoreDnsCustomConfigMap = "coredns-custom";
-
- public string ComponentName => "custom-dns";
-
- public CustomDNSInstaller(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task InstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
- {
- // CoreDNS is already installed on all K8s clusters. "Installing" custom DNS
- // means applying the initial set of customizations to the CoreDNS ConfigMap.
-
- Dictionary values = options.Parameters ?? new();
- List actions = new();
-
- try
- {
- Kubernetes client = BuildClient(cluster);
-
- // Parse and apply stub zones if provided.
-
- if (values.TryGetValue("stubZones", out string? stubZonesRaw) && !string.IsNullOrWhiteSpace(stubZonesRaw))
- {
- List stubZones = ParseStubZones(stubZonesRaw);
- await ApplyStubZones(client, stubZones, ct);
- actions.Add($"Stub zones configured: {string.Join(", ", stubZones.Select(z => z.Domain))}");
- }
-
- // Parse and apply custom Corefile entries if provided.
-
- if (values.TryGetValue("customCorefileEntries", out string? corefileEntries) && !string.IsNullOrWhiteSpace(corefileEntries))
- {
- await ApplyCustomCorefileEntries(client, corefileEntries, ct);
- actions.Add("Custom Corefile entries applied");
- }
-
- // Parse and apply custom static DNS records if provided.
-
- if (values.TryGetValue("customRecords", out string? recordsRaw) && !string.IsNullOrWhiteSpace(recordsRaw))
- {
- Dictionary records = ParseRecords(recordsRaw);
- await ApplyCustomRecords(client, records, ct);
- actions.Add($"Custom records applied: {string.Join(", ", records.Keys)}");
- }
-
- if (actions.Count == 0)
- {
- actions.Add("No customizations specified — CoreDNS left unchanged");
- }
-
- logger.LogInformation("CoreDNS customized on cluster {Cluster}: {Actions}",
- cluster.Name, string.Join("; ", actions));
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: "CoreDNS customized",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to customize CoreDNS on cluster {Cluster}", cluster.Name);
- actions.Add($"Error: {ex.Message}");
-
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to customize CoreDNS: {ex.Message}",
- Actions: actions);
- }
- }
-
- ///
- /// Reconfigures CoreDNS. Accepts the same keys as InstallAsync — stub zones,
- /// custom Corefile entries, and custom records. Each key is applied independently,
- /// so you can update just the stub zones without touching records.
- ///
- public async Task ConfigureAsync(KubernetesCluster cluster, ComponentConfiguration configuration, CancellationToken ct = default)
- {
- // ConfigureAsync delegates to the same logic as InstallAsync since
- // patching ConfigMaps is inherently idempotent — the new content
- // replaces whatever was there before.
-
- ComponentInstallOptions options = new(
- Namespace: configuration.Namespace,
- Parameters: configuration.Values);
-
- return await InstallAsync(cluster, options, ct);
- }
-
- ///
- /// Uninstalls custom DNS configuration by removing the coredns-custom ConfigMap.
- /// This reverts CoreDNS to its default configuration without stub zones or
- /// custom records.
- ///
- public async Task UninstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
- {
- List actions = new();
-
- try
- {
- Kubernetes client = BuildClient(cluster);
-
- // Delete the coredns-custom ConfigMap to remove all custom DNS entries.
-
- try
- {
- await client.CoreV1.DeleteNamespacedConfigMapAsync(
- CoreDnsCustomConfigMap, Namespace, cancellationToken: ct);
- actions.Add($"Deleted ConfigMap '{CoreDnsCustomConfigMap}' in '{Namespace}'");
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- // Already gone.
- }
-
- logger.LogInformation("Custom DNS configuration removed from cluster {Cluster}", cluster.Name);
-
- return new InstallResult(
- Success: true,
- ComponentName: ComponentName,
- Message: "Custom DNS configuration removed",
- Actions: actions);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to uninstall custom DNS from cluster {Cluster}", cluster.Name);
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: $"Failed to uninstall custom DNS: {ex.Message}",
- Actions: new List { $"Error: {ex.Message}" });
- }
- }
-
- ///
- /// Applies stub zones by patching the coredns ConfigMap's Corefile. Each stub zone
- /// becomes a separate server block that forwards queries for that domain to the
- /// specified upstream DNS servers. For example:
- ///
- /// corp.internal:53 {
- /// errors
- /// cache 30
- /// forward . 10.0.0.53 10.0.0.54
- /// }
- ///
- private async Task ApplyStubZones(Kubernetes client, List stubZones, CancellationToken ct)
- {
- // Read the current Corefile to preserve existing configuration.
-
- V1ConfigMap configMap = await client.CoreV1.ReadNamespacedConfigMapAsync(
- CoreDnsConfigMap, Namespace, cancellationToken: ct);
-
- string existingCorefile = "";
-
- if (configMap.Data is not null && configMap.Data.TryGetValue("Corefile", out string? corefileValue))
- {
- existingCorefile = corefileValue;
- }
-
- // Remove any previously managed stub zone blocks (marked with our comment).
- // This makes the operation idempotent — reapplying won't duplicate zones.
-
- string cleanedCorefile = RemoveManagedStubZones(existingCorefile);
-
- // Build the new stub zone blocks.
-
- StringBuilder stubZoneBlocks = new();
- stubZoneBlocks.AppendLine("# BEGIN EntKube managed stub zones");
-
- foreach (StubZone zone in stubZones)
- {
- string upstreams = string.Join(" ", zone.Upstreams);
- stubZoneBlocks.AppendLine($"{zone.Domain}:53 {{");
- stubZoneBlocks.AppendLine(" errors");
- stubZoneBlocks.AppendLine(" cache 30");
- stubZoneBlocks.AppendLine($" forward . {upstreams}");
- stubZoneBlocks.AppendLine("}");
- }
-
- stubZoneBlocks.AppendLine("# END EntKube managed stub zones");
-
- // Append stub zones after the main server block.
-
- string updatedCorefile = cleanedCorefile.TrimEnd() + "\n" + stubZoneBlocks.ToString();
-
- // Patch the ConfigMap with the updated Corefile.
-
- configMap.Data ??= new Dictionary();
- configMap.Data["Corefile"] = updatedCorefile;
-
- await client.CoreV1.ReplaceNamespacedConfigMapAsync(configMap, CoreDnsConfigMap, Namespace, cancellationToken: ct);
- }
-
- ///
- /// Applies custom Corefile entries by injecting them into the "coredns-custom"
- /// ConfigMap. CoreDNS imports this via `import /etc/coredns/custom/*.server`.
- /// This keeps custom entries separate from the main Corefile for cleaner management.
- ///
- private async Task ApplyCustomCorefileEntries(Kubernetes client, string entries, CancellationToken ct)
- {
- V1ConfigMap configMap = await EnsureCustomConfigMap(client, ct);
-
- configMap.Data ??= new Dictionary();
- configMap.Data["entkube-custom.server"] = entries;
-
- await client.CoreV1.ReplaceNamespacedConfigMapAsync(configMap, CoreDnsCustomConfigMap, Namespace, cancellationToken: ct);
- }
-
- ///
- /// Applies static DNS records via the "coredns-custom" ConfigMap using hosts plugin format.
- /// Each record becomes a line in a hosts-format file that CoreDNS serves.
- ///
- /// Example output in ConfigMap:
- /// 192.168.1.100 api.local
- /// 192.168.1.101 cache.local
- ///
- private async Task ApplyCustomRecords(Kubernetes client, Dictionary records, CancellationToken ct)
- {
- V1ConfigMap configMap = await EnsureCustomConfigMap(client, ct);
-
- // Build hosts-format content for the records.
-
- StringBuilder hostsContent = new();
- hostsContent.AppendLine("# BEGIN EntKube managed records");
-
- foreach (KeyValuePair record in records)
- {
- hostsContent.AppendLine($"{record.Value} {record.Key}");
- }
-
- hostsContent.AppendLine("# END EntKube managed records");
-
- // Store as a hosts-plugin compatible file in the custom ConfigMap.
-
- configMap.Data ??= new Dictionary();
- configMap.Data["entkube-records.override"] = hostsContent.ToString();
-
- // Also ensure the main Corefile imports this. We add an import directive
- // to the coredns-custom ConfigMap as a server block that uses the hosts plugin.
-
- string hostsServerBlock =
- $".:53 {{\n" +
- $" hosts /etc/coredns/custom/entkube-records.override {{\n" +
- $" fallthrough\n" +
- $" }}\n" +
- $"}}";
-
- configMap.Data["entkube-hosts.server"] = hostsServerBlock;
-
- await client.CoreV1.ReplaceNamespacedConfigMapAsync(configMap, CoreDnsCustomConfigMap, Namespace, cancellationToken: ct);
- }
-
- ///
- /// Ensures the coredns-custom ConfigMap exists. If it doesn't, create it.
- /// This ConfigMap is the standard extension point for CoreDNS customizations.
- ///
- private async Task EnsureCustomConfigMap(Kubernetes client, CancellationToken ct)
- {
- try
- {
- return await client.CoreV1.ReadNamespacedConfigMapAsync(
- CoreDnsCustomConfigMap, Namespace, cancellationToken: ct);
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- // ConfigMap doesn't exist yet — create it with management labels.
-
- V1ConfigMap newConfigMap = new()
- {
- Metadata = new V1ObjectMeta
- {
- Name = CoreDnsCustomConfigMap,
- NamespaceProperty = Namespace,
- Labels = new Dictionary
- {
- ["app.kubernetes.io/managed-by"] = "entkube"
- }
- },
- Data = new Dictionary()
- };
-
- return await client.CoreV1.CreateNamespacedConfigMapAsync(newConfigMap, Namespace, cancellationToken: ct);
- }
- }
-
- ///
- /// Removes previously managed stub zone blocks from the Corefile so we can
- /// reapply cleanly without duplicating entries.
- ///
- private static string RemoveManagedStubZones(string corefile)
- {
- string[] lines = corefile.Split('\n');
- StringBuilder result = new();
- bool insideManagedBlock = false;
-
- foreach (string line in lines)
- {
- if (line.TrimStart().StartsWith("# BEGIN EntKube managed stub zones"))
- {
- insideManagedBlock = true;
- continue;
- }
-
- if (line.TrimStart().StartsWith("# END EntKube managed stub zones"))
- {
- insideManagedBlock = false;
- continue;
- }
-
- if (!insideManagedBlock)
- {
- result.AppendLine(line);
- }
- }
-
- return result.ToString();
- }
-
- ///
- /// Parses stub zone definitions from the format "domain=ip1,ip2;domain2=ip3".
- /// Each semicolon-separated entry defines one stub zone. Within each entry,
- /// the domain and upstream IPs are separated by '=', and multiple IPs by ','.
- ///
- private static List ParseStubZones(string raw)
- {
- List zones = new();
-
- string[] entries = raw.Split(';', StringSplitOptions.RemoveEmptyEntries);
-
- foreach (string entry in entries)
- {
- string[] parts = entry.Split('=', 2);
-
- if (parts.Length != 2)
- {
- continue;
- }
-
- string domain = parts[0].Trim();
- List upstreams = parts[1].Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(ip => ip.Trim())
- .ToList();
-
- if (!string.IsNullOrWhiteSpace(domain) && upstreams.Count > 0)
- {
- zones.Add(new StubZone(domain, upstreams));
- }
- }
-
- return zones;
- }
-
- ///
- /// Parses custom DNS records from the format "hostname=ip;hostname2=ip2".
- /// Returns a dictionary mapping hostname → IP address.
- ///
- private static Dictionary ParseRecords(string raw)
- {
- Dictionary records = new();
-
- string[] entries = raw.Split(';', StringSplitOptions.RemoveEmptyEntries);
-
- foreach (string entry in entries)
- {
- string[] parts = entry.Split('=', 2);
-
- if (parts.Length != 2)
- {
- continue;
- }
-
- string hostname = parts[0].Trim();
- string ip = parts[1].Trim();
-
- if (!string.IsNullOrWhiteSpace(hostname) && !string.IsNullOrWhiteSpace(ip))
- {
- records[hostname] = ip;
- }
- }
-
- return records;
- }
-
- private static Kubernetes BuildClient(KubernetesCluster cluster)
- {
- KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(
- new MemoryStream(Encoding.UTF8.GetBytes(cluster.KubeConfig)),
- currentContext: cluster.ContextName);
-
- return new Kubernetes(config);
- }
-
- private record StubZone(string Domain, List Upstreams);
-}
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/DomainCA/DomainCACheck.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/DomainCA/DomainCACheck.cs
deleted file mode 100644
index fc44192..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/DomainCA/DomainCACheck.cs
+++ /dev/null
@@ -1,334 +0,0 @@
-using System.Text;
-using System.Text.Json;
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.DomainCA;
-
-///
-/// Checks whether domain-scoped CAs exist on the cluster. Unlike the platform
-/// internal-ca (which is a single general-purpose CA), domain CAs are scoped
-/// to specific DNS domains and may be:
-///
-/// - **Self-signed internal CAs** for private domains (e.g., *.internal.corp.com)
-/// - **Imported external CAs** with a purchased cert+key (e.g., DigiCert for *.example.com)
-/// - **Trust-only CAs** where only the root cert is imported (no key, can't sign)
-///
-/// Discovery looks for CA ClusterIssuers that have cert-manager Certificate policies
-/// (via annotations or Kyverno policies) restricting them to specific domains.
-/// Also detects TLS Secrets labeled as managed domain CAs.
-///
-public class DomainCACheck : IAdoptionCheck
-{
- private readonly ILogger logger;
-
- public string ComponentName => "domain-ca";
- public string DisplayName => "Domain CA (Domain-Scoped Certificate Authorities)";
-
- public DomainCACheck(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task CheckAsync(KubernetesCluster cluster, CancellationToken ct = default)
- {
- Kubernetes client = BuildClient(cluster);
- List details = new();
- List missing = new();
-
- // Check prerequisite: cert-manager must be installed.
-
- bool certManagerPresent = await CertManagerCrdExists(client, ct);
-
- if (!certManagerPresent)
- {
- missing.Add("cert-manager is not installed (prerequisite for domain CAs)");
- return new ComponentCheckResult(ComponentName, ComponentStatus.NotInstalled, details, missing);
- }
-
- details.Add("cert-manager CRD present (prerequisite satisfied)");
-
- // Look for CA ClusterIssuers with domain annotations (EntKube-managed).
-
- List domainCAs = await FindDomainCAs(client, ct);
-
- // Also look for domain CAs discovered through trust-manager Bundle CRs.
- // These are certificates added to the trust bundle — often external CAs
- // imported for trust without a corresponding ClusterIssuer (no private key).
-
- List trustBundleCAs = await FindTrustBundleDomainCAs(client, ct);
-
- // Merge trust bundle CAs that aren't already in the annotated list.
-
- HashSet knownNames = domainCAs.Select(c => c.Name).ToHashSet();
-
- foreach (DiscoveredDomainCA bundleCA in trustBundleCAs)
- {
- if (!knownNames.Contains(bundleCA.Name))
- {
- domainCAs.Add(bundleCA);
- }
- }
-
- if (domainCAs.Count == 0)
- {
- // No domain CAs found — also check for managed cert ConfigMaps
- // (trust-only external certs).
-
- List managedCerts = await FindManagedCertConfigMaps(client, ct);
-
- if (managedCerts.Count == 0)
- {
- missing.Add("No domain-scoped CAs or managed certificates found");
- return new ComponentCheckResult(ComponentName, ComponentStatus.NotInstalled, details, missing);
- }
-
- details.AddRange(managedCerts.Select(c => $"Trust-only certificate: {c}"));
- }
- else
- {
- foreach (DiscoveredDomainCA ca in domainCAs)
- {
- details.Add($"Domain CA: {ca.Name} (domains: {string.Join(", ", ca.Domains)}, external: {ca.IsExternal})");
- }
- }
-
- // Build discovered configuration summarizing what we found.
-
- Dictionary values = new()
- {
- ["domainCACount"] = domainCAs.Count.ToString(),
- ["domainCAs"] = string.Join(";", domainCAs.Select(c => $"{c.Name}:{string.Join(",", c.Domains)}"))
- };
-
- DiscoveredConfiguration config = new(
- Version: null,
- Namespace: "cert-manager",
- HelmReleaseName: null,
- Values: values);
-
- return new ComponentCheckResult(ComponentName, ComponentStatus.Installed, details, missing, config);
- }
-
- private async Task> FindDomainCAs(Kubernetes client, CancellationToken ct)
- {
- List result = new();
-
- try
- {
- JsonElement issuers = await client.CustomObjects.ListClusterCustomObjectAsync(
- group: "cert-manager.io",
- version: "v1",
- plural: "clusterissuers",
- cancellationToken: ct);
-
- if (issuers.TryGetProperty("items", out JsonElement items))
- {
- foreach (JsonElement issuer in items.EnumerateArray())
- {
- // Check for EntKube domain annotation.
-
- if (issuer.TryGetProperty("metadata", out JsonElement metadata) &&
- metadata.TryGetProperty("annotations", out JsonElement annotations) &&
- annotations.TryGetProperty("entkube.io/domains", out JsonElement domainsEl))
- {
- string? name = metadata.GetProperty("name").GetString();
- string? domains = domainsEl.GetString();
-
- bool isExternal = annotations.TryGetProperty("entkube.io/external-ca", out JsonElement extEl)
- && extEl.GetString() == "true";
-
- if (name is not null && domains is not null)
- {
- result.Add(new DiscoveredDomainCA(
- name,
- domains.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(),
- isExternal));
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Failed to list ClusterIssuers while checking for domain CAs");
- }
-
- return result;
- }
-
- private async Task> FindManagedCertConfigMaps(Kubernetes client, CancellationToken ct)
- {
- List result = new();
-
- try
- {
- V1ConfigMapList configMaps = await client.CoreV1.ListNamespacedConfigMapAsync(
- "cert-manager",
- labelSelector: "entkube.io/component=trust-bundle",
- cancellationToken: ct);
-
- foreach (V1ConfigMap cm in configMaps.Items)
- {
- result.Add(cm.Metadata.Name);
- }
- }
- catch (Exception ex)
- {
- logger.LogDebug(ex, "Could not list managed cert ConfigMaps");
- }
-
- return result;
- }
-
- ///
- /// Scans trust-manager Bundle CRs for certificate sources that represent
- /// domain CAs. These are certificates imported into the trust bundle for
- /// workload trust — often external CAs without a ClusterIssuer (trust-only).
- /// This aligns DomainCACheck detection with CertificateAuthorityHandler,
- /// which also discovers domain CAs from Bundle sources.
- ///
- private async Task> FindTrustBundleDomainCAs(Kubernetes client, CancellationToken ct)
- {
- List result = new();
-
- try
- {
- JsonElement bundles = await client.CustomObjects.ListClusterCustomObjectAsync(
- group: "trust.cert-manager.io",
- version: "v1alpha1",
- plural: "bundles",
- cancellationToken: ct);
-
- if (!bundles.TryGetProperty("items", out JsonElement items))
- {
- return result;
- }
-
- // Collect all Secret and ConfigMap source names referenced in Bundles.
- // Each source is a CA certificate that workloads trust.
-
- HashSet issuerSecretNames = await GetIssuerSecretNames(client, ct);
-
- foreach (JsonElement bundle in items.EnumerateArray())
- {
- if (!bundle.TryGetProperty("spec", out JsonElement spec)
- || !spec.TryGetProperty("sources", out JsonElement sources))
- {
- continue;
- }
-
- foreach (JsonElement source in sources.EnumerateArray())
- {
- // Secret sources — check if this Secret is already backing
- // a ClusterIssuer (if so, it'll be found by FindDomainCAs
- // if it has annotations, or it's an internal CA).
-
- if (source.TryGetProperty("secret", out JsonElement secretSource)
- && secretSource.TryGetProperty("name", out JsonElement nameEl))
- {
- string? name = nameEl.GetString();
-
- if (name is not null && !issuerSecretNames.Contains(name))
- {
- result.Add(new DiscoveredDomainCA(name, new List(), IsExternal: true));
- }
- }
-
- // ConfigMap sources — imported CA certificates stored as ConfigMaps.
-
- if (source.TryGetProperty("configMap", out JsonElement cmSource)
- && cmSource.TryGetProperty("name", out JsonElement cmNameEl))
- {
- string? name = cmNameEl.GetString();
-
- if (name is not null)
- {
- result.Add(new DiscoveredDomainCA(name, new List(), IsExternal: true));
- }
- }
- }
- }
- }
- catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- // Bundle CRD not found — trust-manager not installed.
- logger.LogDebug("Bundle CRD not found — trust-manager may not be installed");
- }
- catch (Exception ex)
- {
- logger.LogDebug(ex, "Could not scan trust-manager Bundles for domain CAs");
- }
-
- return result;
- }
-
- ///
- /// Collects the secret names used by all CA ClusterIssuers so we can
- /// exclude them from trust bundle sources (they're already tracked
- /// as ClusterIssuer-backed CAs, not trust-only CAs).
- ///
- private async Task> GetIssuerSecretNames(Kubernetes client, CancellationToken ct)
- {
- HashSet secretNames = new();
-
- try
- {
- JsonElement issuers = await client.CustomObjects.ListClusterCustomObjectAsync(
- group: "cert-manager.io",
- version: "v1",
- plural: "clusterissuers",
- cancellationToken: ct);
-
- if (issuers.TryGetProperty("items", out JsonElement items))
- {
- foreach (JsonElement issuer in items.EnumerateArray())
- {
- if (issuer.TryGetProperty("spec", out JsonElement spec)
- && spec.TryGetProperty("ca", out JsonElement ca)
- && ca.TryGetProperty("secretName", out JsonElement secretNameEl))
- {
- string? secretName = secretNameEl.GetString();
-
- if (secretName is not null)
- {
- secretNames.Add(secretName);
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- logger.LogDebug(ex, "Could not list ClusterIssuer secret names");
- }
-
- return secretNames;
- }
-
- private async Task CertManagerCrdExists(Kubernetes client, CancellationToken ct)
- {
- try
- {
- await client.ApiextensionsV1.ReadCustomResourceDefinitionAsync(
- "certificates.cert-manager.io", cancellationToken: ct);
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private static Kubernetes BuildClient(KubernetesCluster cluster)
- {
- byte[] kubeConfigBytes = Encoding.UTF8.GetBytes(cluster.KubeConfig);
- using MemoryStream stream = new(kubeConfigBytes);
- KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(stream, cluster.ContextName);
- return new Kubernetes(config);
- }
-}
-
-internal record DiscoveredDomainCA(string Name, List Domains, bool IsExternal);
diff --git a/src/EntKube.Clusters/Features/AdoptCluster/Components/DomainCA/DomainCAInstaller.cs b/src/EntKube.Clusters/Features/AdoptCluster/Components/DomainCA/DomainCAInstaller.cs
deleted file mode 100644
index 1a87e25..0000000
--- a/src/EntKube.Clusters/Features/AdoptCluster/Components/DomainCA/DomainCAInstaller.cs
+++ /dev/null
@@ -1,814 +0,0 @@
-using System.Text;
-using System.Text.Json;
-using EntKube.Clusters.Domain;
-using k8s;
-using k8s.Models;
-using Microsoft.Extensions.Logging;
-
-namespace EntKube.Clusters.Features.AdoptCluster.Components.DomainCA;
-
-///
-/// Provisions, imports, or adopts domain-scoped Certificate Authorities.
-/// Unlike the platform internal-ca (general purpose), domain CAs are restricted
-/// to signing certificates only for specific DNS domains.
-///
-/// Three modes of operation:
-///
-/// **1. Provision a new domain CA (self-signed)**
-/// Creates a new self-signed root CA scoped to specific domains.
-/// Use case: internal private domains like *.internal.corp.com
-/// Parameters: caName, domains, caOrganization, caDurationDays
-///
-/// **2. Import an external CA (cert + key provided)**
-/// Imports a purchased CA certificate and private key as a K8s TLS Secret,
-/// then creates a CA ClusterIssuer referencing it. cert-manager can now sign
-/// certificates for those domains using the imported CA.
-/// Use case: DigiCert/Sectigo intermediate CA for *.example.com
-/// Parameters: caName, importExternal=true, tlsCert, tlsKey, domains
-///
-/// **3. Trust-only import (cert only, no key)**
-/// Imports just the CA certificate into the trust bundle. No ClusterIssuer
-/// is created because we can't sign certs without the private key.
-/// Use case: Partner API CA, corporate PKI root that services need to trust
-/// Parameters: caName, importExternal=true, tlsCert (no tlsKey), domains (optional)
-///
-/// All modes add the CA root certificate to the trust-manager Bundle so
-/// workloads automatically trust certificates signed by these CAs.
-///
-/// Configuration keys:
-/// - "caName": Identifier for this domain CA (used as ClusterIssuer name)
-/// - "domains": Comma-separated list of domains this CA covers (e.g., "*.internal.corp.com,*.svc.local")
-/// - "importExternal": "true" to import an existing cert rather than self-signing
-/// - "tlsCert": Base64-encoded PEM certificate (required for import)
-/// - "tlsKey": Base64-encoded PEM private key (optional — if absent, trust-only)
-/// - "caOrganization": Organization field for self-signed CAs (default: "EntKube Domain CA")
-/// - "caDurationDays": Validity in days for self-signed CAs (default: "1825")
-/// - "bundleName": Trust bundle to add the CA to (default: "platform-trust-bundle")
-///
-public class DomainCAInstaller : IComponentInstaller
-{
- private readonly ILogger logger;
-
- private const string DefaultNamespace = "cert-manager";
- private const string DefaultBundleName = "platform-trust-bundle";
-
- public string ComponentName => "domain-ca";
-
- public DomainCAInstaller(ILogger logger)
- {
- this.logger = logger;
- }
-
- public async Task InstallAsync(KubernetesCluster cluster, ComponentInstallOptions options, CancellationToken ct = default)
- {
- List actions = new();
-
- try
- {
- Kubernetes client = BuildClient(cluster);
-
- // Check prerequisite: cert-manager must be installed.
-
- bool certManagerPresent = await CertManagerIsPresent(client, ct);
-
- if (!certManagerPresent)
- {
- return new InstallResult(
- Success: false,
- ComponentName: ComponentName,
- Message: "cert-manager is required but not installed",
- Actions: new List