From 4d0cfb1bbfe6c2c70a702891c4962b46b93402c6 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 25 Mar 2026 10:29:14 -0500 Subject: [PATCH] Migrate k8s config from Terraform to Argo CD + ESO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Install External Secrets Operator via Argo CD app - apps/external-secrets.yaml — ESO Helm chart install - apps/external-secrets-config.yaml — ClusterSecretStore deployment - k8s/external-secrets/cluster-secret-store.yaml — Vault backend using vault-token Secret Phase 2: Create k8s manifests for all services - k8s/neuron/ — PVC, ConfigMap, ExternalSecrets (neuron-secrets, cloudflared-secret), Ingress - k8s/gitea/ — PVC, ConfigMap (custom CSS), ExternalSecret (gitea-db), Ingress - k8s/github-runner/ — ExternalSecret (github-runner-secret) - k8s/gitea-runner/ — ExternalSecret (gitea-runner-secret) - k8s/monitoring/ — ExternalSecrets (grafana, slack), Alloy OTLP service+middleware, datasources ConfigMap, Ingress - k8s/postgres/ — ExternalSecret (postgres-passwords) - k8s/vault/ — ExternalSecret (vault-gcp-sa from Vault) - k8s/adguard/ — PVCs, ConfigMap, Certificate, Ingress, ddclient Deployment+ExternalSecret - k8s/ollama/ — PVC, Ingress - k8s/headscale/ — PVC - k8s/packages/ — PVCs, ConfigMap, Ingresses - k8s/registry/ — PVC, Ingresses - k8s/backup/ — CronJob, ExternalSecret (backup-credentials) New Argo CD apps for Helm releases: - apps/kube-prometheus-stack.yaml, loki.yaml, tempo.yaml, alloy.yaml - apps/postgres.yaml, redis.yaml, vault.yaml New Argo CD apps for k8s config paths: - apps/neuron-config, gitea-config, ci-config, gitea-runner-config - apps/monitoring-config, adguard-config, ollama-config, headscale-config - apps/packages-config, registry-config, postgres-config, vault-config, backup Phase 3: Strip Terraform to infrastructure-only - All kubernetes_* and helm_release resources removed from service .tf files - Each service .tf now contains only kubernetes_namespace (bootstrap dependency) - variables.tf stripped to only cloudflare_api_key, cloudflare_email, gitea_api_token - namespaces.tf gains external-secrets namespace - ingress.tf, backup.tf, ddclient.tf emptied (resources in k8s/) - cert-manager.tf, argocd.tf, traefik.tf unchanged (bootstrap) --- servers/legion/adguard.tf | 170 +------- servers/legion/apps/adguard-config.yaml | 20 + servers/legion/apps/alloy.yaml | 82 ++++ servers/legion/apps/backup.yaml | 20 + servers/legion/apps/ci-config.yaml | 41 ++ .../legion/apps/external-secrets-config.yaml | 20 + servers/legion/apps/external-secrets.yaml | 23 ++ servers/legion/apps/gitea-config.yaml | 20 + servers/legion/apps/headscale-config.yaml | 20 + .../legion/apps/kube-prometheus-stack.yaml | 108 +++++ servers/legion/apps/loki.yaml | 44 ++ servers/legion/apps/monitoring-config.yaml | 20 + servers/legion/apps/neuron-config.yaml | 20 + servers/legion/apps/ollama-config.yaml | 20 + servers/legion/apps/packages-config.yaml | 20 + servers/legion/apps/postgres-config.yaml | 20 + servers/legion/apps/postgres.yaml | 41 ++ servers/legion/apps/redis.yaml | 32 ++ servers/legion/apps/registry-config.yaml | 20 + servers/legion/apps/tempo.yaml | 31 ++ servers/legion/apps/vault-config.yaml | 20 + servers/legion/apps/vault.yaml | 89 +++++ servers/legion/backup.tf | 139 +------ servers/legion/ddclient.tf | 76 +--- servers/legion/gitea-runner.tf | 16 +- servers/legion/gitea.tf | 178 +-------- servers/legion/github-runner.tf | 14 +- servers/legion/headscale.tf | 26 +- servers/legion/ingress.tf | 247 +----------- servers/legion/k8s/adguard/certificate.yaml | 15 + servers/legion/k8s/adguard/configmap.yaml | 59 +++ .../k8s/adguard/ddclient-deployment.yaml | 39 ++ .../k8s/adguard/ddclient-external-secret.yaml | 39 ++ servers/legion/k8s/adguard/ingress.yaml | 25 ++ servers/legion/k8s/adguard/pvc.yaml | 24 ++ servers/legion/k8s/backup/cronjob.yaml | 95 +++++ .../legion/k8s/backup/external-secrets.yaml | 35 ++ .../cluster-secret-store.yaml | 23 ++ .../k8s/gitea-runner/external-secrets.yaml | 24 ++ servers/legion/k8s/gitea/configmap.yaml | 104 +++++ .../legion/k8s/gitea/external-secrets.yaml | 20 + servers/legion/k8s/gitea/ingress.yaml | 25 ++ servers/legion/k8s/gitea/pvc.yaml | 11 + .../k8s/github-runner/external-secrets.yaml | 20 + servers/legion/k8s/headscale/pvc.yaml | 11 + .../k8s/monitoring/alloy-otlp-middleware.yaml | 11 + .../k8s/monitoring/alloy-otlp-service.yaml | 18 + .../k8s/monitoring/external-secrets.yaml | 23 ++ .../k8s/monitoring/grafana-datasources.yaml | 37 ++ .../monitoring/grafana-external-secrets.yaml | 40 ++ servers/legion/k8s/monitoring/ingress.yaml | 29 ++ servers/legion/k8s/neuron/configmap.yaml | 27 ++ .../legion/k8s/neuron/external-secrets.yaml | 52 +++ servers/legion/k8s/neuron/ingress.yaml | 25 ++ servers/legion/k8s/neuron/pvc.yaml | 11 + servers/legion/k8s/ollama/ingress.yaml | 25 ++ servers/legion/k8s/ollama/pvc.yaml | 11 + servers/legion/k8s/packages/configmap.yaml | 32 ++ servers/legion/k8s/packages/ingress.yaml | 54 +++ servers/legion/k8s/packages/pvc.yaml | 24 ++ .../legion/k8s/postgres/external-secrets.yaml | 25 ++ servers/legion/k8s/registry/ingress.yaml | 58 +++ servers/legion/k8s/registry/pvc.yaml | 11 + .../k8s/vault/gcp-sa-external-secret.yaml | 22 + servers/legion/monitoring.tf | 378 +----------------- servers/legion/namespaces.tf | 10 + servers/legion/neuron.tf | 127 +----- servers/legion/ollama.tf | 23 +- servers/legion/packages.tf | 71 +--- servers/legion/postgres.tf | 37 +- servers/legion/redis.tf | 37 +- servers/legion/registry.tf | 23 +- servers/legion/variables.tf | 110 +---- servers/legion/vault.tf | 104 +---- 74 files changed, 1895 insertions(+), 1726 deletions(-) create mode 100644 servers/legion/apps/adguard-config.yaml create mode 100644 servers/legion/apps/alloy.yaml create mode 100644 servers/legion/apps/backup.yaml create mode 100644 servers/legion/apps/ci-config.yaml create mode 100644 servers/legion/apps/external-secrets-config.yaml create mode 100644 servers/legion/apps/external-secrets.yaml create mode 100644 servers/legion/apps/gitea-config.yaml create mode 100644 servers/legion/apps/headscale-config.yaml create mode 100644 servers/legion/apps/kube-prometheus-stack.yaml create mode 100644 servers/legion/apps/loki.yaml create mode 100644 servers/legion/apps/monitoring-config.yaml create mode 100644 servers/legion/apps/neuron-config.yaml create mode 100644 servers/legion/apps/ollama-config.yaml create mode 100644 servers/legion/apps/packages-config.yaml create mode 100644 servers/legion/apps/postgres-config.yaml create mode 100644 servers/legion/apps/postgres.yaml create mode 100644 servers/legion/apps/redis.yaml create mode 100644 servers/legion/apps/registry-config.yaml create mode 100644 servers/legion/apps/tempo.yaml create mode 100644 servers/legion/apps/vault-config.yaml create mode 100644 servers/legion/apps/vault.yaml create mode 100644 servers/legion/k8s/adguard/certificate.yaml create mode 100644 servers/legion/k8s/adguard/configmap.yaml create mode 100644 servers/legion/k8s/adguard/ddclient-deployment.yaml create mode 100644 servers/legion/k8s/adguard/ddclient-external-secret.yaml create mode 100644 servers/legion/k8s/adguard/ingress.yaml create mode 100644 servers/legion/k8s/adguard/pvc.yaml create mode 100644 servers/legion/k8s/backup/cronjob.yaml create mode 100644 servers/legion/k8s/backup/external-secrets.yaml create mode 100644 servers/legion/k8s/external-secrets/cluster-secret-store.yaml create mode 100644 servers/legion/k8s/gitea-runner/external-secrets.yaml create mode 100644 servers/legion/k8s/gitea/configmap.yaml create mode 100644 servers/legion/k8s/gitea/external-secrets.yaml create mode 100644 servers/legion/k8s/gitea/ingress.yaml create mode 100644 servers/legion/k8s/gitea/pvc.yaml create mode 100644 servers/legion/k8s/github-runner/external-secrets.yaml create mode 100644 servers/legion/k8s/headscale/pvc.yaml create mode 100644 servers/legion/k8s/monitoring/alloy-otlp-middleware.yaml create mode 100644 servers/legion/k8s/monitoring/alloy-otlp-service.yaml create mode 100644 servers/legion/k8s/monitoring/external-secrets.yaml create mode 100644 servers/legion/k8s/monitoring/grafana-datasources.yaml create mode 100644 servers/legion/k8s/monitoring/grafana-external-secrets.yaml create mode 100644 servers/legion/k8s/monitoring/ingress.yaml create mode 100644 servers/legion/k8s/neuron/configmap.yaml create mode 100644 servers/legion/k8s/neuron/external-secrets.yaml create mode 100644 servers/legion/k8s/neuron/ingress.yaml create mode 100644 servers/legion/k8s/neuron/pvc.yaml create mode 100644 servers/legion/k8s/ollama/ingress.yaml create mode 100644 servers/legion/k8s/ollama/pvc.yaml create mode 100644 servers/legion/k8s/packages/configmap.yaml create mode 100644 servers/legion/k8s/packages/ingress.yaml create mode 100644 servers/legion/k8s/packages/pvc.yaml create mode 100644 servers/legion/k8s/postgres/external-secrets.yaml create mode 100644 servers/legion/k8s/registry/ingress.yaml create mode 100644 servers/legion/k8s/registry/pvc.yaml create mode 100644 servers/legion/k8s/vault/gcp-sa-external-secret.yaml diff --git a/servers/legion/adguard.tf b/servers/legion/adguard.tf index 6bf9c09..a19a83c 100644 --- a/servers/legion/adguard.tf +++ b/servers/legion/adguard.tf @@ -1,4 +1,6 @@ -# AdGuard Home — household DNS with ad blocking, local resolution, DoH via dns.nook.family, and DoT via dot.nook.family:853 +# AdGuard Home — namespace only +# PVCs, ConfigMap, Certificate, Ingress, ddclient managed by Argo CD + ESO +# See: apps/adguard.yaml, apps/adguard-config.yaml, k8s/adguard/ resource "kubernetes_namespace" "dns" { metadata { @@ -9,169 +11,3 @@ resource "kubernetes_namespace" "dns" { } } } - -resource "kubernetes_persistent_volume_claim" "adguard_config" { - wait_until_bound = false - metadata { - name = "adguard-config" - namespace = kubernetes_namespace.dns.metadata[0].name - } - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "1Gi" - } - } - } -} - -resource "kubernetes_persistent_volume_claim" "adguard_data" { - wait_until_bound = false - metadata { - name = "adguard-data" - namespace = kubernetes_namespace.dns.metadata[0].name - } - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "5Gi" - } - } - } -} - -# Desired AdGuard settings — applied by init container on every pod start. -# To change DNS upstreams, filter lists, or rewrites: edit here, terraform apply, then push apps/adguard.yaml. -resource "kubernetes_config_map" "adguard_defaults" { - metadata { - name = "adguard-defaults" - namespace = kubernetes_namespace.dns.metadata[0].name - } - - data = { - "AdGuardHome.yaml" = <<-EOT - dns: - bind_hosts: - - 0.0.0.0 - port: 53 - upstream_dns: - - https://dns.cloudflare.com/dns-query - - https://dns.google/dns-query - bootstrap_dns: - - 1.1.1.1 - - 8.8.8.8 - - 9.9.9.10 - - 149.112.112.10 - upstream_mode: load_balance - cache_enabled: true - cache_size: 4194304 - filters: - - enabled: true - url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt - name: AdGuard DNS filter - id: 1 - - enabled: true - url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt - name: AdAway Default Blocklist - id: 2 - - enabled: true - url: https://big.oisd.nl/domainswild - name: OISD Big - id: 3 - - enabled: true - url: https://easylist.to/easylist/easylist.txt - name: EasyList - id: 4 - - enabled: true - url: https://easylist.to/easylist/easyprivacy.txt - name: EasyPrivacy - id: 5 - tls: - enabled: true - server_name: dot.nook.family - port_dns_over_tls: 853 - certificate_path: /etc/adguard/tls/tls.crt - private_key_path: /etc/adguard/tls/tls.key - allow_unencrypted_doh: true - filtering: - filtering_enabled: true - rewrites: - - domain: '*.nook.family' - answer: ${var.legion_ip} - enabled: true - - domain: '*.neuralplatform.ai' - answer: ${var.legion_ip} - enabled: true - EOT - } -} - -# TLS certificate for DoT — issued via cert-manager DNS-01 (Cloudflare) -resource "kubernetes_manifest" "adguard_dot_cert" { - manifest = { - apiVersion = "cert-manager.io/v1" - kind = "Certificate" - metadata = { - name = "adguard-dot-tls" - namespace = kubernetes_namespace.dns.metadata[0].name - } - spec = { - secretName = "adguard-dot-tls" - issuerRef = { - name = "letsencrypt-prod" - kind = "ClusterIssuer" - } - dnsNames = ["dot.nook.family"] - } - } - - depends_on = [kubernetes_manifest.cluster_issuer_prod] -} - -# Deployment + Service managed by Argo CD — see servers/legion/apps/adguard.yaml - -resource "kubernetes_ingress_v1" "adguard" { - metadata { - name = "adguard" - namespace = kubernetes_namespace.dns.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - - spec { - tls { - hosts = ["dns.nook.family"] - secret_name = "adguard-tls" - } - - rule { - host = "dns.nook.family" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "adguard-ui" - port { - number = 3000 - } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} - -# Host-level bootstrap notes (run bootstrap.sh once on new Legion installs): -# - systemd-resolved disabled (conflicts with AdGuard on port 53) -# - k3s config.yaml includes tls-san for LAN IP -# - /etc/resolv.conf set to 1.1.1.1 fallback -# See: servers/legion/bootstrap.sh diff --git a/servers/legion/apps/adguard-config.yaml b/servers/legion/apps/adguard-config.yaml new file mode 100644 index 0000000..60ae69a --- /dev/null +++ b/servers/legion/apps/adguard-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: adguard-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/adguard + destination: + server: https://kubernetes.default.svc + namespace: dns + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/alloy.yaml b/servers/legion/apps/alloy.yaml new file mode 100644 index 0000000..77dbfc0 --- /dev/null +++ b/servers/legion/apps/alloy.yaml @@ -0,0 +1,82 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: alloy + namespace: argocd +spec: + project: default + source: + repoURL: https://grafana.github.io/helm-charts + chart: alloy + targetRevision: "*" + helm: + values: | + controller: + type: daemonset + alloy: + configMap: + content: | + discovery.kubernetes "pods" { + role = "pod" + } + + discovery.relabel "pods" { + targets = discovery.kubernetes.pods.targets + rule { + source_labels = ["__meta_kubernetes_namespace"] + target_label = "namespace" + } + rule { + source_labels = ["__meta_kubernetes_pod_name"] + target_label = "pod" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + target_label = "container" + } + rule { + source_labels = ["__meta_kubernetes_pod_label_app"] + target_label = "app" + } + } + + loki.source.kubernetes "pods" { + targets = discovery.relabel.pods.output + forward_to = [loki.write.default.receiver] + } + + loki.write "default" { + endpoint { + url = "http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push" + tenant_id = "legion" + } + } + + otelcol.receiver.otlp "default" { + grpc { endpoint = "0.0.0.0:4317" } + http { endpoint = "0.0.0.0:4318" } + output { + traces = [otelcol.exporter.otlp.tempo.input] + logs = [otelcol.exporter.loki.pantheon.input] + } + } + + otelcol.exporter.otlp "tempo" { + client { + endpoint = "tempo.monitoring.svc.cluster.local:4317" + tls { insecure = true } + } + } + + otelcol.exporter.loki "pantheon" { + forward_to = [loki.write.default.receiver] + } + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/backup.yaml b/servers/legion/apps/backup.yaml new file mode 100644 index 0000000..9ad29e6 --- /dev/null +++ b/servers/legion/apps/backup.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: backup + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/backup + destination: + server: https://kubernetes.default.svc + namespace: git + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/ci-config.yaml b/servers/legion/apps/ci-config.yaml new file mode 100644 index 0000000..1877d37 --- /dev/null +++ b/servers/legion/apps/ci-config.yaml @@ -0,0 +1,41 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ci-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/github-runner + destination: + server: https://kubernetes.default.svc + namespace: ci + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitea-runner-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/gitea-runner + destination: + server: https://kubernetes.default.svc + namespace: ci + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/external-secrets-config.yaml b/servers/legion/apps/external-secrets-config.yaml new file mode 100644 index 0000000..6a2ff49 --- /dev/null +++ b/servers/legion/apps/external-secrets-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/external-secrets + destination: + server: https://kubernetes.default.svc + namespace: external-secrets + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/external-secrets.yaml b/servers/legion/apps/external-secrets.yaml new file mode 100644 index 0000000..ff21cab --- /dev/null +++ b/servers/legion/apps/external-secrets.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets + namespace: argocd +spec: + project: default + source: + repoURL: https://charts.external-secrets.io + chart: external-secrets + targetRevision: 0.10.7 + helm: + values: | + installCRDs: true + destination: + server: https://kubernetes.default.svc + namespace: external-secrets + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/gitea-config.yaml b/servers/legion/apps/gitea-config.yaml new file mode 100644 index 0000000..b042f7a --- /dev/null +++ b/servers/legion/apps/gitea-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitea-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/gitea + destination: + server: https://kubernetes.default.svc + namespace: git + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/headscale-config.yaml b/servers/legion/apps/headscale-config.yaml new file mode 100644 index 0000000..c89d971 --- /dev/null +++ b/servers/legion/apps/headscale-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: headscale-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/headscale + destination: + server: https://kubernetes.default.svc + namespace: headscale + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/kube-prometheus-stack.yaml b/servers/legion/apps/kube-prometheus-stack.yaml new file mode 100644 index 0000000..7458ef5 --- /dev/null +++ b/servers/legion/apps/kube-prometheus-stack.yaml @@ -0,0 +1,108 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kube-prometheus-stack + namespace: argocd +spec: + project: default + source: + repoURL: https://prometheus-community.github.io/helm-charts + chart: kube-prometheus-stack + targetRevision: "*" + helm: + values: | + grafana: + adminPassword: + valueFrom: + secretKeyRef: + name: grafana-admin-secret + key: admin-password + persistence: + enabled: true + size: 5Gi + sidecar: + datasources: + defaultDatasourceEnabled: false + initChownData: + enabled: false + ingress: + enabled: true + ingressClassName: traefik + hosts: + - grafana.neuralplatform.ai + tls: + - secretName: grafana-tls + hosts: + - grafana.neuralplatform.ai + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod + prometheus: + prometheusSpec: + storageSpec: + volumeClaimTemplate: + spec: + resources: + requests: + storage: 20Gi + kubeEtcd: + enabled: false + kubeControllerManager: + enabled: false + kubeScheduler: + enabled: false + kubeProxy: + enabled: false + alertmanager: + config: + global: + resolve_timeout: 5m + inhibit_rules: + - source_matchers: [severity="critical"] + target_matchers: [severity=~"warning|info"] + equal: [namespace, alertname] + - source_matchers: [severity="warning"] + target_matchers: [severity="info"] + equal: [namespace, alertname] + receivers: + - name: "null" + - name: slack-alerts + slack_configs: + - channel: "#infrastructure-alerts" + api_url: "https://slack.com/api/chat.postMessage" + http_config: + authorization: + type: Bearer + credentials_file: /etc/alertmanager/secrets/slack-bot-token/bot_token + send_resolved: true + title: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} ({{ .CommonLabels.namespace }})' + text: | + {{ range .Alerts }} + *{{ if .Annotations.summary }}{{ .Annotations.summary }}{{ else }}{{ .Labels.alertname }}{{ end }}*{{ if .Labels.severity }} · {{ .Labels.severity }}{{ end }} + {{ if .Annotations.description }}{{ .Annotations.description }}{{ end }} + {{ end }} + footer: "Legion · neuralplatform.ai" + route: + group_by: [namespace, alertname] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + receiver: slack-alerts + routes: + - matchers: [alertname="Watchdog"] + receiver: "null" + - matchers: [severity="info"] + receiver: "null" + alertmanagerSpec: + secrets: + - slack-bot-token + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/servers/legion/apps/loki.yaml b/servers/legion/apps/loki.yaml new file mode 100644 index 0000000..9105f7b --- /dev/null +++ b/servers/legion/apps/loki.yaml @@ -0,0 +1,44 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: loki + namespace: argocd +spec: + project: default + source: + repoURL: https://grafana.github.io/helm-charts + chart: loki + targetRevision: "*" + helm: + values: | + loki: + commonConfig: + replication_factor: 1 + storage: + type: filesystem + useTestSchema: true + singleBinary: + replicas: 1 + persistence: + enabled: true + size: 10Gi + deploymentMode: SingleBinary + backend: + replicas: 0 + read: + replicas: 0 + write: + replicas: 0 + chunksCache: + enabled: false + resultsCache: + enabled: false + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/monitoring-config.yaml b/servers/legion/apps/monitoring-config.yaml new file mode 100644 index 0000000..f5853c7 --- /dev/null +++ b/servers/legion/apps/monitoring-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: monitoring-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/monitoring + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/neuron-config.yaml b/servers/legion/apps/neuron-config.yaml new file mode 100644 index 0000000..ef04a57 --- /dev/null +++ b/servers/legion/apps/neuron-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: neuron-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/neuron + destination: + server: https://kubernetes.default.svc + namespace: neuron + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/ollama-config.yaml b/servers/legion/apps/ollama-config.yaml new file mode 100644 index 0000000..76565b8 --- /dev/null +++ b/servers/legion/apps/ollama-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ollama-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/ollama + destination: + server: https://kubernetes.default.svc + namespace: ollama + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/packages-config.yaml b/servers/legion/apps/packages-config.yaml new file mode 100644 index 0000000..f753266 --- /dev/null +++ b/servers/legion/apps/packages-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: packages-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/packages + destination: + server: https://kubernetes.default.svc + namespace: packages + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/postgres-config.yaml b/servers/legion/apps/postgres-config.yaml new file mode 100644 index 0000000..0a60ba0 --- /dev/null +++ b/servers/legion/apps/postgres-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: postgres-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/postgres + destination: + server: https://kubernetes.default.svc + namespace: platform + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/postgres.yaml b/servers/legion/apps/postgres.yaml new file mode 100644 index 0000000..2480f96 --- /dev/null +++ b/servers/legion/apps/postgres.yaml @@ -0,0 +1,41 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: postgres + namespace: argocd +spec: + project: default + sources: + # Helm chart from Docker Hub OCI registry + - repoURL: registry-1.docker.io/bitnamicharts + chart: postgresql + targetRevision: "*" + helm: + values: | + auth: + existingSecret: postgres-passwords + secretKeys: + adminPasswordKey: postgres-password + primary: + persistence: + size: 20Gi + resources: + requests: + memory: 256Mi + cpu: 100m + initdb: + scripts: + create-kong-db.sql: | + CREATE DATABASE kong; + CREATE USER kong WITH PASSWORD '$(KONG_DB_PASSWORD)'; + GRANT ALL PRIVILEGES ON DATABASE kong TO kong; + ALTER DATABASE kong OWNER TO kong; + destination: + server: https://kubernetes.default.svc + namespace: platform + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/redis.yaml b/servers/legion/apps/redis.yaml new file mode 100644 index 0000000..d56c5e9 --- /dev/null +++ b/servers/legion/apps/redis.yaml @@ -0,0 +1,32 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: redis + namespace: argocd +spec: + project: default + source: + repoURL: registry-1.docker.io/bitnamicharts + chart: redis + targetRevision: "*" + helm: + values: | + auth: + enabled: false + architecture: standalone + master: + persistence: + size: 5Gi + resources: + requests: + memory: 128Mi + cpu: 50m + destination: + server: https://kubernetes.default.svc + namespace: platform + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/registry-config.yaml b/servers/legion/apps/registry-config.yaml new file mode 100644 index 0000000..b5e30f8 --- /dev/null +++ b/servers/legion/apps/registry-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: registry-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/registry + destination: + server: https://kubernetes.default.svc + namespace: registry + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/tempo.yaml b/servers/legion/apps/tempo.yaml new file mode 100644 index 0000000..6c86318 --- /dev/null +++ b/servers/legion/apps/tempo.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: tempo + namespace: argocd +spec: + project: default + source: + repoURL: https://grafana.github.io/helm-charts + chart: tempo + targetRevision: "*" + helm: + values: | + tempo: + storage: + trace: + backend: local + local: + path: /var/tempo + persistence: + enabled: true + size: 10Gi + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/vault-config.yaml b/servers/legion/apps/vault-config.yaml new file mode 100644 index 0000000..76476e2 --- /dev/null +++ b/servers/legion/apps/vault-config.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault-config + namespace: argocd +spec: + project: default + source: + repoURL: http://10.43.1.53:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/vault + destination: + server: https://kubernetes.default.svc + namespace: vault + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/apps/vault.yaml b/servers/legion/apps/vault.yaml new file mode 100644 index 0000000..ba36912 --- /dev/null +++ b/servers/legion/apps/vault.yaml @@ -0,0 +1,89 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault + namespace: argocd +spec: + project: default + source: + repoURL: https://helm.releases.hashicorp.com + chart: vault + targetRevision: "0.29.1" + helm: + values: | + server: + image: + repository: hashicorp/vault + tag: "1.18.3" + + extraEnvironmentVars: + GOOGLE_APPLICATION_CREDENTIALS: /vault/gcp/sa.json + + volumes: + - name: gcp-sa + secret: + secretName: vault-gcp-sa + + volumeMounts: + - name: gcp-sa + mountPath: /vault/gcp + readOnly: true + + ha: + enabled: false + + dataStorage: + enabled: true + size: 10Gi + + standalone: + enabled: true + config: | + ui = true + + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + } + + storage "file" { + path = "/vault/data" + } + + seal "gcpckms" { + project = "neuron-785695" + region = "global" + key_ring = "vault" + crypto_key = "vault-unseal" + } + + ingress: + enabled: true + ingressClassName: traefik + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: vault.neuralplatform.ai + paths: + - / + tls: + - secretName: vault-tls + hosts: + - vault.neuralplatform.ai + + ui: + enabled: true + + injector: + enabled: false + destination: + server: https://kubernetes.default.svc + namespace: vault + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/backup.tf b/servers/legion/backup.tf index 4250d1c..07f8b89 100644 --- a/servers/legion/backup.tf +++ b/servers/legion/backup.tf @@ -1,137 +1,2 @@ -# Legion backup — restic to Cloudflare R2 -# Dumps all Postgres databases + Gitea data nightly at 2am -# Retains 30 daily / 12 weekly / 6 monthly snapshots - -resource "kubernetes_secret" "backup_credentials" { - metadata { - name = "backup-credentials" - namespace = "git" - } - - data = { - RESTIC_PASSWORD = "gitea-restic-2026" - AWS_ACCESS_KEY_ID = var.r2_access_key_id - AWS_SECRET_ACCESS_KEY = var.r2_secret_access_key - RESTIC_REPOSITORY = "s3:${var.r2_endpoint}/legion-gitea-backup" - PGPASSWORD = var.postgres_password - } -} - -resource "kubernetes_cron_job_v1" "gitea_backup" { - metadata { - name = "gitea-backup" - namespace = "git" - } - - spec { - schedule = "0 2 * * *" - concurrency_policy = "Forbid" - failed_jobs_history_limit = 3 - successful_jobs_history_limit = 3 - - job_template { - metadata {} - spec { - backoff_limit = 2 - template { - metadata {} - spec { - restart_policy = "OnFailure" - - # Dump all Postgres databases before restic runs - init_container { - name = "pg-dump" - image = "postgres:18-alpine" - - command = ["/bin/sh", "-c", - "pg_dumpall -h postgres-postgresql.platform.svc.cluster.local -U postgres --no-role-passwords > /dump/all-databases.sql && echo 'DB dump complete, size:' $(du -sh /dump/all-databases.sql)" - ] - - env_from { - secret_ref { - name = kubernetes_secret.backup_credentials.metadata[0].name - } - } - - volume_mount { - name = "dump" - mount_path = "/dump" - } - - resources { - requests = { memory = "128Mi", cpu = "100m" } - limits = { memory = "256Mi" } - } - } - - container { - name = "backup" - image = "restic/restic:latest" - - command = ["/bin/sh", "-c", <<-EOT - set -e - - # Init repo if first run - restic snapshots 2>/dev/null || restic init - - # Back up Gitea data + full DB dump - echo "Running restic backup..." - restic backup \ - /gitea-data \ - /dump/all-databases.sql \ - --tag legion \ - --host legion - - # Prune old snapshots - restic forget \ - --keep-daily 30 \ - --keep-weekly 12 \ - --keep-monthly 6 \ - --prune - - echo "Backup complete." - restic snapshots --latest 3 - EOT - ] - - env_from { - secret_ref { - name = kubernetes_secret.backup_credentials.metadata[0].name - } - } - - volume_mount { - name = "gitea-data" - mount_path = "/gitea-data" - read_only = true - } - - volume_mount { - name = "dump" - mount_path = "/dump" - } - - resources { - requests = { memory = "256Mi", cpu = "100m" } - limits = { memory = "512Mi" } - } - } - - volume { - name = "gitea-data" - persistent_volume_claim { - claim_name = "gitea-data" - read_only = true - } - } - - volume { - name = "dump" - empty_dir {} - } - } - } - } - } - } -} +# Legion backup — CronJob and secret managed by Argo CD + ESO +# See: apps/backup.yaml, k8s/backup/ diff --git a/servers/legion/ddclient.tf b/servers/legion/ddclient.tf index 7814b06..cfabf35 100644 --- a/servers/legion/ddclient.tf +++ b/servers/legion/ddclient.tf @@ -1,73 +1,3 @@ -# ddclient — dynamic DNS daemon that keeps dot.nook.family pointing at the home public IP -# Updates Cloudflare A record every 5 minutes; required for DoT (port 853) which can't use Cloudflare proxy - -resource "kubernetes_secret" "ddclient_config" { - metadata { - name = "ddclient-config" - namespace = kubernetes_namespace.dns.metadata[0].name - } - - data = { - "ddclient.conf" = <<-EOT - daemon=300 - syslog=yes - pid=/var/run/ddclient/ddclient.pid - use=web, web=https://api.ipify.org - - protocol=cloudflare - zone=nook.family - ttl=120 - login=${var.cloudflare_email} - password=${var.cloudflare_api_key} - dot.nook.family - EOT - } -} - -resource "kubernetes_deployment" "ddclient" { - metadata { - name = "ddclient" - namespace = kubernetes_namespace.dns.metadata[0].name - labels = { app = "ddclient" } - } - - spec { - replicas = 1 - selector { - match_labels = { app = "ddclient" } - } - template { - metadata { - labels = { app = "ddclient" } - } - spec { - container { - name = "ddclient" - image = "ghcr.io/ddclient/ddclient:latest" - command = ["ddclient", "-file", "/etc/ddclient/ddclient.conf", "-daemon", "300", "-noquiet", "-foreground"] - - volume_mount { - name = "config" - mount_path = "/etc/ddclient/ddclient.conf" - sub_path = "ddclient.conf" - read_only = true - } - - resources { - requests = { memory = "32Mi", cpu = "10m" } - limits = { memory = "64Mi" } - } - } - - volume { - name = "config" - secret { - secret_name = kubernetes_secret.ddclient_config.metadata[0].name - } - } - } - } - } - - depends_on = [kubernetes_namespace.dns] -} +# ddclient — dynamic DNS daemon +# Deployment and config secret managed by Argo CD + ESO +# See: apps/adguard-config.yaml, k8s/adguard/ddclient-*.yaml diff --git a/servers/legion/gitea-runner.tf b/servers/legion/gitea-runner.tf index a6f60a7..9473a76 100644 --- a/servers/legion/gitea-runner.tf +++ b/servers/legion/gitea-runner.tf @@ -1,14 +1,2 @@ -# Gitea act-runner — executes Gitea Actions CI jobs on Legion -# Registered at the instance level so it can serve all repos/orgs - -resource "kubernetes_secret" "gitea_runner_secret" { - metadata { - name = "gitea-runner-secret" - namespace = kubernetes_namespace.ci.metadata[0].name - } - - data = { - GITEA_INSTANCE_URL = "https://git.neuralplatform.ai" - GITEA_RUNNER_REGISTRATION_TOKEN = var.gitea_runner_token - } -} +# Gitea act-runner — secrets managed by Argo CD + ESO +# See: apps/gitea-runner.yaml, apps/ci-config.yaml, k8s/gitea-runner/ diff --git a/servers/legion/gitea.tf b/servers/legion/gitea.tf index 5dcf535..797ca8c 100644 --- a/servers/legion/gitea.tf +++ b/servers/legion/gitea.tf @@ -1,117 +1,6 @@ # Gitea — local git server for private/sensitive infrastructure repos -# Accessible at git.neuralplatform.ai (internal only, real TLS) - -resource "kubernetes_config_map" "gitea_custom_css" { - metadata { - name = "gitea-custom-css" - namespace = kubernetes_namespace.git.metadata[0].name - } - - data = { - "header.tmpl" = <<-TMPL - - TMPL - - - "custom.css" = <<-CSS - /* ── Typography & base ── */ - :root { - --color-primary: #6366f1; - --color-primary-dark: #4f46e5; - --color-secondary: #8b5cf6; - --color-bg: #0f0f17; - --color-surface: #16161f; - --color-surface-2: #1e1e2e; - --color-border: #2a2a3d; - --color-text: #e2e2f0; - --color-text-muted: #8888aa; - --color-green: #22d3a5; - --radius: 10px; - } - - body { - background: var(--color-bg) !important; - color: var(--color-text) !important; - font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; - } - - /* ── Top navbar ── */ - .navbar, #navbar { - background: var(--color-surface) !important; - border-bottom: 1px solid var(--color-border) !important; - box-shadow: 0 1px 12px rgba(0,0,0,0.4) !important; - } - - .navbar .brand svg, .navbar .brand img { filter: brightness(1.2); } - - /* ── Sidebar & panels ── */ - .repository, .ui.container, .ui.segment, - .ui.card, .ui.cards > .card { - background: var(--color-surface) !important; - border: 1px solid var(--color-border) !important; - border-radius: var(--radius) !important; - } - - /* ── Buttons ── */ - .ui.primary.button, .ui.green.button { - background: var(--color-primary) !important; - border: none !important; - border-radius: 8px !important; - } - .ui.primary.button:hover { background: var(--color-primary-dark) !important; } - - /* ── Inputs ── */ - input, textarea, select, - .ui.input > input, .ui.dropdown { - background: var(--color-surface-2) !important; - border: 1px solid var(--color-border) !important; - color: var(--color-text) !important; - border-radius: 8px !important; - } - - /* ── Sign-in page ── */ - .user.signin .ui.segment, - .user.signup .ui.segment { - background: var(--color-surface) !important; - border: 1px solid var(--color-border) !important; - border-radius: 16px !important; - box-shadow: 0 8px 32px rgba(0,0,0,0.5) !important; - padding: 2.5rem !important; - } - - /* ── Repo file tree ── */ - .repository.file.list .file-list { - background: var(--color-surface) !important; - border-radius: var(--radius) !important; - border: 1px solid var(--color-border) !important; - } - - .repository.file.list .file-list tr:hover td { - background: var(--color-surface-2) !important; - } - - /* ── Labels & badges ── */ - .ui.label { border-radius: 6px !important; } - - /* ── Dashboard activity feed ── */ - .feeds .news { border-bottom: 1px solid var(--color-border) !important; } - - /* ── Code blocks ── */ - pre, code { - background: var(--color-surface-2) !important; - border: 1px solid var(--color-border) !important; - border-radius: 6px !important; - } - - /* ── Muted footer ── */ - #footer { - background: var(--color-surface) !important; - border-top: 1px solid var(--color-border) !important; - color: var(--color-text-muted) !important; - } - CSS - } -} +# Namespace only — all other resources managed by Argo CD + ESO +# See: apps/gitea.yaml, apps/gitea-config.yaml, k8s/gitea/ resource "kubernetes_namespace" "git" { metadata { @@ -122,66 +11,3 @@ resource "kubernetes_namespace" "git" { } } } - -resource "kubernetes_persistent_volume_claim" "gitea_data" { - wait_until_bound = false - metadata { - name = "gitea-data" - namespace = kubernetes_namespace.git.metadata[0].name - } - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "20Gi" - } - } - } -} - -resource "kubernetes_secret" "gitea_db_secret" { - metadata { - name = "gitea-db" - namespace = kubernetes_namespace.git.metadata[0].name - } - data = { - password = var.gitea_db_password - } -} - -# Deployment + Service managed by Argo CD — see servers/legion/apps/gitea.yaml - -resource "kubernetes_ingress_v1" "gitea" { - metadata { - name = "gitea" - namespace = kubernetes_namespace.git.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - - spec { - tls { - hosts = [var.gitea_domain] - secret_name = "gitea-tls" - } - rule { - host = var.gitea_domain - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "gitea" - port { number = 3000 } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} diff --git a/servers/legion/github-runner.tf b/servers/legion/github-runner.tf index 1a85d40..47de7ed 100644 --- a/servers/legion/github-runner.tf +++ b/servers/legion/github-runner.tf @@ -1,12 +1,2 @@ -# GitHub Actions Self-Hosted Runner — org-level, serves all repos -# Deployment managed by Argo CD — see servers/legion/apps/github-runner.yaml - -resource "kubernetes_secret" "github_runner_secret" { - metadata { - name = "github-runner-secret" - namespace = kubernetes_namespace.ci.metadata[0].name - } - data = { - ACCESS_TOKEN = var.github_classic_pat - } -} +# GitHub Actions Self-Hosted Runner — secrets managed by Argo CD + ESO +# See: apps/github-runner.yaml, apps/ci-config.yaml, k8s/github-runner/ diff --git a/servers/legion/headscale.tf b/servers/legion/headscale.tf index fe2bf7f..a882fd0 100644 --- a/servers/legion/headscale.tf +++ b/servers/legion/headscale.tf @@ -1,7 +1,6 @@ -# Headscale — self-hosted Tailscale control plane -# Coordination endpoint: https://headscale.neuralplatform.ai (Cloudflare tunnel) -# Clients: Tailscale app on Mac/iOS/Android, pointed at custom server -# Subnet router: tailscaled on Legion host, advertises k3s + LAN CIDRs +# Headscale — namespace only +# PVC, Deployment, Service managed by Argo CD +# See: apps/headscale.yaml, apps/headscale-config.yaml, k8s/headscale/ resource "kubernetes_namespace" "headscale" { metadata { @@ -12,22 +11,3 @@ resource "kubernetes_namespace" "headscale" { } } } - -resource "kubernetes_persistent_volume_claim" "headscale_data" { - metadata { - name = "headscale-data" - namespace = kubernetes_namespace.headscale.metadata[0].name - } - - # wait_until_bound = false because local-path only provisions on pod schedule - wait_until_bound = false - - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "1Gi" - } - } - } -} diff --git a/servers/legion/ingress.tf b/servers/legion/ingress.tf index 480be40..13511b9 100644 --- a/servers/legion/ingress.tf +++ b/servers/legion/ingress.tf @@ -1,243 +1,4 @@ -# Traefik ingress routes for all services — TLS via cert-manager + Cloudflare DNS-01 -# Kong and open-webui ingresses moved to inactive/ — restore when needed - -resource "kubernetes_ingress_v1" "ollama" { - metadata { - name = "ollama" - namespace = kubernetes_namespace.ollama.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - - spec { - tls { - hosts = ["ollama.${var.infra_domain}"] - secret_name = "ollama-tls" - } - rule { - host = "ollama.${var.infra_domain}" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "ollama" - port { number = 11434 } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} - - -resource "kubernetes_ingress_v1" "registry" { - metadata { - name = "registry" - namespace = kubernetes_namespace.registry.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - "traefik.ingress.kubernetes.io/router.middlewares" = "registry-chunked@kubernetescrd" - "nginx.ingress.kubernetes.io/proxy-body-size" = "0" - "nginx.ingress.kubernetes.io/proxy-read-timeout" = "600" - "nginx.ingress.kubernetes.io/proxy-send-timeout" = "600" - } - } - - spec { - tls { - hosts = ["registry.${var.infra_domain}"] - secret_name = "registry-tls" - } - rule { - host = "registry.${var.infra_domain}" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "registry" - port { number = 5000 } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} - -resource "kubernetes_ingress_v1" "registry_ui" { - metadata { - name = "registry-ui" - namespace = kubernetes_namespace.registry.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - - spec { - tls { - hosts = ["docker.neuralplatform.ai"] - secret_name = "docker-ui-tls" - } - rule { - host = "docker.neuralplatform.ai" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "registry-ui" - port { number = 80 } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} - -# npm registry — Verdaccio (proxies npmjs.org, caches locally) -resource "kubernetes_ingress_v1" "npm_registry" { - metadata { - name = "npm-registry" - namespace = kubernetes_namespace.packages.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - - spec { - tls { - hosts = ["npm.neuralplatform.ai"] - secret_name = "npm-registry-tls" - } - rule { - host = "npm.neuralplatform.ai" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "verdaccio" - port { number = 4873 } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} - -# Alloy OTLP HTTP ingress — external log/trace ingestion from RunPod pods -# Auth: HTTP Basic Auth (username: pantheon, password: ALLOY_PUSH_TOKEN from Vault) -# Pods set: Authorization: Basic base64(pantheon:) -# Traefik middleware strips auth before forwarding to Alloy. -resource "kubernetes_manifest" "alloy_otlp_basicauth_middleware" { - manifest = { - apiVersion = "traefik.io/v1alpha1" - kind = "Middleware" - metadata = { - name = "alloy-otlp-auth" - namespace = "monitoring" - } - spec = { - basicAuth = { - secret = "alloy-otlp-auth" - } - } - } - depends_on = [kubernetes_secret.alloy_otlp_auth] -} - -resource "kubernetes_ingress_v1" "alloy_otlp" { - metadata { - name = "alloy-otlp" - namespace = "monitoring" - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "traefik.ingress.kubernetes.io/router.middlewares" = "monitoring-alloy-otlp-auth@kubernetescrd" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - spec { - tls { - hosts = ["alloy.${var.infra_domain}"] - secret_name = "alloy-otlp-tls" - } - rule { - host = "alloy.${var.infra_domain}" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "alloy-otlp" - port { number = 4318 } - } - } - } - } - } - } - depends_on = [ - kubernetes_manifest.alloy_otlp_basicauth_middleware, - kubernetes_service.alloy_otlp, - helm_release.cert_manager, - ] -} - -# PyPI registry — devpi (proxies pypi.org, caches locally) -resource "kubernetes_ingress_v1" "pypi_registry" { - metadata { - name = "pypi-registry" - namespace = kubernetes_namespace.packages.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - - spec { - tls { - hosts = ["pypi.neuralplatform.ai"] - secret_name = "pypi-registry-tls" - } - rule { - host = "pypi.neuralplatform.ai" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "devpi" - port { number = 3141 } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} +# Ingresses — all moved to k8s//ingress.yaml, managed by Argo CD +# See: k8s/neuron/ingress.yaml, k8s/gitea/ingress.yaml, k8s/ollama/ingress.yaml, +# k8s/registry/ingress.yaml, k8s/packages/ingress.yaml, +# k8s/monitoring/ingress.yaml, k8s/adguard/ingress.yaml diff --git a/servers/legion/k8s/adguard/certificate.yaml b/servers/legion/k8s/adguard/certificate.yaml new file mode 100644 index 0000000..fd69de2 --- /dev/null +++ b/servers/legion/k8s/adguard/certificate.yaml @@ -0,0 +1,15 @@ +--- +# TLS certificate for DoT (DNS-over-TLS) on dot.nook.family:853 +# Issued via cert-manager DNS-01 challenge (Cloudflare) +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: adguard-dot-tls + namespace: dns +spec: + secretName: adguard-dot-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - dot.nook.family diff --git a/servers/legion/k8s/adguard/configmap.yaml b/servers/legion/k8s/adguard/configmap.yaml new file mode 100644 index 0000000..359f04d --- /dev/null +++ b/servers/legion/k8s/adguard/configmap.yaml @@ -0,0 +1,59 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: adguard-defaults + namespace: dns +data: + AdGuardHome.yaml: | + dns: + bind_hosts: + - 0.0.0.0 + port: 53 + upstream_dns: + - https://dns.cloudflare.com/dns-query + - https://dns.google/dns-query + bootstrap_dns: + - 1.1.1.1 + - 8.8.8.8 + - 9.9.9.10 + - 149.112.112.10 + upstream_mode: load_balance + cache_enabled: true + cache_size: 4194304 + filters: + - enabled: true + url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt + name: AdGuard DNS filter + id: 1 + - enabled: true + url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt + name: AdAway Default Blocklist + id: 2 + - enabled: true + url: https://big.oisd.nl/domainswild + name: OISD Big + id: 3 + - enabled: true + url: https://easylist.to/easylist/easylist.txt + name: EasyList + id: 4 + - enabled: true + url: https://easylist.to/easylist/easyprivacy.txt + name: EasyPrivacy + id: 5 + tls: + enabled: true + server_name: dot.nook.family + port_dns_over_tls: 853 + certificate_path: /etc/adguard/tls/tls.crt + private_key_path: /etc/adguard/tls/tls.key + allow_unencrypted_doh: true + filtering: + filtering_enabled: true + rewrites: + - domain: '*.nook.family' + answer: 192.168.68.77 + enabled: true + - domain: '*.neuralplatform.ai' + answer: 192.168.68.77 + enabled: true diff --git a/servers/legion/k8s/adguard/ddclient-deployment.yaml b/servers/legion/k8s/adguard/ddclient-deployment.yaml new file mode 100644 index 0000000..dbdcc34 --- /dev/null +++ b/servers/legion/k8s/adguard/ddclient-deployment.yaml @@ -0,0 +1,39 @@ +--- +# ddclient — dynamic DNS daemon keeping dot.nook.family pointed at home public IP +# Updates Cloudflare A record every 5 minutes (required for DoT port 853, no CF proxy) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ddclient + namespace: dns + labels: + app: ddclient +spec: + replicas: 1 + selector: + matchLabels: + app: ddclient + template: + metadata: + labels: + app: ddclient + spec: + containers: + - name: ddclient + image: ghcr.io/ddclient/ddclient:latest + command: ["ddclient", "-file", "/etc/ddclient/ddclient.conf", "-daemon", "300", "-noquiet", "-foreground"] + volumeMounts: + - name: config + mountPath: /etc/ddclient/ddclient.conf + subPath: ddclient.conf + readOnly: true + resources: + requests: + memory: 32Mi + cpu: 10m + limits: + memory: 64Mi + volumes: + - name: config + secret: + secretName: ddclient-config diff --git a/servers/legion/k8s/adguard/ddclient-external-secret.yaml b/servers/legion/k8s/adguard/ddclient-external-secret.yaml new file mode 100644 index 0000000..6e3939a --- /dev/null +++ b/servers/legion/k8s/adguard/ddclient-external-secret.yaml @@ -0,0 +1,39 @@ +--- +# ddclient-config — Cloudflare credentials for dynamic DNS updates +# cloudflare_api_key and cloudflare_email stored in Vault at secret/cloudflare +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: ddclient-config + namespace: dns +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: ddclient-config + creationPolicy: Owner + template: + data: + ddclient.conf: | + daemon=300 + syslog=yes + pid=/var/run/ddclient/ddclient.pid + use=web, web=https://api.ipify.org + + protocol=cloudflare + zone=nook.family + ttl=120 + login={{ .cloudflare_email }} + password={{ .cloudflare_api_key }} + dot.nook.family + data: + - secretKey: cloudflare_api_key + remoteRef: + key: secret/data/cloudflare + property: api_key + - secretKey: cloudflare_email + remoteRef: + key: secret/data/cloudflare + property: email diff --git a/servers/legion/k8s/adguard/ingress.yaml b/servers/legion/k8s/adguard/ingress.yaml new file mode 100644 index 0000000..3873b91 --- /dev/null +++ b/servers/legion/k8s/adguard/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: adguard + namespace: dns + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - dns.nook.family + secretName: adguard-tls + rules: + - host: dns.nook.family + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: adguard-ui + port: + number: 3000 diff --git a/servers/legion/k8s/adguard/pvc.yaml b/servers/legion/k8s/adguard/pvc.yaml new file mode 100644 index 0000000..5a24387 --- /dev/null +++ b/servers/legion/k8s/adguard/pvc.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: adguard-config + namespace: dns +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: adguard-data + namespace: dns +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi diff --git a/servers/legion/k8s/backup/cronjob.yaml b/servers/legion/k8s/backup/cronjob.yaml new file mode 100644 index 0000000..283d943 --- /dev/null +++ b/servers/legion/k8s/backup/cronjob.yaml @@ -0,0 +1,95 @@ +--- +# Nightly backup — dumps Postgres + Gitea data to Cloudflare R2 via restic +# Schedule: 2am daily. Retains 30 daily / 12 weekly / 6 monthly snapshots. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: gitea-backup + namespace: git +spec: + schedule: "0 2 * * *" + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 3 + successfulJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + spec: + restartPolicy: OnFailure + initContainers: + - name: pg-dump + image: postgres:18-alpine + command: + - /bin/sh + - -c + - >- + pg_dumpall + -h postgres-postgresql.platform.svc.cluster.local + -U postgres + --no-role-passwords + > /dump/all-databases.sql + && echo 'DB dump complete, size:' $(du -sh /dump/all-databases.sql) + envFrom: + - secretRef: + name: backup-credentials + volumeMounts: + - name: dump + mountPath: /dump + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 256Mi + containers: + - name: backup + image: restic/restic:latest + command: + - /bin/sh + - -c + - | + set -e + + # Init repo if first run + restic snapshots 2>/dev/null || restic init + + # Back up Gitea data + full DB dump + echo "Running restic backup..." + restic backup \ + /gitea-data \ + /dump/all-databases.sql \ + --tag legion \ + --host legion + + # Prune old snapshots + restic forget \ + --keep-daily 30 \ + --keep-weekly 12 \ + --keep-monthly 6 \ + --prune + + echo "Backup complete." + restic snapshots --latest 3 + envFrom: + - secretRef: + name: backup-credentials + volumeMounts: + - name: gitea-data + mountPath: /gitea-data + readOnly: true + - name: dump + mountPath: /dump + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + volumes: + - name: gitea-data + persistentVolumeClaim: + claimName: gitea-data + readOnly: true + - name: dump + emptyDir: {} diff --git a/servers/legion/k8s/backup/external-secrets.yaml b/servers/legion/k8s/backup/external-secrets.yaml new file mode 100644 index 0000000..7c443eb --- /dev/null +++ b/servers/legion/k8s/backup/external-secrets.yaml @@ -0,0 +1,35 @@ +--- +# backup-credentials — R2 access keys + restic password + Postgres password for nightly backup +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: backup-credentials + namespace: git +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: backup-credentials + creationPolicy: Owner + template: + data: + RESTIC_PASSWORD: "gitea-restic-2026" + AWS_ACCESS_KEY_ID: "{{ .r2_access_key_id }}" + AWS_SECRET_ACCESS_KEY: "{{ .r2_secret_access_key }}" + RESTIC_REPOSITORY: "s3:https://651161e0a3d321561b4c90b5bcd5f15b.r2.cloudflarestorage.com/legion-gitea-backup" + PGPASSWORD: "{{ .postgres_password }}" + data: + - secretKey: r2_access_key_id + remoteRef: + key: secret/data/r2 + property: access_key_id + - secretKey: r2_secret_access_key + remoteRef: + key: secret/data/r2 + property: secret_access_key + - secretKey: postgres_password + remoteRef: + key: secret/data/legion-db + property: postgres_password diff --git a/servers/legion/k8s/external-secrets/cluster-secret-store.yaml b/servers/legion/k8s/external-secrets/cluster-secret-store.yaml new file mode 100644 index 0000000..4c16fdf --- /dev/null +++ b/servers/legion/k8s/external-secrets/cluster-secret-store.yaml @@ -0,0 +1,23 @@ +--- +# ClusterSecretStore — connects ESO to Vault using the root token Secret. +# The vault-token Secret (name: vault-token, key: token) must be created +# via kubectl as a one-time bootstrap step before this store can sync: +# +# kubectl create secret generic vault-token \ +# --namespace external-secrets \ +# --from-literal=token=$(cat ~/Secrets/tokens/vault-root-token) +apiVersion: external-secrets.io/v1beta1 +kind: ClusterSecretStore +metadata: + name: vault +spec: + provider: + vault: + server: "https://vault.neuralplatform.ai" + path: "secret" + version: "v2" + auth: + tokenSecretRef: + name: vault-token + namespace: external-secrets + key: token diff --git a/servers/legion/k8s/gitea-runner/external-secrets.yaml b/servers/legion/k8s/gitea-runner/external-secrets.yaml new file mode 100644 index 0000000..a5a9a39 --- /dev/null +++ b/servers/legion/k8s/gitea-runner/external-secrets.yaml @@ -0,0 +1,24 @@ +--- +# gitea-runner-secret — Gitea act-runner registration token +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: gitea-runner-secret + namespace: ci +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: gitea-runner-secret + creationPolicy: Owner + data: + - secretKey: GITEA_INSTANCE_URL + remoteRef: + key: secret/data/gitea + property: instance_url + - secretKey: GITEA_RUNNER_REGISTRATION_TOKEN + remoteRef: + key: secret/data/gitea + property: runner_token diff --git a/servers/legion/k8s/gitea/configmap.yaml b/servers/legion/k8s/gitea/configmap.yaml new file mode 100644 index 0000000..518170d --- /dev/null +++ b/servers/legion/k8s/gitea/configmap.yaml @@ -0,0 +1,104 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: gitea-custom-css + namespace: git +data: + header.tmpl: | + + custom.css: | + /* ── Typography & base ── */ + :root { + --color-primary: #6366f1; + --color-primary-dark: #4f46e5; + --color-secondary: #8b5cf6; + --color-bg: #0f0f17; + --color-surface: #16161f; + --color-surface-2: #1e1e2e; + --color-border: #2a2a3d; + --color-text: #e2e2f0; + --color-text-muted: #8888aa; + --color-green: #22d3a5; + --radius: 10px; + } + + body { + background: var(--color-bg) !important; + color: var(--color-text) !important; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; + } + + /* ── Top navbar ── */ + .navbar, #navbar { + background: var(--color-surface) !important; + border-bottom: 1px solid var(--color-border) !important; + box-shadow: 0 1px 12px rgba(0,0,0,0.4) !important; + } + + .navbar .brand svg, .navbar .brand img { filter: brightness(1.2); } + + /* ── Sidebar & panels ── */ + .repository, .ui.container, .ui.segment, + .ui.card, .ui.cards > .card { + background: var(--color-surface) !important; + border: 1px solid var(--color-border) !important; + border-radius: var(--radius) !important; + } + + /* ── Buttons ── */ + .ui.primary.button, .ui.green.button { + background: var(--color-primary) !important; + border: none !important; + border-radius: 8px !important; + } + .ui.primary.button:hover { background: var(--color-primary-dark) !important; } + + /* ── Inputs ── */ + input, textarea, select, + .ui.input > input, .ui.dropdown { + background: var(--color-surface-2) !important; + border: 1px solid var(--color-border) !important; + color: var(--color-text) !important; + border-radius: 8px !important; + } + + /* ── Sign-in page ── */ + .user.signin .ui.segment, + .user.signup .ui.segment { + background: var(--color-surface) !important; + border: 1px solid var(--color-border) !important; + border-radius: 16px !important; + box-shadow: 0 8px 32px rgba(0,0,0,0.5) !important; + padding: 2.5rem !important; + } + + /* ── Repo file tree ── */ + .repository.file.list .file-list { + background: var(--color-surface) !important; + border-radius: var(--radius) !important; + border: 1px solid var(--color-border) !important; + } + + .repository.file.list .file-list tr:hover td { + background: var(--color-surface-2) !important; + } + + /* ── Labels & badges ── */ + .ui.label { border-radius: 6px !important; } + + /* ── Dashboard activity feed ── */ + .feeds .news { border-bottom: 1px solid var(--color-border) !important; } + + /* ── Code blocks ── */ + pre, code { + background: var(--color-surface-2) !important; + border: 1px solid var(--color-border) !important; + border-radius: 6px !important; + } + + /* ── Muted footer ── */ + #footer { + background: var(--color-surface) !important; + border-top: 1px solid var(--color-border) !important; + color: var(--color-text-muted) !important; + } diff --git a/servers/legion/k8s/gitea/external-secrets.yaml b/servers/legion/k8s/gitea/external-secrets.yaml new file mode 100644 index 0000000..a239bdb --- /dev/null +++ b/servers/legion/k8s/gitea/external-secrets.yaml @@ -0,0 +1,20 @@ +--- +# gitea-db — password used by the Gitea pod to connect to Postgres +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: gitea-db + namespace: git +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: gitea-db + creationPolicy: Owner + data: + - secretKey: password + remoteRef: + key: secret/data/gitea + property: db_password diff --git a/servers/legion/k8s/gitea/ingress.yaml b/servers/legion/k8s/gitea/ingress.yaml new file mode 100644 index 0000000..dddfda7 --- /dev/null +++ b/servers/legion/k8s/gitea/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gitea + namespace: git + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - git.neuralplatform.ai + secretName: gitea-tls + rules: + - host: git.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gitea + port: + number: 3000 diff --git a/servers/legion/k8s/gitea/pvc.yaml b/servers/legion/k8s/gitea/pvc.yaml new file mode 100644 index 0000000..3de652c --- /dev/null +++ b/servers/legion/k8s/gitea/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gitea-data + namespace: git +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi diff --git a/servers/legion/k8s/github-runner/external-secrets.yaml b/servers/legion/k8s/github-runner/external-secrets.yaml new file mode 100644 index 0000000..e924bf0 --- /dev/null +++ b/servers/legion/k8s/github-runner/external-secrets.yaml @@ -0,0 +1,20 @@ +--- +# github-runner-secret — GitHub classic PAT for runner registration +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: github-runner-secret + namespace: ci +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: github-runner-secret + creationPolicy: Owner + data: + - secretKey: ACCESS_TOKEN + remoteRef: + key: secret/data/github + property: classic_pat diff --git a/servers/legion/k8s/headscale/pvc.yaml b/servers/legion/k8s/headscale/pvc.yaml new file mode 100644 index 0000000..d6c1e43 --- /dev/null +++ b/servers/legion/k8s/headscale/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: headscale-data + namespace: headscale +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/servers/legion/k8s/monitoring/alloy-otlp-middleware.yaml b/servers/legion/k8s/monitoring/alloy-otlp-middleware.yaml new file mode 100644 index 0000000..a517209 --- /dev/null +++ b/servers/legion/k8s/monitoring/alloy-otlp-middleware.yaml @@ -0,0 +1,11 @@ +--- +# Traefik BasicAuth middleware — strips auth before forwarding to Alloy +# Secret alloy-otlp-auth is synced from Vault via ExternalSecret (see external-secrets.yaml) +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: alloy-otlp-auth + namespace: monitoring +spec: + basicAuth: + secret: alloy-otlp-auth diff --git a/servers/legion/k8s/monitoring/alloy-otlp-service.yaml b/servers/legion/k8s/monitoring/alloy-otlp-service.yaml new file mode 100644 index 0000000..f9283dd --- /dev/null +++ b/servers/legion/k8s/monitoring/alloy-otlp-service.yaml @@ -0,0 +1,18 @@ +--- +# Expose Alloy's OTLP HTTP port (4318) for external log/trace ingestion from RunPod pods +apiVersion: v1 +kind: Service +metadata: + name: alloy-otlp + namespace: monitoring + labels: + app: alloy-otlp +spec: + selector: + app.kubernetes.io/name: alloy + ports: + - name: otlp-http + port: 4318 + targetPort: 4318 + protocol: TCP + type: ClusterIP diff --git a/servers/legion/k8s/monitoring/external-secrets.yaml b/servers/legion/k8s/monitoring/external-secrets.yaml new file mode 100644 index 0000000..ab689fc --- /dev/null +++ b/servers/legion/k8s/monitoring/external-secrets.yaml @@ -0,0 +1,23 @@ +--- +# alloy-otlp-auth — bcrypt htpasswd entry for Traefik BasicAuth middleware +# Username: pantheon, password: alloy push token from Vault secret/alloy +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: alloy-otlp-auth + namespace: monitoring +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: alloy-otlp-auth + creationPolicy: Owner + # ESO does not bcrypt — the htpasswd entry must be pre-hashed in Vault. + # Store via: vault kv patch secret/alloy htpasswd_entry="pantheon:$(htpasswd -nbB pantheon | cut -d: -f2)" + data: + - secretKey: users + remoteRef: + key: secret/data/alloy + property: htpasswd_entry diff --git a/servers/legion/k8s/monitoring/grafana-datasources.yaml b/servers/legion/k8s/monitoring/grafana-datasources.yaml new file mode 100644 index 0000000..948de89 --- /dev/null +++ b/servers/legion/k8s/monitoring/grafana-datasources.yaml @@ -0,0 +1,37 @@ +--- +# Grafana extra datasources — Loki and Tempo with cross-linking +# Discovered by Grafana sidecar via label grafana_datasource=1 +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-extra-datasources + namespace: monitoring + labels: + grafana_datasource: "1" +data: + datasources.yaml: | + apiVersion: 1 + datasources: + - name: Loki + type: loki + uid: loki + url: http://loki-gateway.monitoring.svc.cluster.local + access: proxy + jsonData: + httpHeaderName1: X-Scope-OrgID + secureJsonData: + httpHeaderValue1: legion + - name: Tempo + type: tempo + uid: tempo + url: http://tempo.monitoring.svc.cluster.local:3100 + access: proxy + jsonData: + lokiSearch: + datasourceUid: loki + tracesToLogsV2: + datasourceUid: loki + filterByTraceID: true + filterBySpanID: false + tracesToMetrics: + datasourceUid: prometheus diff --git a/servers/legion/k8s/monitoring/grafana-external-secrets.yaml b/servers/legion/k8s/monitoring/grafana-external-secrets.yaml new file mode 100644 index 0000000..b52d810 --- /dev/null +++ b/servers/legion/k8s/monitoring/grafana-external-secrets.yaml @@ -0,0 +1,40 @@ +--- +# grafana-admin-secret — Grafana admin password from Vault +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: grafana-admin-secret + namespace: monitoring +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: grafana-admin-secret + creationPolicy: Owner + data: + - secretKey: admin-password + remoteRef: + key: secret/data/grafana + property: admin_password +--- +# slack-bot-token — Slack bot token for Alertmanager notifications +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: slack-bot-token + namespace: monitoring +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: slack-bot-token + creationPolicy: Owner + data: + - secretKey: bot_token + remoteRef: + key: secret/data/slack + property: bot_token diff --git a/servers/legion/k8s/monitoring/ingress.yaml b/servers/legion/k8s/monitoring/ingress.yaml new file mode 100644 index 0000000..1af264a --- /dev/null +++ b/servers/legion/k8s/monitoring/ingress.yaml @@ -0,0 +1,29 @@ +--- +# Alloy OTLP HTTP ingress — external log/trace ingestion from RunPod pods +# Auth: HTTP Basic Auth (username: pantheon, password: ALLOY_PUSH_TOKEN) +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: alloy-otlp + namespace: monitoring + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.middlewares: monitoring-alloy-otlp-auth@kubernetescrd + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - alloy.neuralplatform.ai + secretName: alloy-otlp-tls + rules: + - host: alloy.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: alloy-otlp + port: + number: 4318 diff --git a/servers/legion/k8s/neuron/configmap.yaml b/servers/legion/k8s/neuron/configmap.yaml new file mode 100644 index 0000000..a1f7cb0 --- /dev/null +++ b/servers/legion/k8s/neuron/configmap.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: neuron-config + namespace: neuron +data: + MCP_PORT: "8001" + MCP_HOST: "0.0.0.0" + WEBHOOK_PORT: "3847" + NEURON_DB_PATH: "/data/neuron.db" + AXON_DB_PATH: "/data/axon.db" + TUNNEL_NAME: "neural-platform" + default.yaml: | + runtime: + name: neuron + version: 0.1.0 + storage: + db_path: /data/neuron.db + cache: + max_l1_size: 1000 + gc: + gen0_threshold: 3 + gen1_threshold: 5 + knowledge: + base_path: /data/knowledge + synapse: + path: /data/synapse diff --git a/servers/legion/k8s/neuron/external-secrets.yaml b/servers/legion/k8s/neuron/external-secrets.yaml new file mode 100644 index 0000000..c9a70e1 --- /dev/null +++ b/servers/legion/k8s/neuron/external-secrets.yaml @@ -0,0 +1,52 @@ +--- +# neuron-secrets — GITHUB_WEBHOOK_SECRET, GITEA_WEBHOOK_SECRET, SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: neuron-secrets + namespace: neuron +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: neuron-secrets + creationPolicy: Owner + data: + - secretKey: GITHUB_WEBHOOK_SECRET + remoteRef: + key: secret/data/github + property: webhook_secret + - secretKey: GITEA_WEBHOOK_SECRET + remoteRef: + key: secret/data/gitea + property: api_token + - secretKey: SLACK_BOT_TOKEN + remoteRef: + key: secret/data/slack + property: bot_token + - secretKey: SLACK_SIGNING_SECRET + remoteRef: + key: secret/data/slack + property: signing_secret +--- +# cloudflared-secret — TUNNEL_TOKEN for the Cloudflare tunnel +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: cloudflared-secret + namespace: neuron +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: cloudflared-secret + creationPolicy: Owner + data: + - secretKey: TUNNEL_TOKEN + remoteRef: + key: secret/data/cloudflare-tunnel + property: token diff --git a/servers/legion/k8s/neuron/ingress.yaml b/servers/legion/k8s/neuron/ingress.yaml new file mode 100644 index 0000000..aac1b56 --- /dev/null +++ b/servers/legion/k8s/neuron/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: neuron + namespace: neuron + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - secretName: neuron-tls + hosts: + - neuron.neuralplatform.ai + rules: + - host: neuron.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: neuron + port: + number: 8001 diff --git a/servers/legion/k8s/neuron/pvc.yaml b/servers/legion/k8s/neuron/pvc.yaml new file mode 100644 index 0000000..5926d0e --- /dev/null +++ b/servers/legion/k8s/neuron/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: neuron-data + namespace: neuron +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi diff --git a/servers/legion/k8s/ollama/ingress.yaml b/servers/legion/k8s/ollama/ingress.yaml new file mode 100644 index 0000000..500d0bc --- /dev/null +++ b/servers/legion/k8s/ollama/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ollama + namespace: ollama + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - ollama.neuralplatform.ai + secretName: ollama-tls + rules: + - host: ollama.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ollama + port: + number: 11434 diff --git a/servers/legion/k8s/ollama/pvc.yaml b/servers/legion/k8s/ollama/pvc.yaml new file mode 100644 index 0000000..1493199 --- /dev/null +++ b/servers/legion/k8s/ollama/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ollama-models + namespace: ollama +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi diff --git a/servers/legion/k8s/packages/configmap.yaml b/servers/legion/k8s/packages/configmap.yaml new file mode 100644 index 0000000..bea3ed8 --- /dev/null +++ b/servers/legion/k8s/packages/configmap.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: verdaccio-config + namespace: packages +data: + config.yaml: | + storage: /verdaccio/storage + auth: + htpasswd: + file: /verdaccio/storage/htpasswd + max_users: 1 + uplinks: + npmjs: + url: https://registry.npmjs.org/ + cache: true + packages: + '@harmonic-framework/*': + access: $authenticated + publish: $authenticated + unpublish: $authenticated + '**': + access: $all + publish: $authenticated + proxy: npmjs + listen: 0.0.0.0:4873 + server: + keepAliveTimeout: 60 + log: + type: stdout + format: pretty + level: http diff --git a/servers/legion/k8s/packages/ingress.yaml b/servers/legion/k8s/packages/ingress.yaml new file mode 100644 index 0000000..6ef4738 --- /dev/null +++ b/servers/legion/k8s/packages/ingress.yaml @@ -0,0 +1,54 @@ +--- +# npm registry — Verdaccio +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: npm-registry + namespace: packages + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - npm.neuralplatform.ai + secretName: npm-registry-tls + rules: + - host: npm.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: verdaccio + port: + number: 4873 +--- +# PyPI registry — devpi +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pypi-registry + namespace: packages + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - pypi.neuralplatform.ai + secretName: pypi-registry-tls + rules: + - host: pypi.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: devpi + port: + number: 3141 diff --git a/servers/legion/k8s/packages/pvc.yaml b/servers/legion/k8s/packages/pvc.yaml new file mode 100644 index 0000000..f9c2ea0 --- /dev/null +++ b/servers/legion/k8s/packages/pvc.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: verdaccio-data + namespace: packages +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: devpi-data + namespace: packages +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/servers/legion/k8s/postgres/external-secrets.yaml b/servers/legion/k8s/postgres/external-secrets.yaml new file mode 100644 index 0000000..e8c7829 --- /dev/null +++ b/servers/legion/k8s/postgres/external-secrets.yaml @@ -0,0 +1,25 @@ +--- +# postgres-passwords — Postgres superuser + Kong DB passwords from Vault +# These are passed to the Bitnami postgresql chart via the app's Helm values. +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: postgres-passwords + namespace: platform +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: postgres-passwords + creationPolicy: Owner + data: + - secretKey: postgres-password + remoteRef: + key: secret/data/legion-db + property: postgres_password + - secretKey: kong-db-password + remoteRef: + key: secret/data/legion-db + property: kong_db_password diff --git a/servers/legion/k8s/registry/ingress.yaml b/servers/legion/k8s/registry/ingress.yaml new file mode 100644 index 0000000..fdb782e --- /dev/null +++ b/servers/legion/k8s/registry/ingress.yaml @@ -0,0 +1,58 @@ +--- +# Docker registry ingress with chunked upload middleware +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: registry + namespace: registry + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: registry-chunked@kubernetescrd + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" +spec: + ingressClassName: traefik + tls: + - hosts: + - registry.neuralplatform.ai + secretName: registry-tls + rules: + - host: registry.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: registry + port: + number: 5000 +--- +# Docker registry UI +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: registry-ui + namespace: registry + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - docker.neuralplatform.ai + secretName: docker-ui-tls + rules: + - host: docker.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: registry-ui + port: + number: 80 diff --git a/servers/legion/k8s/registry/pvc.yaml b/servers/legion/k8s/registry/pvc.yaml new file mode 100644 index 0000000..91941e6 --- /dev/null +++ b/servers/legion/k8s/registry/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: registry-data + namespace: registry +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi diff --git a/servers/legion/k8s/vault/gcp-sa-external-secret.yaml b/servers/legion/k8s/vault/gcp-sa-external-secret.yaml new file mode 100644 index 0000000..8f9eec0 --- /dev/null +++ b/servers/legion/k8s/vault/gcp-sa-external-secret.yaml @@ -0,0 +1,22 @@ +--- +# vault-gcp-sa — GCP service account JSON for KMS auto-unseal +# The SA JSON is stored in Vault at secret/data/vault-gcp-sa +# Store it: vault kv put secret/vault-gcp-sa sa_json=@vault-gcp-sa.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: vault-gcp-sa + namespace: vault +spec: + refreshInterval: 24h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: vault-gcp-sa + creationPolicy: Owner + data: + - secretKey: sa.json + remoteRef: + key: secret/data/vault-gcp-sa + property: sa_json diff --git a/servers/legion/monitoring.tf b/servers/legion/monitoring.tf index 5db4f83..1787997 100644 --- a/servers/legion/monitoring.tf +++ b/servers/legion/monitoring.tf @@ -1,8 +1,7 @@ -# Monitoring stack — Prometheus, Grafana, Loki, Tempo, Alloy, Alertmanager -# Grafana UI: https://grafana.neuralplatform.ai (admin / var.grafana_admin_password) -# -# TODO: Migrate Tempo to tempo-distributed once R2 is wired as trace storage backend -# TODO: Replace loki.useTestSchema=true with proper schema_config for production +# Monitoring stack — Prometheus, Grafana, Loki, Tempo, Alloy +# Namespace only — all Helm releases and config managed by Argo CD + ESO +# See: apps/kube-prometheus-stack.yaml, apps/loki.yaml, apps/tempo.yaml, apps/alloy.yaml +# See: apps/monitoring-config.yaml, k8s/monitoring/ resource "kubernetes_namespace" "monitoring" { metadata { @@ -13,372 +12,3 @@ resource "kubernetes_namespace" "monitoring" { } } } - -# kube-prometheus-stack — Prometheus, Grafana, Alertmanager, node exporters -resource "helm_release" "kube_prometheus_stack" { - name = "kube-prometheus-stack" - namespace = kubernetes_namespace.monitoring.metadata[0].name - repository = "https://prometheus-community.github.io/helm-charts" - chart = "kube-prometheus-stack" - timeout = 600 - - # Grafana - set { - name = "grafana.adminPassword" - value = var.grafana_admin_password - } - set { - name = "grafana.persistence.enabled" - value = "true" - } - set { - name = "grafana.persistence.size" - value = "5Gi" - } - set { - name = "grafana.sidecar.datasources.defaultDatasourceEnabled" - value = "false" - } - set { - name = "grafana.initChownData.enabled" - value = "false" - } - set { - name = "grafana.ingress.enabled" - value = "true" - } - set { - name = "grafana.ingress.ingressClassName" - value = "traefik" - } - set { - name = "grafana.ingress.hosts[0]" - value = "grafana.${var.infra_domain}" - } - set { - name = "grafana.ingress.tls[0].secretName" - value = "grafana-tls" - } - set { - name = "grafana.ingress.tls[0].hosts[0]" - value = "grafana.${var.infra_domain}" - } - set { - name = "grafana.ingress.annotations.traefik\\.ingress\\.kubernetes\\.io/router\\.entrypoints" - value = "websecure" - } - set { - name = "grafana.ingress.annotations.cert-manager\\.io/cluster-issuer" - value = "letsencrypt-prod" - } - - # Prometheus persistent storage - set { - name = "prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage" - value = "20Gi" - } - - # Disable k3s-incompatible scrapers - set { - name = "kubeEtcd.enabled" - value = "false" - } - set { - name = "kubeControllerManager.enabled" - value = "false" - } - set { - name = "kubeScheduler.enabled" - value = "false" - } - set { - name = "kubeProxy.enabled" - value = "false" - } - - # Alertmanager — route alerts to #infrastructure-alerts in Slack - values = [<<-EOT - alertmanager: - config: - global: - resolve_timeout: 5m - inhibit_rules: - - source_matchers: [severity="critical"] - target_matchers: [severity=~"warning|info"] - equal: [namespace, alertname] - - source_matchers: [severity="warning"] - target_matchers: [severity="info"] - equal: [namespace, alertname] - receivers: - - name: "null" - - name: slack-alerts - slack_configs: - - channel: "#infrastructure-alerts" - api_url: "https://slack.com/api/chat.postMessage" - http_config: - authorization: - type: Bearer - credentials: "${var.slack_bot_token}" - send_resolved: true - title: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} ({{ .CommonLabels.namespace }})' - text: | - {{ range .Alerts }} - *{{ if .Annotations.summary }}{{ .Annotations.summary }}{{ else }}{{ .Labels.alertname }}{{ end }}*{{ if .Labels.severity }} · {{ .Labels.severity }}{{ end }} - {{ if .Annotations.description }}{{ .Annotations.description }}{{ end }} - {{ end }} - footer: "Legion · neuralplatform.ai" - route: - group_by: [namespace, alertname] - group_wait: 30s - group_interval: 5m - repeat_interval: 4h - receiver: slack-alerts - routes: - - matchers: [alertname="Watchdog"] - receiver: "null" - - matchers: [severity="info"] - receiver: "null" - EOT - ] - - depends_on = [helm_release.cert_manager] -} - -# Grafana extra datasources — Loki and Tempo wired up with cross-linking -resource "kubernetes_config_map" "grafana_extra_datasources" { - metadata { - name = "grafana-extra-datasources" - namespace = kubernetes_namespace.monitoring.metadata[0].name - labels = { grafana_datasource = "1" } - } - - data = { - "datasources.yaml" = <<-EOT - apiVersion: 1 - datasources: - - name: Loki - type: loki - uid: loki - url: http://loki-gateway.monitoring.svc.cluster.local - access: proxy - jsonData: - httpHeaderName1: X-Scope-OrgID - secureJsonData: - httpHeaderValue1: legion - - name: Tempo - type: tempo - uid: tempo - url: http://tempo.monitoring.svc.cluster.local:3100 - access: proxy - jsonData: - lokiSearch: - datasourceUid: loki - tracesToLogsV2: - datasourceUid: loki - filterByTraceID: true - filterBySpanID: false - tracesToMetrics: - datasourceUid: prometheus - EOT - } - - depends_on = [helm_release.kube_prometheus_stack] -} - -# Loki — log aggregation (single binary mode) -# TODO: Replace useTestSchema with proper schema_config before storing long-term logs -resource "helm_release" "loki" { - name = "loki" - namespace = kubernetes_namespace.monitoring.metadata[0].name - repository = "https://grafana.github.io/helm-charts" - chart = "loki" - timeout = 300 - - set { - name = "loki.commonConfig.replication_factor" - value = "1" - } - set { - name = "loki.storage.type" - value = "filesystem" - } - set { - name = "loki.useTestSchema" - value = "true" - } - set { - name = "singleBinary.replicas" - value = "1" - } - set { - name = "singleBinary.persistence.enabled" - value = "true" - } - set { - name = "singleBinary.persistence.size" - value = "10Gi" - } - set { - name = "deploymentMode" - value = "SingleBinary" - } - set { - name = "backend.replicas" - value = "0" - } - set { - name = "read.replicas" - value = "0" - } - set { - name = "write.replicas" - value = "0" - } - set { - name = "chunksCache.enabled" - value = "false" - } - set { - name = "resultsCache.enabled" - value = "false" - } -} - -# Tempo — distributed tracing backend (local storage) -# TODO: Migrate to tempo-distributed with R2 storage once R2 trace bucket is created -resource "helm_release" "tempo" { - name = "tempo" - namespace = kubernetes_namespace.monitoring.metadata[0].name - repository = "https://grafana.github.io/helm-charts" - chart = "tempo" - timeout = 300 - - set { - name = "tempo.storage.trace.backend" - value = "local" - } - set { - name = "tempo.storage.trace.local.path" - value = "/var/tempo" - } - set { - name = "persistence.enabled" - value = "true" - } - set { - name = "persistence.size" - value = "10Gi" - } -} - -# Alloy — log shipping + OTLP trace forwarding (replaces deprecated Promtail) -resource "helm_release" "alloy" { - name = "alloy" - namespace = kubernetes_namespace.monitoring.metadata[0].name - repository = "https://grafana.github.io/helm-charts" - chart = "alloy" - timeout = 300 - - set { - name = "controller.type" - value = "daemonset" - } - - values = [<<-EOT - alloy: - configMap: - content: | - discovery.kubernetes "pods" { - role = "pod" - } - - discovery.relabel "pods" { - targets = discovery.kubernetes.pods.targets - rule { - source_labels = ["__meta_kubernetes_namespace"] - target_label = "namespace" - } - rule { - source_labels = ["__meta_kubernetes_pod_name"] - target_label = "pod" - } - rule { - source_labels = ["__meta_kubernetes_pod_container_name"] - target_label = "container" - } - rule { - source_labels = ["__meta_kubernetes_pod_label_app"] - target_label = "app" - } - } - - loki.source.kubernetes "pods" { - targets = discovery.relabel.pods.output - forward_to = [loki.write.default.receiver] - } - - loki.write "default" { - endpoint { - url = "http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push" - tenant_id = "legion" - } - } - - otelcol.receiver.otlp "default" { - grpc { endpoint = "0.0.0.0:4317" } - http { endpoint = "0.0.0.0:4318" } - output { - traces = [otelcol.exporter.otlp.tempo.input] - logs = [otelcol.exporter.loki.pantheon.input] - } - } - - otelcol.exporter.otlp "tempo" { - client { - endpoint = "tempo.monitoring.svc.cluster.local:4317" - tls { insecure = true } - } - } - - otelcol.exporter.loki "pantheon" { - forward_to = [loki.write.default.receiver] - } - EOT - ] - - depends_on = [helm_release.loki, helm_release.tempo] -} - -# Expose Alloy's OTLP HTTP port (4318) for external log/trace ingestion from RunPod pods -resource "kubernetes_service" "alloy_otlp" { - metadata { - name = "alloy-otlp" - namespace = kubernetes_namespace.monitoring.metadata[0].name - labels = { app = "alloy-otlp" } - } - spec { - selector = { "app.kubernetes.io/name" = "alloy" } - port { - name = "otlp-http" - port = 4318 - target_port = 4318 - protocol = "TCP" - } - type = "ClusterIP" - } - depends_on = [helm_release.alloy] -} - -# BasicAuth secret for Traefik middleware — pods authenticate with -# Authorization: Basic base64(pantheon:) -resource "kubernetes_secret" "alloy_otlp_auth" { - metadata { - name = "alloy-otlp-auth" - namespace = kubernetes_namespace.monitoring.metadata[0].name - } - data = { - # htpasswd entry — bcrypt hash of the push token, username "pantheon" - users = "pantheon:${bcrypt(var.alloy_push_token)}" - } - depends_on = [kubernetes_namespace.monitoring] -} diff --git a/servers/legion/namespaces.tf b/servers/legion/namespaces.tf index 3e885c5..ff6169b 100644 --- a/servers/legion/namespaces.tf +++ b/servers/legion/namespaces.tf @@ -17,3 +17,13 @@ resource "kubernetes_namespace" "ci" { } } } + +resource "kubernetes_namespace" "external_secrets" { + metadata { + name = "external-secrets" + labels = { + managed-by = "terraform" + tier = "infrastructure" + } + } +} diff --git a/servers/legion/neuron.tf b/servers/legion/neuron.tf index 6369a07..d45ac76 100644 --- a/servers/legion/neuron.tf +++ b/servers/legion/neuron.tf @@ -1,12 +1,6 @@ # Neuron — MCP server + Axon webhook hub -# MCP: https://neuron.nook.family (internal, Traefik) -# Webhooks: https://axon.neuralplatform.ai (external, Cloudflare tunnel) -# -# Image: registry.nook.family/neural-platform/neuron:latest -# Built by GitHub Actions runner on Legion — see neural-platform/neuron ci.yml -# -# TODO: Migrate SQLite → Postgres once neuron db migration is ready -# TODO: Wire Gmail OAuth credentials via k8s Secret (currently unauthenticated) +# Namespace only — all other resources managed by Argo CD + ESO +# See: apps/neuron.yaml, apps/neuron-config.yaml, k8s/neuron/ resource "kubernetes_namespace" "neuron" { metadata { @@ -17,120 +11,3 @@ resource "kubernetes_namespace" "neuron" { } } } - -resource "kubernetes_persistent_volume_claim" "neuron_data" { - metadata { - name = "neuron-data" - namespace = kubernetes_namespace.neuron.metadata[0].name - } - - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "5Gi" - } - } - } -} - -resource "kubernetes_config_map" "neuron_config" { - metadata { - name = "neuron-config" - namespace = kubernetes_namespace.neuron.metadata[0].name - } - - data = { - MCP_PORT = "8001" - MCP_HOST = "0.0.0.0" - WEBHOOK_PORT = "3847" - NEURON_DB_PATH = "/data/neuron.db" - AXON_DB_PATH = "/data/axon.db" - TUNNEL_NAME = "neural-platform" - - "default.yaml" = <<-EOT - runtime: - name: neuron - version: 0.1.0 - storage: - db_path: /data/neuron.db - cache: - max_l1_size: 1000 - gc: - gen0_threshold: 3 - gen1_threshold: 5 - knowledge: - base_path: /data/knowledge - synapse: - path: /data/synapse - EOT - } -} - -resource "kubernetes_secret" "neuron_secrets" { - metadata { - name = "neuron-secrets" - namespace = kubernetes_namespace.neuron.metadata[0].name - } - - data = { - GITHUB_WEBHOOK_SECRET = var.github_webhook_secret - GITEA_WEBHOOK_SECRET = var.gitea_webhook_secret - SLACK_BOT_TOKEN = var.slack_bot_token - SLACK_SIGNING_SECRET = var.slack_signing_secret - } -} - -# Deployment + Service + cloudflared managed by Argo CD — see servers/legion/apps/neuron.yaml - -resource "kubernetes_secret" "cloudflared_secret" { - metadata { - name = "cloudflared-secret" - namespace = kubernetes_namespace.neuron.metadata[0].name - } - data = { - TUNNEL_TOKEN = var.cloudflared_tunnel_token - } -} - -# Internal MCP ingress — accessible within nook.family network -resource "kubernetes_ingress_v1" "neuron" { - metadata { - name = "neuron" - namespace = kubernetes_namespace.neuron.metadata[0].name - annotations = { - "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - "cert-manager.io/cluster-issuer" = "letsencrypt-prod" - } - } - - spec { - ingress_class_name = "traefik" - - tls { - secret_name = "neuron-tls" - hosts = ["neuron.${var.infra_domain}"] - } - - rule { - host = "neuron.${var.infra_domain}" - http { - path { - path = "/" - path_type = "Prefix" - backend { - service { - name = "neuron" - port { - number = 8001 - } - } - } - } - } - } - } - - depends_on = [helm_release.cert_manager] -} - diff --git a/servers/legion/ollama.tf b/servers/legion/ollama.tf index 16bc408..530c1a8 100644 --- a/servers/legion/ollama.tf +++ b/servers/legion/ollama.tf @@ -1,4 +1,7 @@ -# Ollama — local LLM inference with GPU +# Ollama — namespace only +# PVC, Deployment, Service, Ingress managed by Argo CD +# See: apps/ollama.yaml, apps/ollama-config.yaml, k8s/ollama/ + resource "kubernetes_namespace" "ollama" { metadata { name = "ollama" @@ -8,21 +11,3 @@ resource "kubernetes_namespace" "ollama" { } } } - -resource "kubernetes_persistent_volume_claim" "ollama_models" { - metadata { - name = "ollama-models" - namespace = kubernetes_namespace.ollama.metadata[0].name - } - - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "50Gi" - } - } - } -} - -# Deployment + Service managed by Argo CD — see servers/legion/apps/ollama.yaml diff --git a/servers/legion/packages.tf b/servers/legion/packages.tf index 715d4e1..2e1231f 100644 --- a/servers/legion/packages.tf +++ b/servers/legion/packages.tf @@ -1,5 +1,6 @@ -# Package registries — Verdaccio (npm) and devpi (PyPI) -# Both proxy to public registries and cache locally on Legion +# Package registries — namespace only +# PVCs, ConfigMap, Ingresses managed by Argo CD +# See: apps/packages.yaml, apps/packages-config.yaml, k8s/packages/ resource "kubernetes_namespace" "packages" { metadata { @@ -10,69 +11,3 @@ resource "kubernetes_namespace" "packages" { } } } - -resource "kubernetes_persistent_volume_claim" "verdaccio_data" { - metadata { - name = "verdaccio-data" - namespace = kubernetes_namespace.packages.metadata[0].name - } - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { storage = "10Gi" } - } - } -} - -resource "kubernetes_persistent_volume_claim" "devpi_data" { - metadata { - name = "devpi-data" - namespace = kubernetes_namespace.packages.metadata[0].name - } - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { storage = "10Gi" } - } - } -} - -# Verdaccio — npm proxy/cache -resource "kubernetes_config_map" "verdaccio_config" { - metadata { - name = "verdaccio-config" - namespace = kubernetes_namespace.packages.metadata[0].name - } - - data = { - "config.yaml" = <<-EOT - storage: /verdaccio/storage - auth: - htpasswd: - file: /verdaccio/storage/htpasswd - max_users: 1 - uplinks: - npmjs: - url: https://registry.npmjs.org/ - cache: true - packages: - '@harmonic-framework/*': - access: $authenticated - publish: $authenticated - unpublish: $authenticated - '**': - access: $all - publish: $authenticated - proxy: npmjs - listen: 0.0.0.0:4873 - server: - keepAliveTimeout: 60 - log: - type: stdout - format: pretty - level: http - EOT - } -} - -# Deployments + Services managed by Argo CD — see servers/legion/apps/packages.yaml diff --git a/servers/legion/postgres.tf b/servers/legion/postgres.tf index 09b5507..a1a4938 100644 --- a/servers/legion/postgres.tf +++ b/servers/legion/postgres.tf @@ -1,34 +1,3 @@ -# Shared Postgres — Kong, app databases -resource "helm_release" "postgres" { - name = "postgres" - namespace = kubernetes_namespace.platform.metadata[0].name - repository = "oci://registry-1.docker.io/bitnamicharts" - chart = "postgresql" - - timeout = 600 - - set { - name = "auth.postgresPassword" - value = var.postgres_password - } - - set { - name = "primary.persistence.size" - value = "20Gi" - } - - set { - name = "primary.resources.requests.memory" - value = "256Mi" - } - - set { - name = "primary.resources.requests.cpu" - value = "100m" - } - - set { - name = "primary.initdb.scripts.create-kong-db\\.sql" - value = "CREATE DATABASE kong; CREATE USER kong WITH PASSWORD '${var.kong_db_password}'; GRANT ALL PRIVILEGES ON DATABASE kong TO kong; ALTER DATABASE kong OWNER TO kong;" - } -} +# Shared Postgres — namespace only +# Helm release and secrets managed by Argo CD + ESO +# See: apps/postgres.yaml, apps/postgres-config.yaml, k8s/postgres/ diff --git a/servers/legion/redis.tf b/servers/legion/redis.tf index f8308b6..cdf6f1a 100644 --- a/servers/legion/redis.tf +++ b/servers/legion/redis.tf @@ -1,34 +1,3 @@ -# Shared Redis — caching, session store, pub/sub -resource "helm_release" "redis" { - name = "redis" - namespace = kubernetes_namespace.platform.metadata[0].name - repository = "oci://registry-1.docker.io/bitnamicharts" - chart = "redis" - - timeout = 600 - - set { - name = "auth.enabled" - value = "false" - } - - set { - name = "architecture" - value = "standalone" - } - - set { - name = "master.persistence.size" - value = "5Gi" - } - - set { - name = "master.resources.requests.memory" - value = "128Mi" - } - - set { - name = "master.resources.requests.cpu" - value = "50m" - } -} +# Shared Redis — namespace only +# Helm release managed by Argo CD +# See: apps/redis.yaml diff --git a/servers/legion/registry.tf b/servers/legion/registry.tf index c812c50..5a6da69 100644 --- a/servers/legion/registry.tf +++ b/servers/legion/registry.tf @@ -1,4 +1,7 @@ -# Local Docker Registry — push images locally, no DockerHub +# Local Docker Registry — namespace only +# PVC, Deployment, Service, Ingresses managed by Argo CD +# See: apps/registry.yaml, apps/registry-config.yaml, k8s/registry/ + resource "kubernetes_namespace" "registry" { metadata { name = "registry" @@ -8,21 +11,3 @@ resource "kubernetes_namespace" "registry" { } } } - -resource "kubernetes_persistent_volume_claim" "registry_data" { - metadata { - name = "registry-data" - namespace = kubernetes_namespace.registry.metadata[0].name - } - - spec { - access_modes = ["ReadWriteOnce"] - resources { - requests = { - storage = "50Gi" - } - } - } -} - -# Deployments + Services managed by Argo CD — see servers/legion/apps/registry.yaml diff --git a/servers/legion/variables.tf b/servers/legion/variables.tf index 6c5531f..a3ab42d 100644 --- a/servers/legion/variables.tf +++ b/servers/legion/variables.tf @@ -11,7 +11,7 @@ variable "legion_ip" { } variable "domain_suffix" { - description = "Internal-only domain (nook.family) — not exposed via Cloudflare tunnel. Use infra_domain for public services." + description = "Internal-only domain (nook.family)" type = string default = "nook.family" } @@ -34,116 +34,8 @@ variable "cloudflare_email" { default = "andersonwilliam85@gmail.com" } -variable "gitea_domain" { - description = "Domain for Gitea git server" - type = string - default = "git.neuralplatform.ai" -} - -variable "gitea_admin_password" { - description = "Gitea admin user password" - type = string - sensitive = true -} - -variable "gitea_db_password" { - description = "Gitea database password" - type = string - sensitive = true -} - -variable "postgres_password" { - description = "Postgres superuser password" - type = string - sensitive = true -} - -variable "kong_db_password" { - description = "Kong database password" - type = string - sensitive = true -} - -variable "grafana_admin_password" { - description = "Grafana admin password" - type = string - sensitive = true -} - -variable "github_classic_pat" { - description = "GitHub classic PAT with admin:org scope for runner registration" - type = string - sensitive = true -} - -variable "github_org" { - description = "GitHub organization" - type = string - default = "harmonic-framework" -} - -variable "r2_endpoint" { - description = "Cloudflare R2 S3-compatible endpoint" - type = string - default = "https://651161e0a3d321561b4c90b5bcd5f15b.r2.cloudflarestorage.com" -} - -variable "r2_access_key_id" { - description = "Cloudflare R2 access key ID" - type = string - sensitive = true -} - -variable "r2_secret_access_key" { - description = "Cloudflare R2 secret access key" - type = string - sensitive = true -} - -variable "cloudflared_tunnel_token" { - description = "Cloudflare tunnel token for neural-platform tunnel" - type = string - sensitive = true -} - -variable "slack_bot_token" { - description = "Slack bot token for Neuron bot" - type = string - sensitive = true -} - -variable "slack_signing_secret" { - description = "Slack signing secret for webhook verification" - type = string - sensitive = true -} - -variable "github_webhook_secret" { - description = "GitHub webhook secret for Axon event ingestion" - type = string - sensitive = true -} - -variable "gitea_webhook_secret" { - description = "Gitea webhook secret for Axon event ingestion" - type = string - sensitive = true -} - variable "gitea_api_token" { description = "Gitea API token for Argo CD repo access" type = string sensitive = true } - -variable "gitea_runner_token" { - description = "Gitea act-runner registration token" - type = string - sensitive = true -} - -variable "alloy_push_token" { - description = "Bearer token required to push OTLP logs/traces to alloy.neuralplatform.ai" - type = string - sensitive = true -} diff --git a/servers/legion/vault.tf b/servers/legion/vault.tf index 6049c41..9836a59 100644 --- a/servers/legion/vault.tf +++ b/servers/legion/vault.tf @@ -1,6 +1,8 @@ -# Vault — secrets management with GCP KMS auto-unseal -# UI: https://vault.neuralplatform.ai -# Auto-unseal: GCP KMS projects/neuron-785695/locations/global/keyRings/vault/cryptoKeys/vault-unseal +# Vault — namespace only +# Helm release and GCP SA secret managed by Argo CD + ESO +# See: apps/vault.yaml, apps/vault-config.yaml, k8s/vault/ +# Note: vault-gcp-sa.json must be stored in Vault at secret/vault-gcp-sa (sa_json field) +# before the ESO ExternalSecret can sync it. resource "kubernetes_namespace" "vault" { metadata { @@ -11,99 +13,3 @@ resource "kubernetes_namespace" "vault" { } } } - -resource "kubernetes_secret" "vault_gcp_sa" { - metadata { - name = "vault-gcp-sa" - namespace = kubernetes_namespace.vault.metadata[0].name - } - - data = { - "sa.json" = file("${path.module}/vault-gcp-sa.json") - } -} - -resource "helm_release" "vault" { - name = "vault" - namespace = kubernetes_namespace.vault.metadata[0].name - repository = "https://helm.releases.hashicorp.com" - chart = "vault" - version = "0.29.1" - timeout = 300 - - values = [<<-EOT - server: - image: - repository: hashicorp/vault - tag: "1.18.3" - - extraEnvironmentVars: - GOOGLE_APPLICATION_CREDENTIALS: /vault/gcp/sa.json - - volumes: - - name: gcp-sa - secret: - secretName: vault-gcp-sa - - volumeMounts: - - name: gcp-sa - mountPath: /vault/gcp - readOnly: true - - ha: - enabled: false - - dataStorage: - enabled: true - size: 10Gi - - standalone: - enabled: true - config: | - ui = true - - listener "tcp" { - tls_disable = 1 - address = "[::]:8200" - cluster_address = "[::]:8201" - } - - storage "file" { - path = "/vault/data" - } - - seal "gcpckms" { - project = "neuron-785695" - region = "global" - key_ring = "vault" - crypto_key = "vault-unseal" - } - - ingress: - enabled: true - ingressClassName: traefik - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: websecure - cert-manager.io/cluster-issuer: letsencrypt-prod - hosts: - - host: vault.${var.infra_domain} - paths: - - / - tls: - - secretName: vault-tls - hosts: - - vault.${var.infra_domain} - - ui: - enabled: true - - injector: - enabled: false - EOT - ] - - depends_on = [ - kubernetes_secret.vault_gcp_sa, - helm_release.cert_manager, - ] -}