From ed3ae405baa0ec1e831b6cf422da57c33b240e0d Mon Sep 17 00:00:00 2001 From: Maksim Shakavin Date: Fri, 24 May 2024 01:59:08 +0200 Subject: [PATCH] "Initial commit :rocket:" --- .devcontainer/ci/Dockerfile | 2 + .devcontainer/ci/devcontainer.json | 26 + .../ci/features/devcontainer-feature.json | 6 + .devcontainer/ci/features/install.sh | 77 +++ .devcontainer/devcontainer.json | 11 + .devcontainer/postCreateCommand.sh | 19 + .editorconfig | 14 + .envrc | 19 + .gitattributes | 3 + .github/labeler.yaml | 14 + .github/labels.yaml | 20 + .github/release.yaml | 4 + .github/renovate.json5 | 203 +++++++ .github/tests/config-talos.yaml | 44 ++ .github/workflows/devcontainer.yaml | 57 ++ .github/workflows/e2e.yaml | 107 ++++ .github/workflows/flux-diff.yaml | 90 +++ .github/workflows/kubeconform.yaml | 29 + .github/workflows/label-sync.yaml | 23 + .github/workflows/labeler.yaml | 21 + .github/workflows/lychee.yaml | 66 +++ .github/workflows/release.yaml | 43 ++ .gitignore | 27 + .lycheeignore | 2 + .sops.yaml | 12 + .taskfiles/ExternalSecrets/Taskfile.yaml | 35 ++ .taskfiles/Flux/Taskfile.yaml | 72 +++ .taskfiles/Kubernetes/Taskfile.yaml | 35 ++ .taskfiles/Repository/Taskfile.yaml | 43 ++ .taskfiles/Sops/Taskfile.yaml | 36 ++ .taskfiles/Talos/Taskfile.yaml | 84 +++ .taskfiles/VolSync/Taskfile.yaml | 214 +++++++ .taskfiles/VolSync/scripts/wait-for-job.sh | 13 + .taskfiles/VolSync/templates/list.tmpl.yaml | 20 + .taskfiles/VolSync/templates/unlock.tmpl.yaml | 20 + .taskfiles/VolSync/templates/wipe.tmpl.yaml | 26 + .taskfiles/Workstation/Archfile | 17 + .taskfiles/Workstation/Brewfile | 20 + .taskfiles/Workstation/Taskfile.yaml | 71 +++ README.md | 108 ++++ Taskfile.yaml | 80 +++ bootstrap/overrides/readme.partial.yaml.j2 | 5 + bootstrap/scripts/plugin.py | 63 ++ bootstrap/scripts/validation.py | 113 ++++ bootstrap/templates/.sops.yaml.j2 | 12 + .../cert-manager/app/helmrelease.yaml.j2 | 36 ++ .../cert-manager/app/kustomization.yaml.j2 | 5 + .../cert-manager/issuers/.mjfilter.py | 1 + .../cert-manager/issuers/issuers.yaml.j2 | 39 ++ .../issuers/kustomization.yaml.j2 | 7 + .../cert-manager/issuers/secret.sops.yaml.j2 | 7 + .../apps/cert-manager/cert-manager/ks.yaml.j2 | 46 ++ .../apps/cert-manager/kustomization.yaml.j2 | 7 + .../apps/cert-manager/namespace.yaml.j2 | 7 + .../apps/flux-system/kustomization.yaml.j2 | 7 + .../apps/flux-system/namespace.yaml.j2 | 7 + .../webhooks/app/github/ingress.yaml.j2 | 25 + .../webhooks/app/github/kustomization.yaml.j2 | 10 + .../webhooks/app/github/receiver.yaml.j2 | 25 + .../webhooks/app/github/secret.sops.yaml.j2 | 7 + .../webhooks/app/kustomization.yaml.j2 | 6 + .../apps/flux-system/webhooks/ks.yaml.j2 | 21 + .../kube-system/cilium/app/cilium-bgp.yaml.j2 | 37 ++ .../kube-system/cilium/app/cilium-l2.yaml.j2 | 22 + .../cilium/app/helmrelease.yaml.j2 | 26 + .../cilium/app/kustomization.yaml.j2 | 12 + .../apps/kube-system/cilium/ks.yaml.j2 | 21 + .../kubelet-csr-approver/.mjfilter.py | 1 + .../app/helmrelease.yaml.j2 | 30 + .../app/kustomization.yaml.j2 | 6 + .../kubelet-csr-approver/ks.yaml.j2 | 21 + .../apps/kube-system/kustomization.yaml.j2 | 15 + .../metrics-server/app/helmrelease.yaml.j2 | 32 ++ .../metrics-server/app/kustomization.yaml.j2 | 6 + .../kube-system/metrics-server/ks.yaml.j2 | 21 + .../apps/kube-system/namespace.yaml.j2 | 7 + .../reloader/app/helmrelease.yaml.j2 | 29 + .../reloader/app/kustomization.yaml.j2 | 6 + .../apps/kube-system/reloader/ks.yaml.j2 | 21 + .../apps/kube-system/spegel/.mjfilter.py | 1 + .../spegel/app/helmrelease.yaml.j2 | 31 + .../spegel/app/kustomization.yaml.j2 | 6 + .../apps/kube-system/spegel/ks.yaml.j2 | 21 + .../kubernetes/apps/network/.mjfilter.py | 1 + .../cloudflared/app/configs/config.yaml.j2 | 10 + .../cloudflared/app/dnsendpoint.yaml.j2 | 10 + .../cloudflared/app/helmrelease.yaml.j2 | 113 ++++ .../cloudflared/app/kustomization.yaml.j2 | 14 + .../cloudflared/app/secret.sops.yaml.j2 | 13 + .../apps/network/cloudflared/ks.yaml.j2 | 23 + .../echo-server/app/helmrelease.yaml.j2 | 95 +++ .../echo-server/app/kustomization.yaml.j2 | 6 + .../apps/network/echo-server/ks.yaml.j2 | 21 + .../external-dns/app/dnsendpoint-crd.yaml.j2 | 93 +++ .../external-dns/app/helmrelease.yaml.j2 | 45 ++ .../external-dns/app/kustomization.yaml.j2 | 8 + .../external-dns/app/secret.sops.yaml.j2 | 7 + .../apps/network/external-dns/ks.yaml.j2 | 21 + .../certificates/kustomization.yaml.j2 | 9 + .../certificates/production.yaml.j2 | 14 + .../certificates/staging.yaml.j2 | 14 + .../external/helmrelease.yaml.j2 | 91 +++ .../external/kustomization.yaml.j2 | 6 + .../internal/helmrelease.yaml.j2 | 88 +++ .../internal/kustomization.yaml.j2 | 6 + .../apps/network/ingress-nginx/ks.yaml.j2 | 69 +++ .../k8s-gateway/app/helmrelease.yaml.j2 | 32 ++ .../k8s-gateway/app/kustomization.yaml.j2 | 6 + .../apps/network/k8s-gateway/ks.yaml.j2 | 21 + .../apps/network/kustomization.yaml.j2 | 11 + .../kubernetes/apps/network/namespace.yaml.j2 | 7 + .../apps/system-upgrade/k3s/.mjfilter.py | 1 + .../k3s/app/kustomization.yaml.j2 | 6 + .../apps/system-upgrade/k3s/app/plan.yaml.j2 | 50 ++ .../apps/system-upgrade/k3s/ks.yaml.j2 | 27 + .../apps/system-upgrade/kustomization.yaml.j2 | 15 + .../apps/system-upgrade/namespace.yaml.j2 | 7 + .../system-upgrade-controller/.mjfilter.py | 1 + .../app/helmrelease.yaml.j2 | 102 ++++ .../app/kustomization.yaml.j2 | 9 + .../app/rbac.yaml.j2 | 23 + .../system-upgrade-controller/ks.yaml.j2 | 21 + .../apps/system-upgrade/talos/.mjfilter.py | 4 + .../talos/app/kustomization.yaml.j2 | 6 + .../system-upgrade/talos/app/plan.yaml.j2 | 93 +++ .../apps/system-upgrade/talos/ks.yaml.j2 | 29 + .../flux/github-deploy-key.sops.yaml.j2 | 17 + .../bootstrap/flux/kustomization.yaml.j2 | 62 ++ .../bootstrap/talos/helmfile.yaml.j2 | 59 ++ .../bootstrap/talos/talconfig.yaml.j2 | 251 ++++++++ .../templates/kubernetes/flux/apps.yaml.j2 | 57 ++ .../kubernetes/flux/config/cluster.yaml.j2 | 46 ++ .../kubernetes/flux/config/flux.yaml.j2 | 88 +++ .../flux/config/kustomization.yaml.j2 | 7 + .../repositories/git/kustomization.yaml.j2 | 5 + .../flux/repositories/helm/bjw-s.yaml.j2 | 11 + .../flux/repositories/helm/cilium.yaml.j2 | 10 + .../flux/repositories/helm/coredns.yaml.j2 | 10 + .../repositories/helm/external-dns.yaml.j2 | 12 + .../repositories/helm/ingress-nginx.yaml.j2 | 12 + .../flux/repositories/helm/jetstack.yaml.j2 | 10 + .../repositories/helm/k8s-gateway.yaml.j2 | 12 + .../repositories/helm/kustomization.yaml.j2 | 19 + .../repositories/helm/metrics-server.yaml.j2 | 10 + .../repositories/helm/postfinance.yaml.j2 | 12 + .../helm/prometheus-community.yaml.j2 | 11 + .../flux/repositories/helm/spegel.yaml.j2 | 11 + .../flux/repositories/helm/stakater.yaml.j2 | 10 + .../flux/repositories/kustomization.yaml.j2 | 8 + .../repositories/oci/kustomization.yaml.j2 | 5 + .../flux/vars/cluster-secrets.sops.yaml.j2 | 12 + .../flux/vars/cluster-settings.yaml.j2 | 16 + .../flux/vars/kustomization.yaml.j2 | 7 + config.sample.yaml | 207 +++++++ docs/assets/logo.png | Bin 0 -> 18691 bytes docs/assets/rack.jpg | Bin 0 -> 154027 bytes docs/set-up.md | 46 ++ flake.lock | 133 +++++ flake.nix | 32 ++ .../apps/actions-runner-system/alert.yaml | 29 + .../app/externalsecret.yaml | 24 + .../app/helmrelease.yaml | 28 + .../app/kustomization.yaml | 7 + .../gha-runner-scale-set-controller/ks.yaml | 23 + .../gha-runner-scale-set/app/helmrelease.yaml | 49 ++ .../app/kustomization.yaml | 6 + .../gha-runner-scale-set/ks.yaml | 21 + .../actions-runner-system/kustomization.yaml | 9 + .../apps/actions-runner-system/namespace.yaml | 8 + kubernetes/apps/cert-manager/alert.yaml | 29 + .../cert-manager/app/helmrelease.yaml | 36 ++ .../cert-manager/app/kustomization.yaml | 7 + .../cert-manager/app/prometheusrule.yaml | 68 +++ .../cert-manager/issuers/issuers.yaml | 39 ++ .../cert-manager/issuers/kustomization.yaml | 7 + .../cert-manager/issuers/secret.sops.yaml | 26 + .../apps/cert-manager/cert-manager/ks.yaml | 44 ++ .../apps/cert-manager/kustomization.yaml | 8 + kubernetes/apps/cert-manager/namespace.yaml | 7 + kubernetes/apps/database/alert.yaml | 29 + .../cloudnative-pg/app/externalsecret.yaml | 34 ++ .../cloudnative-pg/app/helmrelease.yaml | 36 ++ .../cloudnative-pg/app/kustomization.yaml | 19 + .../cloudnative-pg/app/prometheusrule.yaml | 67 +++ .../cloudnative-pg/cluster/cluster.yaml | 54 ++ .../cloudnative-pg/cluster/gatus.yaml | 21 + .../cloudnative-pg/cluster/kustomization.yaml | 9 + .../cluster/scheduledbackup.yaml | 12 + .../apps/database/cloudnative-pg/ks.yaml | 46 ++ .../database/dragonfly/app/helmrelease.yaml | 102 ++++ .../database/dragonfly/app/kustomization.yaml | 9 + .../apps/database/dragonfly/app/rbac.yaml | 40 ++ .../database/dragonfly/cluster/cluster.yaml | 25 + .../dragonfly/cluster/kustomization.yaml | 7 + .../dragonfly/cluster/podmonitor.yaml | 13 + kubernetes/apps/database/dragonfly/ks.yaml | 44 ++ kubernetes/apps/database/kustomization.yaml | 9 + kubernetes/apps/database/namespace.yaml | 7 + kubernetes/apps/default/alert.yaml | 29 + .../apps/default/homepage/app/configmap.yaml | 81 +++ .../default/homepage/app/externalsecret.yaml | 69 +++ .../default/homepage/app/helmrelease.yaml | 77 +++ .../default/homepage/app/kustomization.yaml | 10 + .../apps/default/homepage/app/rbac.yaml | 63 ++ kubernetes/apps/default/homepage/ks.yaml | 26 + kubernetes/apps/default/kustomization.yaml | 16 + kubernetes/apps/default/namespace.yaml | 7 + .../default/nodered/app/configs/settings.js | 542 ++++++++++++++++++ .../default/nodered/app/externalsecret.yaml | 19 + .../apps/default/nodered/app/helmrelease.yaml | 91 +++ .../default/nodered/app/kustomization.yaml | 15 + kubernetes/apps/default/nodered/ks.yaml | 27 + .../default/notifiarr/app/externalsecret.yaml | 48 ++ .../default/notifiarr/app/helmrelease.yaml | 75 +++ .../default/notifiarr/app/kustomization.yaml | 8 + kubernetes/apps/default/notifiarr/ks.yaml | 26 + .../default/overseerr/app/helmrelease.yaml | 115 ++++ .../default/overseerr/app/kustomization.yaml | 9 + .../apps/default/overseerr/app/pvc.yaml | 12 + kubernetes/apps/default/overseerr/ks.yaml | 25 + .../default/paperless/app/helmrelease.yaml | 87 +++ .../default/paperless/app/kustomization.yaml | 8 + kubernetes/apps/default/paperless/ks.yaml | 27 + .../apps/default/plex/app/helmrelease.yaml | 125 ++++ .../apps/default/plex/app/kustomization.yaml | 9 + kubernetes/apps/default/plex/app/pvc.yaml | 12 + kubernetes/apps/default/plex/ks.yaml | 26 + .../default/prowlarr/app/externalsecret.yaml | 33 ++ .../default/prowlarr/app/helmrelease.yaml | 106 ++++ .../default/prowlarr/app/kustomization.yaml | 9 + kubernetes/apps/default/prowlarr/ks.yaml | 28 + .../default/qbittorrent/app/helmrelease.yaml | 157 +++++ .../qbittorrent/app/kustomization.yaml | 8 + kubernetes/apps/default/qbittorrent/ks.yaml | 25 + .../default/radarr/app/externalsecret.yaml | 33 ++ .../apps/default/radarr/app/helmrelease.yaml | 107 ++++ .../default/radarr/app/kustomization.yaml | 9 + kubernetes/apps/default/radarr/ks.yaml | 28 + .../default/sonarr/app/externalsecret.yaml | 32 ++ .../apps/default/sonarr/app/helmrelease.yaml | 109 ++++ .../default/sonarr/app/kustomization.yaml | 10 + kubernetes/apps/default/sonarr/ks.yaml | 28 + kubernetes/apps/external-secrets/alert.yaml | 29 + .../external-secrets/app/helmrelease.yaml | 37 ++ .../external-secrets/app/kustomization.yaml | 7 + .../app/onepassword-connect.secret.sops.yaml | 28 + .../external-secrets/external-secrets/ks.yaml | 44 ++ .../store/clustersecretstore.yaml | 18 + .../external-secrets/store/helmrelease.yaml | 137 +++++ .../external-secrets/store/kustomization.yaml | 7 + .../apps/external-secrets/kustomization.yaml | 9 + .../apps/external-secrets/namespace.yaml | 7 + .../flux-system/addons/app/kustomization.yaml | 7 + .../addons/app/monitoring/kustomization.yaml | 8 + .../addons/app/monitoring/podmonitor.yaml | 32 ++ .../addons/app/monitoring/prometheusrule.yaml | 32 ++ .../addons/app/webhooks/github/ingress.yaml | 20 + .../app/webhooks/github/kustomization.yaml | 8 + .../addons/app/webhooks/github/receiver.yaml | 25 + .../app/webhooks/github/secret.sops.yaml | 26 + .../addons/app/webhooks/kustomization.yaml | 6 + kubernetes/apps/flux-system/addons/ks.yaml | 21 + kubernetes/apps/flux-system/alert.yaml | 38 ++ .../apps/flux-system/kustomization.yaml | 8 + kubernetes/apps/flux-system/namespace.yaml | 7 + kubernetes/apps/kube-system/alert.yaml | 29 + .../kube-system/cilium/app/helm-values.yaml | 57 ++ .../kube-system/cilium/app/helmrelease.yaml | 81 +++ .../kube-system/cilium/app/kustomization.yaml | 12 + .../cilium/app/kustomizeconfig.yaml | 7 + .../kube-system/cilium/config/cilium-l2.yaml | 24 + .../cilium/config/kustomization.yaml | 6 + kubernetes/apps/kube-system/cilium/ks.yaml | 44 ++ .../kube-system/coredns/app/helm-values.yaml | 50 ++ .../kube-system/coredns/app/helmrelease.yaml | 27 + .../coredns/app/kustomization.yaml | 12 + .../coredns/app/kustomizeconfig.yaml | 7 + kubernetes/apps/kube-system/coredns/ks.yaml | 21 + .../intel-device-plugin/app/helmrelease.yaml | 28 + .../app/kustomization.yaml | 6 + .../intel-device-plugin/gpu/helmrelease.yaml | 30 + .../gpu/kustomization.yaml | 6 + .../kube-system/intel-device-plugin/ks.yaml | 42 ++ .../kubelet-csr-approver/app/helm-values.yaml | 3 + .../kubelet-csr-approver/app/helmrelease.yaml | 31 + .../app/kustomization.yaml | 12 + .../app/kustomizeconfig.yaml | 7 + .../kube-system/kubelet-csr-approver/ks.yaml | 21 + .../apps/kube-system/kustomization.yaml | 15 + .../metrics-server/app/helmrelease.yaml | 32 ++ .../metrics-server/app/kustomization.yaml | 6 + .../apps/kube-system/metrics-server/ks.yaml | 21 + kubernetes/apps/kube-system/namespace.yaml | 7 + .../app/helmrelease.yaml | 32 ++ .../app/kustomization.yaml | 6 + .../node-feature-discovery/ks.yaml | 44 ++ .../rules/intel-gpu-device.yaml | 17 + .../rules/kustomization.yaml | 6 + .../kube-system/reloader/app/helmrelease.yaml | 30 + .../reloader/app/kustomization.yaml | 6 + kubernetes/apps/kube-system/reloader/ks.yaml | 21 + .../kube-system/spegel/app/helm-values.yaml | 7 + .../kube-system/spegel/app/helmrelease.yaml | 30 + .../kube-system/spegel/app/kustomization.yaml | 12 + .../spegel/app/kustomizeconfig.yaml | 7 + kubernetes/apps/kube-system/spegel/ks.yaml | 21 + kubernetes/apps/network/alert.yaml | 29 + .../cloudflared/app/configs/config.yaml | 10 + .../network/cloudflared/app/dnsendpoint.yaml | 11 + .../cloudflared/app/externalsecret.yaml | 26 + .../network/cloudflared/app/helmrelease.yaml | 116 ++++ .../cloudflared/app/kustomization.yaml | 14 + kubernetes/apps/network/cloudflared/ks.yaml | 24 + .../network/echo-server/app/helmrelease.yaml | 92 +++ .../echo-server/app/kustomization.yaml | 6 + kubernetes/apps/network/echo-server/ks.yaml | 21 + .../external-dns/app/externalsecret.yaml | 19 + .../network/external-dns/app/helmrelease.yaml | 50 ++ .../external-dns/app/kustomization.yaml | 7 + kubernetes/apps/network/external-dns/ks.yaml | 23 + .../apps/network/external-services/ks.yaml | 21 + .../services/kustomization.yaml | 10 + .../external-services/services/minio.yaml | 31 + .../external-services/services/pihole.yaml | 34 ++ .../external-services/services/proxmox.yaml | 37 ++ .../external-services/services/sprut.yaml | 31 + .../external-services/services/synology.yaml | 36 ++ .../certificates/kustomization.yaml | 7 + .../certificates/production.yaml | 14 + .../ingress-nginx/certificates/staging.yaml | 14 + .../ingress-nginx/external/helmrelease.yaml | 76 +++ .../ingress-nginx/external/kustomization.yaml | 6 + .../ingress-nginx/internal/helmrelease.yaml | 73 +++ .../ingress-nginx/internal/kustomization.yaml | 6 + kubernetes/apps/network/ingress-nginx/ks.yaml | 69 +++ .../network/k8s-gateway/app/helmrelease.yaml | 34 ++ .../k8s-gateway/app/kustomization.yaml | 6 + kubernetes/apps/network/k8s-gateway/ks.yaml | 21 + kubernetes/apps/network/kustomization.yaml | 13 + kubernetes/apps/network/namespace.yaml | 7 + kubernetes/apps/observability/alert.yaml | 29 + .../gatus/app/externalsecret.yaml | 29 + .../observability/gatus/app/helmrelease.yaml | 143 +++++ .../gatus/app/kustomization.yaml | 14 + .../apps/observability/gatus/app/rbac.yaml | 22 + .../gatus/app/resources/config.yaml | 46 ++ kubernetes/apps/observability/gatus/ks.yaml | 24 + .../grafana/app/externalsecret.yaml | 34 ++ .../grafana/app/helmrelease.yaml | 355 ++++++++++++ .../grafana/app/kustomization.yaml | 7 + kubernetes/apps/observability/grafana/ks.yaml | 24 + .../app/externalsecret.yaml | 48 ++ .../app/helmrelease.yaml | 188 ++++++ .../app/kustomization.yaml | 8 + .../app/prometheusrule.yaml | 25 + .../kube-prometheus-stack/ks.yaml | 27 + .../apps/observability/kustomization.yaml | 15 + .../loki/app/externalsecret.yaml | 23 + .../observability/loki/app/helmrelease.yaml | 148 +++++ .../observability/loki/app/kustomization.yaml | 7 + kubernetes/apps/observability/loki/ks.yaml | 24 + kubernetes/apps/observability/namespace.yaml | 7 + .../portainer/app/helmrelease.yaml | 50 ++ .../portainer/app/kustomization.yaml | 7 + .../apps/observability/portainer/ks.yaml | 25 + .../thanos/app/externalsecret.yaml | 23 + .../observability/thanos/app/helmrelease.yaml | 124 ++++ .../thanos/app/kustomization.yaml | 13 + .../thanos/app/resources/cache.yaml | 5 + kubernetes/apps/observability/thanos/ks.yaml | 24 + .../unpoller/app/externalsecret.yaml | 21 + .../unpoller/app/helmrelease.yaml | 79 +++ .../unpoller/app/kustomization.yaml | 7 + .../apps/observability/unpoller/ks.yaml | 23 + .../vector/app/agent/helmrelease.yaml | 103 ++++ .../vector/app/agent/kustomization.yaml | 13 + .../observability/vector/app/agent/rbac.yaml | 22 + .../vector/app/agent/resources/vector.yaml | 25 + .../vector/app/aggregator/helmrelease.yaml | 78 +++ .../vector/app/aggregator/kustomization.yaml | 12 + .../app/aggregator/resources/vector.yaml | 63 ++ .../vector/app/kustomization.yaml | 7 + kubernetes/apps/observability/vector/ks.yaml | 21 + kubernetes/apps/storage/alert.yaml | 29 + kubernetes/apps/storage/kustomization.yaml | 10 + .../app/helmrelease.yaml | 84 +++ .../app/kustomization.yaml | 6 + .../storage/local-path-provisioner/ks.yaml | 21 + .../storage/longhorn/app/helmrelease.yaml | 50 ++ .../storage/longhorn/app/kustomization.yaml | 7 + .../apps/storage/longhorn/app/snapshot.yaml | 14 + kubernetes/apps/storage/longhorn/ks.yaml | 21 + kubernetes/apps/storage/namespace.yaml | 8 + .../apps/storage/volsync/app/helmrelease.yaml | 40 ++ .../storage/volsync/app/kustomization.yaml | 7 + .../storage/volsync/app/prometheusrule.yaml | 28 + kubernetes/apps/storage/volsync/ks.yaml | 42 ++ .../snapshot-controller/helmrelease.yaml | 43 ++ .../snapshot-controller/kustomization.yaml | 6 + kubernetes/bootstrap/flux/kustomization.yaml | 62 ++ kubernetes/bootstrap/helmfile.yaml | 44 ++ .../bootstrap/talos/clusterconfig/.gitignore | 4 + kubernetes/bootstrap/talos/talconfig.yaml | 202 +++++++ .../bootstrap/talos/talsecret.sops.yaml | 43 ++ kubernetes/flux/apps.yaml | 57 ++ kubernetes/flux/config/cluster.yaml | 42 ++ kubernetes/flux/config/flux.yaml | 88 +++ kubernetes/flux/config/kustomization.yaml | 6 + .../flux/repositories/git/kustomization.yaml | 5 + .../helm/actions-runner-controller.yaml | 11 + .../flux/repositories/helm/backube.yaml | 10 + kubernetes/flux/repositories/helm/bjw-s.yaml | 11 + kubernetes/flux/repositories/helm/cilium.yaml | 10 + .../repositories/helm/cloudnative-pg.yaml | 10 + .../flux/repositories/helm/coredns.yaml | 10 + .../repositories/helm/democratic-csi.yaml | 10 + .../flux/repositories/helm/external-dns.yaml | 10 + .../repositories/helm/external-secrets.yaml | 10 + .../flux/repositories/helm/grafana.yaml | 10 + .../flux/repositories/helm/ingress-nginx.yaml | 10 + kubernetes/flux/repositories/helm/intel.yaml | 10 + .../flux/repositories/helm/jetstack.yaml | 10 + .../flux/repositories/helm/k8s-gateway.yaml | 10 + .../flux/repositories/helm/kustomization.yaml | 29 + .../flux/repositories/helm/longhorn.yaml | 10 + .../repositories/helm/metrics-server.yaml | 10 + .../helm/node-feature-discovery.yaml | 10 + .../flux/repositories/helm/piraeus.yaml | 10 + .../repositories/helm/portainer-charts.yaml | 10 + .../flux/repositories/helm/postfinance.yaml | 10 + .../helm/prometheus-community.yaml | 11 + kubernetes/flux/repositories/helm/spegel.yaml | 11 + .../flux/repositories/helm/stakater.yaml | 10 + .../flux/repositories/helm/stevehipwell.yaml | 11 + .../flux/repositories/kustomization.yaml | 8 + .../flux/repositories/oci/kustomization.yaml | 5 + .../flux/vars/cluster-secrets.sops.yaml | 30 + kubernetes/flux/vars/cluster-settings.yaml | 15 + kubernetes/flux/vars/kustomization.yaml | 7 + kubernetes/talos/clusterconfig/talosconfig | 2 + .../templates/gatus/external/configmap.yaml | 20 + .../gatus/external/kustomization.yaml | 6 + .../templates/gatus/internal/configmap.yaml | 24 + .../gatus/internal/kustomization.yaml | 6 + kubernetes/templates/volsync/claim.yaml | 15 + .../templates/volsync/kustomization.yaml | 8 + kubernetes/templates/volsync/minio.yaml | 51 ++ kubernetes/templates/volsync/secret.sops.yaml | 34 ++ requirements.txt | 6 + scripts/kubeconform.sh | 52 ++ talosconfig | 2 + 451 files changed, 14545 insertions(+) create mode 100644 .devcontainer/ci/Dockerfile create mode 100644 .devcontainer/ci/devcontainer.json create mode 100644 .devcontainer/ci/features/devcontainer-feature.json create mode 100644 .devcontainer/ci/features/install.sh create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/postCreateCommand.sh create mode 100644 .editorconfig create mode 100644 .envrc create mode 100644 .gitattributes create mode 100644 .github/labeler.yaml create mode 100644 .github/labels.yaml create mode 100644 .github/release.yaml create mode 100644 .github/renovate.json5 create mode 100644 .github/tests/config-talos.yaml create mode 100644 .github/workflows/devcontainer.yaml create mode 100644 .github/workflows/e2e.yaml create mode 100644 .github/workflows/flux-diff.yaml create mode 100644 .github/workflows/kubeconform.yaml create mode 100644 .github/workflows/label-sync.yaml create mode 100644 .github/workflows/labeler.yaml create mode 100644 .github/workflows/lychee.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 .lycheeignore create mode 100644 .sops.yaml create mode 100644 .taskfiles/ExternalSecrets/Taskfile.yaml create mode 100644 .taskfiles/Flux/Taskfile.yaml create mode 100644 .taskfiles/Kubernetes/Taskfile.yaml create mode 100644 .taskfiles/Repository/Taskfile.yaml create mode 100644 .taskfiles/Sops/Taskfile.yaml create mode 100644 .taskfiles/Talos/Taskfile.yaml create mode 100644 .taskfiles/VolSync/Taskfile.yaml create mode 100644 .taskfiles/VolSync/scripts/wait-for-job.sh create mode 100644 .taskfiles/VolSync/templates/list.tmpl.yaml create mode 100644 .taskfiles/VolSync/templates/unlock.tmpl.yaml create mode 100644 .taskfiles/VolSync/templates/wipe.tmpl.yaml create mode 100644 .taskfiles/Workstation/Archfile create mode 100644 .taskfiles/Workstation/Brewfile create mode 100644 .taskfiles/Workstation/Taskfile.yaml create mode 100644 README.md create mode 100644 Taskfile.yaml create mode 100644 bootstrap/overrides/readme.partial.yaml.j2 create mode 100644 bootstrap/scripts/plugin.py create mode 100644 bootstrap/scripts/validation.py create mode 100644 bootstrap/templates/.sops.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/.mjfilter.py create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/cert-manager/namespace.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/namespace.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/ingress.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/secret.sops.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-bgp.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-l2.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/cilium/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/.mjfilter.py create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/namespace.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/reloader/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/spegel/.mjfilter.py create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/spegel/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/.mjfilter.py create mode 100644 bootstrap/templates/kubernetes/apps/network/cloudflared/app/configs/config.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/cloudflared/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/cloudflared/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/cloudflared/app/secret.sops.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/echo-server/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/echo-server/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/external-dns/app/dnsendpoint-crd.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/external-dns/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/external-dns/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/external-dns/app/secret.sops.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/production.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/network/namespace.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/k3s/.mjfilter.py create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/plan.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/k3s/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/namespace.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/.mjfilter.py create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/helmrelease.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/rbac.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/talos/.mjfilter.py create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/plan.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/apps/system-upgrade/talos/ks.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/bootstrap/flux/github-deploy-key.sops.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/bootstrap/flux/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/bootstrap/talos/helmfile.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/bootstrap/talos/talconfig.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/apps.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/config/flux.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/config/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/git/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/bjw-s.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/cilium.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/coredns.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/external-dns.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/ingress-nginx.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/jetstack.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/k8s-gateway.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/metrics-server.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/postfinance.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/prometheus-community.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/spegel.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/helm/stakater.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/repositories/oci/kustomization.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/vars/cluster-secrets.sops.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/vars/cluster-settings.yaml.j2 create mode 100644 bootstrap/templates/kubernetes/flux/vars/kustomization.yaml.j2 create mode 100644 config.sample.yaml create mode 100644 docs/assets/logo.png create mode 100644 docs/assets/rack.jpg create mode 100644 docs/set-up.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 kubernetes/apps/actions-runner-system/alert.yaml create mode 100644 kubernetes/apps/actions-runner-system/gha-runner-scale-set-controller/app/externalsecret.yaml create mode 100644 kubernetes/apps/actions-runner-system/gha-runner-scale-set-controller/app/helmrelease.yaml create mode 100644 kubernetes/apps/actions-runner-system/gha-runner-scale-set-controller/app/kustomization.yaml create mode 100644 kubernetes/apps/actions-runner-system/gha-runner-scale-set-controller/ks.yaml create mode 100644 kubernetes/apps/actions-runner-system/gha-runner-scale-set/app/helmrelease.yaml create mode 100644 kubernetes/apps/actions-runner-system/gha-runner-scale-set/app/kustomization.yaml create mode 100644 kubernetes/apps/actions-runner-system/gha-runner-scale-set/ks.yaml create mode 100644 kubernetes/apps/actions-runner-system/kustomization.yaml create mode 100644 kubernetes/apps/actions-runner-system/namespace.yaml create mode 100644 kubernetes/apps/cert-manager/alert.yaml create mode 100644 kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml create mode 100644 kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml create mode 100644 kubernetes/apps/cert-manager/cert-manager/app/prometheusrule.yaml create mode 100644 kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml create mode 100644 kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml create mode 100644 kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml create mode 100644 kubernetes/apps/cert-manager/cert-manager/ks.yaml create mode 100644 kubernetes/apps/cert-manager/kustomization.yaml create mode 100644 kubernetes/apps/cert-manager/namespace.yaml create mode 100644 kubernetes/apps/database/alert.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/app/externalsecret.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/app/helmrelease.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/app/kustomization.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/app/prometheusrule.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/cluster/cluster.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/cluster/gatus.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/cluster/kustomization.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/cluster/scheduledbackup.yaml create mode 100644 kubernetes/apps/database/cloudnative-pg/ks.yaml create mode 100644 kubernetes/apps/database/dragonfly/app/helmrelease.yaml create mode 100644 kubernetes/apps/database/dragonfly/app/kustomization.yaml create mode 100644 kubernetes/apps/database/dragonfly/app/rbac.yaml create mode 100644 kubernetes/apps/database/dragonfly/cluster/cluster.yaml create mode 100644 kubernetes/apps/database/dragonfly/cluster/kustomization.yaml create mode 100644 kubernetes/apps/database/dragonfly/cluster/podmonitor.yaml create mode 100644 kubernetes/apps/database/dragonfly/ks.yaml create mode 100644 kubernetes/apps/database/kustomization.yaml create mode 100644 kubernetes/apps/database/namespace.yaml create mode 100644 kubernetes/apps/default/alert.yaml create mode 100644 kubernetes/apps/default/homepage/app/configmap.yaml create mode 100644 kubernetes/apps/default/homepage/app/externalsecret.yaml create mode 100644 kubernetes/apps/default/homepage/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/homepage/app/kustomization.yaml create mode 100644 kubernetes/apps/default/homepage/app/rbac.yaml create mode 100644 kubernetes/apps/default/homepage/ks.yaml create mode 100644 kubernetes/apps/default/kustomization.yaml create mode 100644 kubernetes/apps/default/namespace.yaml create mode 100644 kubernetes/apps/default/nodered/app/configs/settings.js create mode 100644 kubernetes/apps/default/nodered/app/externalsecret.yaml create mode 100644 kubernetes/apps/default/nodered/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/nodered/app/kustomization.yaml create mode 100644 kubernetes/apps/default/nodered/ks.yaml create mode 100644 kubernetes/apps/default/notifiarr/app/externalsecret.yaml create mode 100644 kubernetes/apps/default/notifiarr/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/notifiarr/app/kustomization.yaml create mode 100644 kubernetes/apps/default/notifiarr/ks.yaml create mode 100644 kubernetes/apps/default/overseerr/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/overseerr/app/kustomization.yaml create mode 100644 kubernetes/apps/default/overseerr/app/pvc.yaml create mode 100644 kubernetes/apps/default/overseerr/ks.yaml create mode 100644 kubernetes/apps/default/paperless/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/paperless/app/kustomization.yaml create mode 100644 kubernetes/apps/default/paperless/ks.yaml create mode 100644 kubernetes/apps/default/plex/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/plex/app/kustomization.yaml create mode 100644 kubernetes/apps/default/plex/app/pvc.yaml create mode 100644 kubernetes/apps/default/plex/ks.yaml create mode 100644 kubernetes/apps/default/prowlarr/app/externalsecret.yaml create mode 100644 kubernetes/apps/default/prowlarr/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/prowlarr/app/kustomization.yaml create mode 100644 kubernetes/apps/default/prowlarr/ks.yaml create mode 100644 kubernetes/apps/default/qbittorrent/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/qbittorrent/app/kustomization.yaml create mode 100644 kubernetes/apps/default/qbittorrent/ks.yaml create mode 100644 kubernetes/apps/default/radarr/app/externalsecret.yaml create mode 100644 kubernetes/apps/default/radarr/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/radarr/app/kustomization.yaml create mode 100644 kubernetes/apps/default/radarr/ks.yaml create mode 100644 kubernetes/apps/default/sonarr/app/externalsecret.yaml create mode 100644 kubernetes/apps/default/sonarr/app/helmrelease.yaml create mode 100644 kubernetes/apps/default/sonarr/app/kustomization.yaml create mode 100644 kubernetes/apps/default/sonarr/ks.yaml create mode 100644 kubernetes/apps/external-secrets/alert.yaml create mode 100644 kubernetes/apps/external-secrets/external-secrets/app/helmrelease.yaml create mode 100644 kubernetes/apps/external-secrets/external-secrets/app/kustomization.yaml create mode 100644 kubernetes/apps/external-secrets/external-secrets/app/onepassword-connect.secret.sops.yaml create mode 100644 kubernetes/apps/external-secrets/external-secrets/ks.yaml create mode 100644 kubernetes/apps/external-secrets/external-secrets/store/clustersecretstore.yaml create mode 100644 kubernetes/apps/external-secrets/external-secrets/store/helmrelease.yaml create mode 100644 kubernetes/apps/external-secrets/external-secrets/store/kustomization.yaml create mode 100644 kubernetes/apps/external-secrets/kustomization.yaml create mode 100644 kubernetes/apps/external-secrets/namespace.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/kustomization.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/monitoring/kustomization.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/monitoring/podmonitor.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/monitoring/prometheusrule.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/webhooks/github/ingress.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/webhooks/github/kustomization.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/webhooks/github/receiver.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/webhooks/github/secret.sops.yaml create mode 100644 kubernetes/apps/flux-system/addons/app/webhooks/kustomization.yaml create mode 100644 kubernetes/apps/flux-system/addons/ks.yaml create mode 100644 kubernetes/apps/flux-system/alert.yaml create mode 100644 kubernetes/apps/flux-system/kustomization.yaml create mode 100644 kubernetes/apps/flux-system/namespace.yaml create mode 100644 kubernetes/apps/kube-system/alert.yaml create mode 100644 kubernetes/apps/kube-system/cilium/app/helm-values.yaml create mode 100644 kubernetes/apps/kube-system/cilium/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/cilium/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/cilium/app/kustomizeconfig.yaml create mode 100644 kubernetes/apps/kube-system/cilium/config/cilium-l2.yaml create mode 100644 kubernetes/apps/kube-system/cilium/config/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/cilium/ks.yaml create mode 100644 kubernetes/apps/kube-system/coredns/app/helm-values.yaml create mode 100644 kubernetes/apps/kube-system/coredns/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/coredns/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/coredns/app/kustomizeconfig.yaml create mode 100644 kubernetes/apps/kube-system/coredns/ks.yaml create mode 100644 kubernetes/apps/kube-system/intel-device-plugin/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/intel-device-plugin/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/intel-device-plugin/gpu/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/intel-device-plugin/gpu/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/intel-device-plugin/ks.yaml create mode 100644 kubernetes/apps/kube-system/kubelet-csr-approver/app/helm-values.yaml create mode 100644 kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomizeconfig.yaml create mode 100644 kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml create mode 100644 kubernetes/apps/kube-system/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/metrics-server/ks.yaml create mode 100644 kubernetes/apps/kube-system/namespace.yaml create mode 100644 kubernetes/apps/kube-system/node-feature-discovery/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/node-feature-discovery/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/node-feature-discovery/ks.yaml create mode 100644 kubernetes/apps/kube-system/node-feature-discovery/rules/intel-gpu-device.yaml create mode 100644 kubernetes/apps/kube-system/node-feature-discovery/rules/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/reloader/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/reloader/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/reloader/ks.yaml create mode 100644 kubernetes/apps/kube-system/spegel/app/helm-values.yaml create mode 100644 kubernetes/apps/kube-system/spegel/app/helmrelease.yaml create mode 100644 kubernetes/apps/kube-system/spegel/app/kustomization.yaml create mode 100644 kubernetes/apps/kube-system/spegel/app/kustomizeconfig.yaml create mode 100644 kubernetes/apps/kube-system/spegel/ks.yaml create mode 100644 kubernetes/apps/network/alert.yaml create mode 100644 kubernetes/apps/network/cloudflared/app/configs/config.yaml create mode 100644 kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml create mode 100644 kubernetes/apps/network/cloudflared/app/externalsecret.yaml create mode 100644 kubernetes/apps/network/cloudflared/app/helmrelease.yaml create mode 100644 kubernetes/apps/network/cloudflared/app/kustomization.yaml create mode 100644 kubernetes/apps/network/cloudflared/ks.yaml create mode 100644 kubernetes/apps/network/echo-server/app/helmrelease.yaml create mode 100644 kubernetes/apps/network/echo-server/app/kustomization.yaml create mode 100644 kubernetes/apps/network/echo-server/ks.yaml create mode 100644 kubernetes/apps/network/external-dns/app/externalsecret.yaml create mode 100644 kubernetes/apps/network/external-dns/app/helmrelease.yaml create mode 100644 kubernetes/apps/network/external-dns/app/kustomization.yaml create mode 100644 kubernetes/apps/network/external-dns/ks.yaml create mode 100644 kubernetes/apps/network/external-services/ks.yaml create mode 100644 kubernetes/apps/network/external-services/services/kustomization.yaml create mode 100644 kubernetes/apps/network/external-services/services/minio.yaml create mode 100644 kubernetes/apps/network/external-services/services/pihole.yaml create mode 100644 kubernetes/apps/network/external-services/services/proxmox.yaml create mode 100644 kubernetes/apps/network/external-services/services/sprut.yaml create mode 100644 kubernetes/apps/network/external-services/services/synology.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/certificates/production.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/certificates/staging.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/external/kustomization.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml create mode 100644 kubernetes/apps/network/ingress-nginx/ks.yaml create mode 100644 kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml create mode 100644 kubernetes/apps/network/k8s-gateway/app/kustomization.yaml create mode 100644 kubernetes/apps/network/k8s-gateway/ks.yaml create mode 100644 kubernetes/apps/network/kustomization.yaml create mode 100644 kubernetes/apps/network/namespace.yaml create mode 100644 kubernetes/apps/observability/alert.yaml create mode 100644 kubernetes/apps/observability/gatus/app/externalsecret.yaml create mode 100644 kubernetes/apps/observability/gatus/app/helmrelease.yaml create mode 100644 kubernetes/apps/observability/gatus/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/gatus/app/rbac.yaml create mode 100644 kubernetes/apps/observability/gatus/app/resources/config.yaml create mode 100644 kubernetes/apps/observability/gatus/ks.yaml create mode 100644 kubernetes/apps/observability/grafana/app/externalsecret.yaml create mode 100644 kubernetes/apps/observability/grafana/app/helmrelease.yaml create mode 100644 kubernetes/apps/observability/grafana/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/grafana/ks.yaml create mode 100644 kubernetes/apps/observability/kube-prometheus-stack/app/externalsecret.yaml create mode 100644 kubernetes/apps/observability/kube-prometheus-stack/app/helmrelease.yaml create mode 100644 kubernetes/apps/observability/kube-prometheus-stack/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/kube-prometheus-stack/app/prometheusrule.yaml create mode 100644 kubernetes/apps/observability/kube-prometheus-stack/ks.yaml create mode 100644 kubernetes/apps/observability/kustomization.yaml create mode 100644 kubernetes/apps/observability/loki/app/externalsecret.yaml create mode 100644 kubernetes/apps/observability/loki/app/helmrelease.yaml create mode 100644 kubernetes/apps/observability/loki/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/loki/ks.yaml create mode 100644 kubernetes/apps/observability/namespace.yaml create mode 100644 kubernetes/apps/observability/portainer/app/helmrelease.yaml create mode 100644 kubernetes/apps/observability/portainer/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/portainer/ks.yaml create mode 100644 kubernetes/apps/observability/thanos/app/externalsecret.yaml create mode 100644 kubernetes/apps/observability/thanos/app/helmrelease.yaml create mode 100644 kubernetes/apps/observability/thanos/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/thanos/app/resources/cache.yaml create mode 100644 kubernetes/apps/observability/thanos/ks.yaml create mode 100644 kubernetes/apps/observability/unpoller/app/externalsecret.yaml create mode 100644 kubernetes/apps/observability/unpoller/app/helmrelease.yaml create mode 100644 kubernetes/apps/observability/unpoller/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/unpoller/ks.yaml create mode 100644 kubernetes/apps/observability/vector/app/agent/helmrelease.yaml create mode 100644 kubernetes/apps/observability/vector/app/agent/kustomization.yaml create mode 100644 kubernetes/apps/observability/vector/app/agent/rbac.yaml create mode 100644 kubernetes/apps/observability/vector/app/agent/resources/vector.yaml create mode 100644 kubernetes/apps/observability/vector/app/aggregator/helmrelease.yaml create mode 100644 kubernetes/apps/observability/vector/app/aggregator/kustomization.yaml create mode 100644 kubernetes/apps/observability/vector/app/aggregator/resources/vector.yaml create mode 100644 kubernetes/apps/observability/vector/app/kustomization.yaml create mode 100644 kubernetes/apps/observability/vector/ks.yaml create mode 100644 kubernetes/apps/storage/alert.yaml create mode 100644 kubernetes/apps/storage/kustomization.yaml create mode 100644 kubernetes/apps/storage/local-path-provisioner/app/helmrelease.yaml create mode 100644 kubernetes/apps/storage/local-path-provisioner/app/kustomization.yaml create mode 100644 kubernetes/apps/storage/local-path-provisioner/ks.yaml create mode 100644 kubernetes/apps/storage/longhorn/app/helmrelease.yaml create mode 100644 kubernetes/apps/storage/longhorn/app/kustomization.yaml create mode 100644 kubernetes/apps/storage/longhorn/app/snapshot.yaml create mode 100644 kubernetes/apps/storage/longhorn/ks.yaml create mode 100644 kubernetes/apps/storage/namespace.yaml create mode 100644 kubernetes/apps/storage/volsync/app/helmrelease.yaml create mode 100644 kubernetes/apps/storage/volsync/app/kustomization.yaml create mode 100644 kubernetes/apps/storage/volsync/app/prometheusrule.yaml create mode 100644 kubernetes/apps/storage/volsync/ks.yaml create mode 100644 kubernetes/apps/storage/volsync/snapshot-controller/helmrelease.yaml create mode 100644 kubernetes/apps/storage/volsync/snapshot-controller/kustomization.yaml create mode 100644 kubernetes/bootstrap/flux/kustomization.yaml create mode 100644 kubernetes/bootstrap/helmfile.yaml create mode 100644 kubernetes/bootstrap/talos/clusterconfig/.gitignore create mode 100644 kubernetes/bootstrap/talos/talconfig.yaml create mode 100644 kubernetes/bootstrap/talos/talsecret.sops.yaml create mode 100644 kubernetes/flux/apps.yaml create mode 100644 kubernetes/flux/config/cluster.yaml create mode 100644 kubernetes/flux/config/flux.yaml create mode 100644 kubernetes/flux/config/kustomization.yaml create mode 100644 kubernetes/flux/repositories/git/kustomization.yaml create mode 100644 kubernetes/flux/repositories/helm/actions-runner-controller.yaml create mode 100644 kubernetes/flux/repositories/helm/backube.yaml create mode 100644 kubernetes/flux/repositories/helm/bjw-s.yaml create mode 100644 kubernetes/flux/repositories/helm/cilium.yaml create mode 100644 kubernetes/flux/repositories/helm/cloudnative-pg.yaml create mode 100644 kubernetes/flux/repositories/helm/coredns.yaml create mode 100644 kubernetes/flux/repositories/helm/democratic-csi.yaml create mode 100644 kubernetes/flux/repositories/helm/external-dns.yaml create mode 100644 kubernetes/flux/repositories/helm/external-secrets.yaml create mode 100644 kubernetes/flux/repositories/helm/grafana.yaml create mode 100644 kubernetes/flux/repositories/helm/ingress-nginx.yaml create mode 100644 kubernetes/flux/repositories/helm/intel.yaml create mode 100644 kubernetes/flux/repositories/helm/jetstack.yaml create mode 100644 kubernetes/flux/repositories/helm/k8s-gateway.yaml create mode 100644 kubernetes/flux/repositories/helm/kustomization.yaml create mode 100644 kubernetes/flux/repositories/helm/longhorn.yaml create mode 100644 kubernetes/flux/repositories/helm/metrics-server.yaml create mode 100644 kubernetes/flux/repositories/helm/node-feature-discovery.yaml create mode 100644 kubernetes/flux/repositories/helm/piraeus.yaml create mode 100644 kubernetes/flux/repositories/helm/portainer-charts.yaml create mode 100644 kubernetes/flux/repositories/helm/postfinance.yaml create mode 100644 kubernetes/flux/repositories/helm/prometheus-community.yaml create mode 100644 kubernetes/flux/repositories/helm/spegel.yaml create mode 100644 kubernetes/flux/repositories/helm/stakater.yaml create mode 100644 kubernetes/flux/repositories/helm/stevehipwell.yaml create mode 100644 kubernetes/flux/repositories/kustomization.yaml create mode 100644 kubernetes/flux/repositories/oci/kustomization.yaml create mode 100644 kubernetes/flux/vars/cluster-secrets.sops.yaml create mode 100644 kubernetes/flux/vars/cluster-settings.yaml create mode 100644 kubernetes/flux/vars/kustomization.yaml create mode 100644 kubernetes/talos/clusterconfig/talosconfig create mode 100644 kubernetes/templates/gatus/external/configmap.yaml create mode 100644 kubernetes/templates/gatus/external/kustomization.yaml create mode 100644 kubernetes/templates/gatus/internal/configmap.yaml create mode 100644 kubernetes/templates/gatus/internal/kustomization.yaml create mode 100644 kubernetes/templates/volsync/claim.yaml create mode 100644 kubernetes/templates/volsync/kustomization.yaml create mode 100644 kubernetes/templates/volsync/minio.yaml create mode 100644 kubernetes/templates/volsync/secret.sops.yaml create mode 100644 requirements.txt create mode 100755 scripts/kubeconform.sh create mode 100644 talosconfig diff --git a/.devcontainer/ci/Dockerfile b/.devcontainer/ci/Dockerfile new file mode 100644 index 00000000..e6e945b4 --- /dev/null +++ b/.devcontainer/ci/Dockerfile @@ -0,0 +1,2 @@ +# Ref: https://github.com/devcontainers/ci/issues/191 +FROM mcr.microsoft.com/devcontainers/base:alpine diff --git a/.devcontainer/ci/devcontainer.json b/.devcontainer/ci/devcontainer.json new file mode 100644 index 00000000..2064da8c --- /dev/null +++ b/.devcontainer/ci/devcontainer.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json", + "name": "Flux Cluster Template (CI)", + "build": { + "dockerfile": "./Dockerfile", + "context": "." + }, + "features": { + "./features": {} + }, + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/usr/bin/fish" + } + }, + "terminal.integrated.defaultProfile.linux": "fish" + }, + "extensions": [ + "redhat.vscode-yaml" + ] + } + } +} diff --git a/.devcontainer/ci/features/devcontainer-feature.json b/.devcontainer/ci/features/devcontainer-feature.json new file mode 100644 index 00000000..5f771e34 --- /dev/null +++ b/.devcontainer/ci/features/devcontainer-feature.json @@ -0,0 +1,6 @@ +{ + "name": "Flux Cluster Template (Tools)", + "id": "cluster-template", + "version": "1.0.0", + "description": "Install Tools" +} diff --git a/.devcontainer/ci/features/install.sh b/.devcontainer/ci/features/install.sh new file mode 100644 index 00000000..bbb27428 --- /dev/null +++ b/.devcontainer/ci/features/install.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -e +set -o noglob + +apk add --no-cache \ + age bash bind-tools ca-certificates curl direnv gettext python3 \ + py3-pip moreutils jq git iputils openssh-client \ + starship fzf fish yq helm + +apk add --no-cache \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \ + kubectl sops + +apk add --no-cache \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing \ + lsd + +for app in \ + "budimanjojo/talhelper!!?as=talhelper&type=script" \ + "cilium/cilium-cli!!?as=cilium&type=script" \ + "cli/cli!!?as=gh&type=script" \ + "cloudflare/cloudflared!!?as=cloudflared&type=script" \ + "derailed/k9s!!?as=k9s&type=script" \ + "fluxcd/flux2!!?as=flux&type=script" \ + "go-task/task!!?as=task&type=script" \ + "helmfile/helmfile!!?as=helmfile&type=script" \ + "kubecolor/kubecolor!!?as=kubecolor&type=script" \ + "kubernetes-sigs/krew!!?as=krew&type=script" \ + "kubernetes-sigs/kustomize!!?as=kustomize&type=script" \ + "stern/stern!!?as=stern&type=script" \ + "siderolabs/talos!!?as=talosctl&type=script" \ + "yannh/kubeconform!!?as=kubeconform&type=script" +do + echo "=== Installing ${app} ===" + curl -fsSL "https://i.jpillora.com/${app}" | bash +done + +# Create the fish configuration directory +mkdir -p /home/vscode/.config/fish/{completions,conf.d} + +# Setup autocompletions for fish +for tool in cilium flux helm helmfile k9s kubectl kustomize talhelper talosctl; do + $tool completion fish > /home/vscode/.config/fish/completions/$tool.fish +done +gh completion --shell fish > /home/vscode/.config/fish/completions/gh.fish +stern --completion fish > /home/vscode/.config/fish/completions/stern.fish +yq shell-completion fish > /home/vscode/.config/fish/completions/yq.fish + +# Add hooks into fish +tee /home/vscode/.config/fish/conf.d/hooks.fish > /dev/null < /dev/null < /dev/null < /dev/null <\\S+) depName=(?\\S+)( repository=(?\\S+))?\\n.+: (&\\S+\\s)?(?\\S+)" + ], + "datasourceTemplate": "{{#if datasource}}{{{datasource}}}{{else}}github-releases{{/if}}" + } + ] +} diff --git a/.github/tests/config-talos.yaml b/.github/tests/config-talos.yaml new file mode 100644 index 00000000..3df4ce6d --- /dev/null +++ b/.github/tests/config-talos.yaml @@ -0,0 +1,44 @@ +--- +skip_tests: true + +boostrap_talos: + schematic_id: "376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba" +bootstrap_node_network: 10.10.10.0/24 +bootstrap_node_default_gateway: 10.10.10.1 +bootstrap_node_inventory: + - name: k8s-controller-0 + address: 10.10.10.100 + controller: true + disk: fake + mac_addr: fake + - name: k8s-worker-0 + address: 10.10.10.101 + controller: false + disk: fake + mac_addr: fake +bootstrap_dns_servers: ["1.1.1.1", "1.0.0.1"] +bootstrap_dntp_servers: ["time.cloudflare.com"] +bootstrap_pod_network: 10.69.0.0/16 +bootstrap_service_network: 10.96.0.0/16 +bootstrap_controller_vip: 10.10.10.254 +bootstrap_tls_sans: ["fake"] +bootstrap_sops_age_pubkey: $BOOTSTRAP_AGE_PUBLIC_KEY +bootstrap_bgp: + enabled: false +bootstrap_github_address: https://github.com/onedr0p/cluster-template +bootstrap_github_branch: main +bootstrap_github_webhook_token: fake +bootstrap_cloudflare: + enabled: true + domain: fake + token: take + acme: + email: fake@example.com + production: false + tunnel: + account_id: fake + id: fake + secret: fake + ingress_vip: 10.10.10.252 + ingress_vip: 10.10.10.251 + gateway_vip: 10.10.10.253 diff --git a/.github/workflows/devcontainer.yaml b/.github/workflows/devcontainer.yaml new file mode 100644 index 00000000..00d37c31 --- /dev/null +++ b/.github/workflows/devcontainer.yaml @@ -0,0 +1,57 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "devcontainer" + +on: + workflow_dispatch: + push: + branches: ["main"] + paths: [".devcontainer/ci/**"] + pull_request: + branches: ["main"] + paths: [".devcontainer/ci/**"] + schedule: + - cron: "0 0 * * 1" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +jobs: + devcontainer: + name: publish + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - if: ${{ github.event_name != 'pull_request' }} + name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: devcontainers/ci@v0.3 + env: + BUILDX_NO_DEFAULT_ATTESTATIONS: true + with: + imageName: ghcr.io/${{ github.repository }}/devcontainer + # cacheFrom: ghcr.io/${{ github.repository }}/devcontainer + imageTag: base,latest + platform: linux/amd64,linux/arm64 + configFile: .devcontainer/ci/devcontainer.json + push: ${{ github.event_name == 'pull_request' && 'never' || 'always' }} diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 00000000..441b1e18 --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,107 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "e2e" + +on: + workflow_dispatch: + pull_request: + branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +jobs: + configure: + name: configure + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + config-files: + - k3s-ipv4 + - k3s-ipv6 + - talos + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Homebrew + id: setup-homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Setup Python + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: "3.11" # minimum supported version + + - name: Cache homebrew packages + if: ${{ github.event_name == 'pull_request' }} + uses: actions/cache@v4 + id: cache-homebrew-packages + with: + key: homebrew-${{ runner.os }}-${{ steps.setup-homebrew.outputs.gems-hash }}-${{ hashFiles('.taskfiles/Workstation/Brewfile') }} + path: /home/linuxbrew/.linuxbrew + + - name: Cache venv + if: ${{ github.event_name == 'pull_request' }} + uses: actions/cache@v4 + with: + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'requirements.yaml') }} + path: .venv + + - name: Setup Workflow Tools + if: ${{ github.event_name == 'pull_request' && steps.cache-homebrew-packages.outputs.cache-hit != 'true' }} + shell: bash + run: brew install go-task + + - name: Run Workstation Brew tasks + if: ${{ github.event_name == 'pull_request' && steps.cache-homebrew-packages.outputs.cache-hit != 'true' }} + shell: bash + run: task workstation:brew + + - name: Run Workstation venv tasks + shell: bash + run: task workstation:venv + + - name: Run Workstation direnv tasks + shell: bash + run: task workstation:direnv + + - name: Run Sops Age key task + shell: bash + run: task sops:age-keygen + + - name: Run init tasks + shell: bash + run: | + task init + cp ./.github/tests/config-${{ matrix.config-files }}.yaml ./config.yaml + export BOOTSTRAP_AGE_PUBLIC_KEY=$(sed -n 's/# public key: //gp' age.key) + envsubst < ./config.yaml | sponge ./config.yaml + + - name: Run configure task + shell: bash + run: task configure --yes + + - name: Run Talos tasks + if: ${{ startsWith(matrix.config-files, 'talos') }} + shell: bash + run: | + task talos:bootstrap-gensecret + task talos:bootstrap-genconfig + + - name: Run Ansible tasks + if: ${{ startsWith(matrix.config-files, 'k3s') }} + shell: bash + run: | + task ansible:deps force=false + task ansible:lint + task ansible:list + + - name: Run repo clean and reset tasks + shell: bash + run: | + task repository:clean + task repository:reset --yes diff --git a/.github/workflows/flux-diff.yaml b/.github/workflows/flux-diff.yaml new file mode 100644 index 00000000..c771e167 --- /dev/null +++ b/.github/workflows/flux-diff.yaml @@ -0,0 +1,90 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "Flux Diff" + +on: + pull_request: + branches: ["main"] + paths: ["kubernetes/**"] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +jobs: + flux-diff: + name: Flux Diff + runs-on: ubuntu-latest + permissions: + pull-requests: write + strategy: + matrix: + paths: ["kubernetes"] + resources: ["helmrelease", "kustomization"] + max-parallel: 4 + fail-fast: false + steps: + - name: Generate Token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: "${{ secrets.BOT_APP_ID }}" + private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}" + + - name: Checkout + uses: actions/checkout@v4 + with: + token: "${{ steps.app-token.outputs.token }}" + path: pull + + - name: Checkout Default Branch + uses: actions/checkout@v4 + with: + token: "${{ steps.app-token.outputs.token }}" + ref: "${{ github.event.repository.default_branch }}" + path: default + + - name: Diff Resources + uses: docker://ghcr.io/allenporter/flux-local:main + with: + args: >- + diff ${{ matrix.resources }} + --unified 6 + --path /github/workspace/pull/${{ matrix.paths }} + --path-orig /github/workspace/default/${{ matrix.paths }} + --strip-attrs "helm.sh/chart,checksum/config,app.kubernetes.io/version,chart" + --limit-bytes 10000 + --all-namespaces + --sources "home-kubernetes" + --output-file diff.patch + + - name: Generate Diff + id: diff + run: | + cat diff.patch + echo "diff<> $GITHUB_OUTPUT + cat diff.patch >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - if: ${{ steps.diff.outputs.diff != '' }} + name: Add comment + uses: mshick/add-pr-comment@v2 + with: + repo-token: "${{ steps.app-token.outputs.token }}" + message-id: "${{ github.event.pull_request.number }}/${{ matrix.paths }}/${{ matrix.resources }}" + message-failure: Diff was not successful + message: | + ```diff + ${{ steps.diff.outputs.diff }} + ``` + + # Summarize matrix https://github.community/t/status-check-for-a-matrix-jobs/127354/7 + flux-diff-success: + if: ${{ always() }} + needs: ["flux-diff"] + name: Flux Diff Successful + runs-on: ubuntu-latest + steps: + - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + name: Check matrix status + run: exit 1 diff --git a/.github/workflows/kubeconform.yaml b/.github/workflows/kubeconform.yaml new file mode 100644 index 00000000..58a63cc1 --- /dev/null +++ b/.github/workflows/kubeconform.yaml @@ -0,0 +1,29 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "Kubeconform" + +on: + pull_request: + branches: ["main"] + paths: ["kubernetes/**"] + +env: + KUBERNETES_DIR: ./kubernetes + +jobs: + kubeconform: + name: Kubeconform + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Setup Workflow Tools + run: brew install fluxcd/tap/flux kubeconform kustomize + + - name: Run kubeconform + shell: bash + run: bash ./scripts/kubeconform.sh ${{ env.KUBERNETES_DIR }} diff --git a/.github/workflows/label-sync.yaml b/.github/workflows/label-sync.yaml new file mode 100644 index 00000000..90804e0a --- /dev/null +++ b/.github/workflows/label-sync.yaml @@ -0,0 +1,23 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "Label Sync" + +on: + workflow_dispatch: + push: + branches: ["main"] + paths: [".github/labels.yaml"] + +jobs: + label-sync: + name: Label Sync + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync Labels + uses: EndBug/label-sync@v2 + with: + config-file: .github/labels.yaml + delete-other-labels: true diff --git a/.github/workflows/labeler.yaml b/.github/workflows/labeler.yaml new file mode 100644 index 00000000..d658c1d9 --- /dev/null +++ b/.github/workflows/labeler.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "Labeler" + +on: + workflow_dispatch: + pull_request_target: + branches: ["main"] + +jobs: + labeler: + name: Labeler + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Labeler + uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yaml diff --git a/.github/workflows/lychee.yaml b/.github/workflows/lychee.yaml new file mode 100644 index 00000000..10a60eaa --- /dev/null +++ b/.github/workflows/lychee.yaml @@ -0,0 +1,66 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "Lychee" + +on: + workflow_dispatch: + push: + branches: ["main"] + paths: [".github/workflows/lychee.yaml"] + schedule: + - cron: "0 0 * * *" + +env: + LYCHEE_OUTPUT: lychee/out.md + WORKFLOW_ISSUE_TITLE: "Link Checker Dashboard 🔗" + +jobs: + lychee: + name: Lychee + runs-on: ubuntu-latest + steps: + - name: Generate Token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: "${{ secrets.BOT_APP_ID }}" + private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}" + + - name: Checkout + uses: actions/checkout@v4 + with: + token: "${{ steps.app-token.outputs.token }}" + + - name: Scan for broken links + uses: lycheeverse/lychee-action@v1 + id: lychee + with: + token: "${{ steps.app-token.outputs.token }}" + args: --verbose --no-progress --exclude-mail './**/*.md' + format: markdown + output: "${{ env.LYCHEE_OUTPUT }}" + debug: true + + - name: Find Link Checker Issue + id: find-issue + shell: bash + env: + GH_TOKEN: "${{ steps.app-token.outputs.token }}" + run: | + issue_number=$( \ + gh issue list \ + --search "in:title ${{ env.WORKFLOW_ISSUE_TITLE }}" \ + --state open \ + --json number \ + | jq --raw-output '.[0].number' \ + ) + echo "issue-number=${issue_number}" >> $GITHUB_OUTPUT + echo "${issue_number}" + + - name: Create or Update Issue + uses: peter-evans/create-issue-from-file@v5 + with: + token: "${{ steps.app-token.outputs.token }}" + title: "${{ env.WORKFLOW_ISSUE_TITLE }}" + issue-number: "${{ steps.find-issue.outputs.issue-number || '' }}" + content-filepath: "${{ env.LYCHEE_OUTPUT }}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..fb943f8f --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,43 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: "Release" + +on: + workflow_dispatch: + schedule: + - cron: "0 0 1 * *" + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Release + shell: bash + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + run: | + # Retrieve previous release tag + previous_tag="$(gh release list --limit 1 | awk '{ print $1 }')" + previous_major="${previous_tag%%\.*}" + previous_minor="${previous_tag#*.}" + previous_minor="${previous_minor%.*}" + previous_patch="${previous_tag##*.}" + # Determine next release tag + next_major_minor="$(date +'%Y').$(date +'%-m')" + if [[ "${previous_major}.${previous_minor}" == "${next_major_minor}" ]]; then + echo "Month release already exists for year, incrementing patch number by 1" + next_patch="$((previous_patch + 1))" + else + echo "Month release does not exist for year, setting patch number to 0" + next_patch="0" + fi + # Create release + release_tag="${next_major_minor}.${next_patch}" + gh release create "${release_tag}" \ + --repo="${GITHUB_REPOSITORY}" \ + --title="${release_tag}" \ + --generate-notes diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6ba7ebdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Trash +.DS_Store +Thumbs.db +# k8s +kubeconfig +.decrypted~*.yaml +.config.env +*.agekey +*.pub +*.key +# Private +.private +.bin +# Ansible +.venv* +# Taskfile +.task +# Brew +Brewfile.lock.json +# intellij +.idea +# wiki +wiki +# Bootstrap +/config.yaml +# Direnv +.direnv diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 00000000..8cbc880a --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,2 @@ +https://dash.cloudflare.com/profile/api-tokens +https://www.mend.io/free-developer-tools/renovate/ diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 00000000..ef98b50f --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,12 @@ +--- +creation_rules: + - # IMPORTANT: This rule MUST be above the others + path_regex: talos/.*\.sops\.ya?ml + key_groups: + - age: + - "age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m" + - path_regex: kubernetes/.*\.sops\.ya?ml + encrypted_regex: "^(data|stringData)$" + key_groups: + - age: + - "age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m" diff --git a/.taskfiles/ExternalSecrets/Taskfile.yaml b/.taskfiles/ExternalSecrets/Taskfile.yaml new file mode 100644 index 00000000..c5207685 --- /dev/null +++ b/.taskfiles/ExternalSecrets/Taskfile.yaml @@ -0,0 +1,35 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +tasks: + + sync: + desc: Sync an ExternalSecret for a cluster + summary: | + Args: + ns: Namespace the externalsecret is in (default: default) + secret: Secret to sync (required) + cmd: kubectl -n {{.ns}} annotate externalsecret {{.secret}} force-sync=$(date +%s) --overwrite + env: + KUBECONFIG: "{{.KUBERNETES_DIR}}/kubeconfig" + requires: + vars: ["secret"] + vars: + ns: '{{.ns | default "default"}}' + preconditions: + - kubectl -n {{.ns}} get externalsecret {{.secret}} + + sync-all: + desc: Sync all ExternalSecrets for a cluster + cmds: + - for: { var: secrets, split: '' } + task: sync + vars: + ns: '{{$a := split "|" .ITEM}}{{$a._0}}' + secret: '{{$a := split "|" .ITEM}}{{$a._1}}' + env: + KUBECONFIG: "{{.KUBERNETES_DIR}}/kubeconfig" + vars: + secrets: + sh: kubectl get externalsecret --all-namespaces --no-headers -A | awk '{print $1 "|" $2}' diff --git a/.taskfiles/Flux/Taskfile.yaml b/.taskfiles/Flux/Taskfile.yaml new file mode 100644 index 00000000..2fe84d3e --- /dev/null +++ b/.taskfiles/Flux/Taskfile.yaml @@ -0,0 +1,72 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +vars: + # renovate: datasource=github-releases depName=prometheus-operator/prometheus-operator + PROMETHEUS_OPERATOR_VERSION: v0.74.0 + CLUSTER_SECRET_SOPS_FILE: "{{.KUBERNETES_DIR}}/flux/vars/cluster-secrets.sops.yaml" + CLUSTER_SETTINGS_FILE: "{{.KUBERNETES_DIR}}/flux/vars/cluster-settings.yaml" + GITHUB_DEPLOY_KEY_FILE: "{{.KUBERNETES_DIR}}/bootstrap/flux/github-deploy-key.sops.yaml" + +tasks: + + bootstrap: + desc: Bootstrap Flux into a Kubernetes cluster + cmds: + # Install essential Prometheus Operator CRDs + - kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --filename https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/{{.PROMETHEUS_OPERATOR_VERSION}}/example/prometheus-operator-crd/monitoring.coreos.com_podmonitors.yaml + - kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --filename https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/{{.PROMETHEUS_OPERATOR_VERSION}}/example/prometheus-operator-crd/monitoring.coreos.com_prometheusrules.yaml + - kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --filename https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/{{.PROMETHEUS_OPERATOR_VERSION}}/example/prometheus-operator-crd/monitoring.coreos.com_scrapeconfigs.yaml + - kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --filename https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/{{.PROMETHEUS_OPERATOR_VERSION}}/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml + # Install Flux + - kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --kustomize {{.KUBERNETES_DIR}}/bootstrap/flux + # Set up secrets + - cat {{.AGE_FILE}} | kubectl -n flux-system create secret generic sops-age --from-file=age.agekey=/dev/stdin + - sops --decrypt {{.CLUSTER_SECRET_SOPS_FILE}} | kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --filename - + - kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --filename {{.CLUSTER_SETTINGS_FILE}} + # Install Flux Kustomization resources + - kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --kustomize {{.KUBERNETES_DIR}}/flux/config + preconditions: + - { msg: "Missing kubeconfig", sh: "test -f {{.KUBECONFIG_FILE}}" } + - { msg: "Missing Sops Age key file", sh: "test -f {{.AGE_FILE}}" } + + apply: + desc: Apply a Flux Kustomization resource for a cluster + summary: | + Args: + path: Path under apps containing the Flux Kustomization resource (ks.yaml) (required) + ns: Namespace the Flux Kustomization exists in (default: flux-system) + cmd: | + flux --kubeconfig {{.KUBECONFIG_FILE}} build ks $(basename {{.path}}) \ + --namespace {{.ns}} \ + --kustomization-file {{.KUBERNETES_DIR}}/apps/{{.path}}/ks.yaml \ + --path {{.KUBERNETES_DIR}}/apps/{{.path}} \ + {{- if contains "not found" .ks }}--dry-run \{{ end }} + | \ + kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side \ + --field-manager=kustomize-controller -f - + requires: + vars: ["path"] + vars: + ns: '{{.ns | default "flux-system"}}' + ks: + sh: flux --kubeconfig {{.KUBECONFIG_FILE}} --namespace {{.ns}} get kustomizations $(basename {{.path}}) 2>&1 + preconditions: + - { msg: "Missing kubeconfig", sh: "test -f {{.KUBECONFIG_FILE}}" } + - { msg: "Missing Flux Kustomization for app {{.path}}", sh: "test -f {{.KUBERNETES_DIR}}/apps/{{.path}}/ks.yaml" } + + reconcile: + desc: Force update Flux to pull in changes from your Git repository + cmd: flux --kubeconfig {{.KUBECONFIG_FILE}} reconcile --namespace flux-system kustomization cluster --with-source + preconditions: + - { msg: "Missing kubeconfig", sh: "test -f {{.KUBECONFIG_FILE}}" } + + github-deploy-key: + cmds: + - kubectl create namespace flux-system --dry-run=client -o yaml | kubectl --kubeconfig {{.KUBECONFIG_FILE}} apply --filename - + - sops --decrypt {{.GITHUB_DEPLOY_KEY_FILE}} | kubectl apply --kubeconfig {{.KUBECONFIG_FILE}} --server-side --filename - + preconditions: + - { msg: "Missing kubeconfig", sh: "test -f {{.KUBECONFIG_FILE}}" } + - { msg: "Missing Sops Age key file", sh: "test -f {{.AGE_FILE}}" } + - { msg: "Missing Github deploy key file", sh: "test -f {{.GITHUB_DEPLOY_KEY_FILE}}" } diff --git a/.taskfiles/Kubernetes/Taskfile.yaml b/.taskfiles/Kubernetes/Taskfile.yaml new file mode 100644 index 00000000..e4f52e0c --- /dev/null +++ b/.taskfiles/Kubernetes/Taskfile.yaml @@ -0,0 +1,35 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +vars: + KUBECONFORM_SCRIPT: "{{.SCRIPTS_DIR}}/kubeconform.sh" + +tasks: + + resources: + desc: Gather common resources in your cluster, useful when asking for support + cmds: + - for: { var: resource } + cmd: kubectl get {{.ITEM}} {{.CLI_ARGS | default "-A"}} + vars: + resource: >- + nodes + gitrepositories + kustomizations + helmrepositories + helmreleases + certificates + certificaterequests + ingresses + pods + + kubeconform: + desc: Validate Kubernetes manifests with kubeconform + cmd: bash {{.KUBECONFORM_SCRIPT}} {{.KUBERNETES_DIR}} + preconditions: + - { msg: "Missing kubeconform script", sh: "test -f {{.KUBECONFORM_SCRIPT}}" } + + .reset: + internal: true + cmd: rm -rf {{.KUBERNETES_DIR}} diff --git a/.taskfiles/Repository/Taskfile.yaml b/.taskfiles/Repository/Taskfile.yaml new file mode 100644 index 00000000..9e6bae36 --- /dev/null +++ b/.taskfiles/Repository/Taskfile.yaml @@ -0,0 +1,43 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +tasks: + + clean: + desc: Clean files and directories no longer needed after cluster bootstrap + cmds: + - mkdir -p {{.PRIVATE_DIR}} + # Clean up CI + - rm -rf {{.ROOT_DIR}}/.github/tests + - rm -rf {{.ROOT_DIR}}/.github/workflows/e2e.yaml + # Clean up devcontainer + - rm -rf {{.ROOT_DIR}}/.devcontainer/ci + - rm -rf {{.ROOT_DIR}}/.github/workflows/devcontainer.yaml + # Move bootstrap directory to gitignored directory + - mv {{.BOOTSTRAP_DIR}} {{.PRIVATE_DIR}}/bootstrap-{{now | date "150405"}} + - mv {{.MAKEJINJA_CONFIG_FILE}} {{.PRIVATE_DIR}}/makejinja-{{now | date "150405"}}.toml + # Update renovate.json5 + - sed -i {{if eq OS "darwin"}}''{{end}} 's/(..\.j2)\?//g' {{.ROOT_DIR}}/.github/renovate.json5 + preconditions: + - msg: Missing bootstrap directory + sh: test -d {{.BOOTSTRAP_DIR}} + - msg: Missing Renovate config file + sh: test -f {{.ROOT_DIR}}/.github/renovate.json5 + + reset: + desc: Reset templated configuration files + prompt: Reset templated configuration files... continue? + cmds: + - task: :kubernetes:.reset + - task: :sops:.reset + - task: :talos:.reset + + force-reset: + desc: Reset repo back to HEAD + prompt: Reset repo back to HEAD... continue? + cmds: + - task: reset + - git reset --hard HEAD + - git clean -f -d + - git pull origin main diff --git a/.taskfiles/Sops/Taskfile.yaml b/.taskfiles/Sops/Taskfile.yaml new file mode 100644 index 00000000..7880a005 --- /dev/null +++ b/.taskfiles/Sops/Taskfile.yaml @@ -0,0 +1,36 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +tasks: + + age-keygen: + desc: Initialize Age Key for Sops + cmd: age-keygen --output {{.AGE_FILE}} + status: ["test -f {{.AGE_FILE}}"] + + encrypt: + desc: Encrypt all Kubernetes SOPS secrets + cmds: + - for: { var: file } + task: .encrypt-file + vars: + file: "{{.ITEM}}" + vars: + file: + sh: find "{{.KUBERNETES_DIR}}" -type f -name "*.sops.*" -exec grep -L "ENC\[AES256_GCM" {} \; + + .encrypt-file: + internal: true + cmd: sops --encrypt --in-place {{.file}} + requires: + vars: ["file"] + preconditions: + - msg: Missing Sops config file + sh: test -f {{.SOPS_CONFIG_FILE}} + - msg: Missing Sops Age key file + sh: test -f {{.AGE_FILE}} + + .reset: + internal: true + cmd: rm -rf {{.SOPS_CONFIG_FILE}} diff --git a/.taskfiles/Talos/Taskfile.yaml b/.taskfiles/Talos/Taskfile.yaml new file mode 100644 index 00000000..d99dbc0a --- /dev/null +++ b/.taskfiles/Talos/Taskfile.yaml @@ -0,0 +1,84 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +vars: + TALHELPER_CLUSTER_DIR: "{{.KUBERNETES_DIR}}/bootstrap/talos/clusterconfig" + TALHELPER_SECRET_FILE: "{{.KUBERNETES_DIR}}/bootstrap/talos/talsecret.sops.yaml" + TALHELPER_CONFIG_FILE: "{{.KUBERNETES_DIR}}/bootstrap/talos/talconfig.yaml" + HELMFILE_FILE: "{{.KUBERNETES_DIR}}/bootstrap/helmfile.yaml" + +env: + TALOSCONFIG: "{{.TALHELPER_CLUSTER_DIR}}/talosconfig" + +tasks: + + bootstrap: + desc: Bootstrap the Talos cluster + cmds: + - | + if [ ! -f "{{.TALHELPER_SECRET_FILE}}" ]; then + talhelper gensecret > {{.TALHELPER_SECRET_FILE}} + sops --encrypt --in-place {{.TALHELPER_SECRET_FILE}} + fi + - talhelper genconfig --config-file {{.TALHELPER_CONFIG_FILE}} --secret-file {{.TALHELPER_SECRET_FILE}} --out-dir {{.TALHELPER_CLUSTER_DIR}} + - talhelper gencommand apply --config-file {{.TALHELPER_CONFIG_FILE}} --out-dir {{.TALHELPER_CLUSTER_DIR}} --extra-flags="--insecure" | bash + - until talhelper gencommand bootstrap --config-file {{.TALHELPER_CONFIG_FILE}} --out-dir {{.TALHELPER_CLUSTER_DIR}} | bash; do sleep 10; done + - task: fetch-kubeconfig + - task: install-helm-apps + - talosctl health --server=false + preconditions: + - msg: Missing talhelper config file + sh: test -f {{.TALHELPER_CONFIG_FILE}} + - msg: Missing Sops config file + sh: test -f {{.SOPS_CONFIG_FILE}} + - msg: Missing Sops Age key file + sh: test -f {{.AGE_FILE}} + + fetch-kubeconfig: + desc: Fetch kubeconfig + cmd: until talhelper gencommand kubeconfig --config-file {{.TALHELPER_CONFIG_FILE}} --out-dir {{.TALHELPER_CLUSTER_DIR}} --extra-flags="{{.ROOT_DIR}} --force" | bash; do sleep 10; done + preconditions: + - msg: Missing talhelper config file + sh: test -f {{.TALHELPER_CONFIG_FILE}} + + install-helm-apps: + desc: Bootstrap core apps needed for Talos + cmds: + - until kubectl --kubeconfig {{.KUBECONFIG_FILE}} wait --for=condition=Ready=False nodes --all --timeout=600s; do sleep 10; done + - helmfile --file {{.HELMFILE_FILE}} apply --skip-diff-on-install --suppress-diff + - until kubectl --kubeconfig {{.KUBECONFIG_FILE}} wait --for=condition=Ready nodes --all --timeout=600s; do sleep 10; done + env: + KUBECONFIG: "{{.KUBERNETES_DIR}}/kubeconfig" + preconditions: + - msg: Missing kubeconfig + sh: test -f {{.KUBECONFIG_FILE}} + - msg: Missing helmfile + sh: test -f {{.HELMFILE_FILE}} + + upgrade-talos: + desc: Upgrade talos on a node + cmd: talosctl --nodes {{.node}} upgrade --image {{.image}} --preserve=true --reboot-mode=default + requires: + vars: ["node", "image"] + preconditions: + - msg: Node not found + sh: talosctl --nodes {{.node}} get machineconfig + + upgrade-k8s: + desc: Upgrade k8s on a node + cmd: talosctl --nodes {{.node}} upgrade-k8s --to {{.to}} + requires: + vars: ["node", "to"] + preconditions: + - msg: Node not found + sh: talosctl --nodes {{.node}} get machineconfig + + nuke: + desc: Resets nodes back to maintenance mode + prompt: This will destroy your cluster and reset the nodes back to maintenance mode... continue? + cmd: talhelper gencommand reset --config-file {{.TALHELPER_CONFIG_FILE}} --out-dir {{.TALHELPER_CLUSTER_DIR}} --extra-flags="--reboot {{- if eq .CLI_FORCE false }} --system-labels-to-wipe STATE --system-labels-to-wipe EPHEMERAL{{ end }} --graceful=false --wait=false" | bash + + .reset: + internal: true + cmd: rm -rf {{.TALHELPER_CLUSTER_DIR}} {{.TALHELPER_SECRET_FILE}} {{.TALHELPER_CONFIG_FILE}} diff --git a/.taskfiles/VolSync/Taskfile.yaml b/.taskfiles/VolSync/Taskfile.yaml new file mode 100644 index 00000000..52031822 --- /dev/null +++ b/.taskfiles/VolSync/Taskfile.yaml @@ -0,0 +1,214 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +# This taskfile is used to manage certain VolSync tasks for a given application, limitations are described below. +# 1. Fluxtomization, HelmRelease, PVC, ReplicationSource all have the same name (e.g. plex) +# 2. ReplicationSource and ReplicationDestination are a Restic repository +# 3. Applications are deployed as either a Kubernetes Deployment or StatefulSet +# 4. Each application only has one PVC that is being replicated + +x-env: &env + app: "{{.app}}" + claim: "{{.claim}}" + controller: "{{.controller}}" + job: "{{.job}}" + ns: "{{.ns}}" + pgid: "{{.pgid}}" + previous: "{{.previous}}" + puid: "{{.puid}}" + +vars: + VOLSYNC_SCRIPTS_DIR: "{{.ROOT_DIR}}/.taskfiles/VolSync/scripts" + VOLSYNC_TEMPLATES_DIR: "{{.ROOT_DIR}}/.taskfiles/VolSync/templates" + +tasks: + + state-*: + desc: Suspend or Resume Volsync + summary: | + Args: + state: resume or suspend (required) + cmds: + - flux {{.state}} kustomization volsync + - flux -n {{.ns}} {{.state}} helmrelease volsync + - kubectl -n {{.ns}} scale deployment volsync --replicas {{if eq "suspend" .state}}0{{else}}1{{end}} + env: *env + vars: + ns: '{{.ns | default "volsync-system"}}' + state: '{{index .MATCH 0}}' + + list: + desc: List snapshots for an application + summary: | + Args: + ns: Namespace the PVC is in (default: default) + app: Application to list snapshots for (required) + cmds: + - envsubst < <(cat {{.VOLSYNC_TEMPLATES_DIR}}/list.tmpl.yaml) | kubectl apply -f - + - bash {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh {{.job}} {{.ns}} + - kubectl -n {{.ns}} wait job/{{.job}} --for condition=complete --timeout=1m + - kubectl -n {{.ns}} logs job/{{.job}} --container main + - kubectl -n {{.ns}} delete job {{.job}} + env: *env + requires: + vars: ["app"] + vars: + ns: '{{.ns | default "default"}}' + job: volsync-list-{{.app}} + preconditions: + - test -f {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh + - test -f {{.VOLSYNC_TEMPLATES_DIR}}/list.tmpl.yaml + silent: true + + unlock: + desc: Unlock a Restic repository for an application + summary: | + Args: + ns: Namespace the PVC is in (default: default) + app: Application to unlock (required) + cmds: + - envsubst < <(cat {{.VOLSYNC_TEMPLATES_DIR}}/unlock.tmpl.yaml) | kubectl apply -f - + - bash {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh {{.job}} {{.ns}} + - kubectl -n {{.ns}} wait job/{{.job}} --for condition=complete --timeout=1m + - kubectl -n {{.ns}} logs job/{{.job}} --container minio + - kubectl -n {{.ns}} delete job {{.job}} + env: *env + requires: + vars: ["app"] + vars: + ns: '{{.ns | default "default"}}' + job: volsync-unlock-{{.app}} + preconditions: + - test -f {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh + - test -f {{.VOLSYNC_TEMPLATES_DIR}}/unlock.tmpl.yaml + silent: true + + # To run backup jobs in parallel for all replicationsources: + # - kubectl get replicationsources --all-namespaces --no-headers | awk '{print $2, $1}' | xargs --max-procs=4 -l bash -c 'task volsync:snapshot app=$0 ns=$1' + snapshot: + desc: Snapshot a PVC for an application + summary: | + Args: + ns: Namespace the PVC is in (default: default) + app: Application to snapshot (required) + cmds: + - kubectl -n {{.ns}} patch replicationsources {{.app}} --type merge -p '{"spec":{"trigger":{"manual":"{{.now}}"}}}' + - bash {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh {{.job}} {{.ns}} + - kubectl -n {{.ns}} wait job/{{.job}} --for condition=complete --timeout=120m + env: *env + requires: + vars: ["app"] + vars: + now: '{{now | date "150405"}}' + ns: '{{.ns | default "default"}}' + job: volsync-src-{{.app}} + controller: + sh: true && {{.VOLSYNC_SCRIPTS_DIR}}/which-controller.sh {{.app}} {{.ns}} + preconditions: + - test -f {{.VOLSYNC_SCRIPTS_DIR}}/which-controller.sh + - test -f {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh + - kubectl -n {{.ns}} get replicationsources {{.app}} + + # To run restore jobs in parallel for all replicationdestinations: + # - kubectl get replicationsources --all-namespaces --no-headers | awk '{print $2, $1}' | xargs --max-procs=4 -l bash -c 'task volsync:restore app=$0 ns=$1' + restore: + desc: Restore a PVC for an application + summary: | + Args: + ns: Namespace the PVC is in (default: default) + app: Application to restore (required) + previous: Previous number of snapshots to restore (default: 2) + cmds: + - { task: .suspend, vars: *env } + - { task: .wipe, vars: *env } + - { task: .restore, vars: *env } + - { task: .resume, vars: *env } + env: *env + requires: + vars: ["app"] + vars: + ns: '{{.ns | default "default"}}' + previous: '{{.previous | default 2}}' + controller: + sh: "{{.VOLSYNC_SCRIPTS_DIR}}/which-controller.sh {{.app}} {{.ns}}" + claim: + sh: kubectl -n {{.ns}} get replicationsources/{{.app}} -o jsonpath="{.spec.sourcePVC}" + puid: + sh: kubectl -n {{.ns}} get replicationsources/{{.app}} -o jsonpath="{.spec.restic.moverSecurityContext.runAsUser}" + pgid: + sh: kubectl -n {{.ns}} get replicationsources/{{.app}} -o jsonpath="{.spec.restic.moverSecurityContext.runAsGroup}" + preconditions: + - test -f {{.VOLSYNC_SCRIPTS_DIR}}/which-controller.sh + - test -f {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh + - test -f {{.VOLSYNC_TEMPLATES_DIR}}/replicationdestination.tmpl.yaml + - test -f {{.VOLSYNC_TEMPLATES_DIR}}/wipe.tmpl.yaml + + cleanup: + desc: Delete volume populator PVCs in all namespaces + summary: | + Args: + cmds: + - for: { var: dest } + cmd: | + {{- $items := (split "/" .ITEM) }} + kubectl delete pvc -n {{ $items._0 }} {{ $items._1 }} + - for: { var: cache } + cmd: | + {{- $items := (split "/" .ITEM) }} + kubectl delete pvc -n {{ $items._0 }} {{ $items._1 }} + - for: { var: snaps } + cmd: | + {{- $items := (split "/" .ITEM) }} + kubectl delete volumesnapshot -n {{ $items._0 }} {{ $items._1 }} + env: *env + vars: + dest: + sh: kubectl get pvc --all-namespaces --no-headers | grep "dst-dest" | awk '{print $1 "/" $2}' + cache: + sh: kubectl get pvc --all-namespaces --no-headers | grep "dst-cache" | awk '{print $1 "/" $2}' + snaps: + sh: kubectl get volumesnapshot --all-namespaces --no-headers | grep "dst-dest" | awk '{print $1 "/" $2}' + + # Suspend the Flux ks and hr + .suspend: + internal: true + cmds: + - flux -n flux-system suspend kustomization {{.app}} + - flux -n {{.ns}} suspend helmrelease {{.app}} + - kubectl -n {{.ns}} scale {{.controller}} --replicas 0 + - kubectl -n {{.ns}} wait pod --for delete --selector="app.kubernetes.io/name={{.app}}" --timeout=2m + env: *env + + # Wipe the PVC of all data + .wipe: + internal: true + cmds: + - envsubst < <(cat {{.VOLSYNC_TEMPLATES_DIR}}/wipe.tmpl.yaml) | kubectl apply -f - + - bash {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh {{.job}} {{.ns}} + - kubectl -n {{.ns}} wait job/{{.job}} --for condition=complete --timeout=120m + - kubectl -n {{.ns}} logs job/{{.job}} --container main + - kubectl -n {{.ns}} delete job {{.job}} + env: *env + vars: + job: volsync-wipe-{{.app}} + + # Create VolSync replicationdestination CR to restore data + .restore: + internal: true + cmds: + - envsubst < <(cat {{.VOLSYNC_TEMPLATES_DIR}}/replicationdestination.tmpl.yaml) | kubectl apply -f - + - bash {{.VOLSYNC_SCRIPTS_DIR}}/wait-for-job.sh {{.job}} {{.ns}} + - kubectl -n {{.ns}} wait job/{{.job}} --for condition=complete --timeout=120m + - kubectl -n {{.ns}} delete replicationdestination {{.job}} + env: *env + vars: + job: volsync-dst-{{.app}} + + # Resume Flux ks and hr + .resume: + internal: true + cmds: + - flux -n {{.ns}} resume helmrelease {{.app}} + - flux -n flux-system resume kustomization {{.app}} + env: *env diff --git a/.taskfiles/VolSync/scripts/wait-for-job.sh b/.taskfiles/VolSync/scripts/wait-for-job.sh new file mode 100644 index 00000000..8cb3fbe1 --- /dev/null +++ b/.taskfiles/VolSync/scripts/wait-for-job.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +JOB=$1 +NAMESPACE="${2:-default}" + +[[ -z "${JOB}" ]] && echo "Job name not specified" && exit 1 +while true; do + STATUS="$(kubectl -n "${NAMESPACE}" get pod -l job-name="${JOB}" -o jsonpath='{.items[*].status.phase}')" + if [ "${STATUS}" == "Pending" ]; then + break + fi + sleep 1 +done diff --git a/.taskfiles/VolSync/templates/list.tmpl.yaml b/.taskfiles/VolSync/templates/list.tmpl.yaml new file mode 100644 index 00000000..f538ab63 --- /dev/null +++ b/.taskfiles/VolSync/templates/list.tmpl.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: ${job} + namespace: ${ns} +spec: + ttlSecondsAfterFinished: 3600 + template: + spec: + automountServiceAccountToken: false + restartPolicy: OnFailure + containers: + - name: main + image: docker.io/restic/restic:0.16.4 + args: ["snapshots"] + envFrom: + - secretRef: + name: ${app}-volsync-secret + resources: {} diff --git a/.taskfiles/VolSync/templates/unlock.tmpl.yaml b/.taskfiles/VolSync/templates/unlock.tmpl.yaml new file mode 100644 index 00000000..fceac382 --- /dev/null +++ b/.taskfiles/VolSync/templates/unlock.tmpl.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: ${job} + namespace: ${ns} +spec: + ttlSecondsAfterFinished: 3600 + template: + spec: + automountServiceAccountToken: false + restartPolicy: OnFailure + containers: + - name: minio + image: docker.io/restic/restic:0.16.4 + args: ["unlock", "--remove-all"] + envFrom: + - secretRef: + name: ${app}-volsync-secret + resources: {} diff --git a/.taskfiles/VolSync/templates/wipe.tmpl.yaml b/.taskfiles/VolSync/templates/wipe.tmpl.yaml new file mode 100644 index 00000000..9d6852e3 --- /dev/null +++ b/.taskfiles/VolSync/templates/wipe.tmpl.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: ${job} + namespace: ${ns} +spec: + ttlSecondsAfterFinished: 3600 + template: + spec: + automountServiceAccountToken: false + restartPolicy: OnFailure + containers: + - name: main + image: ghcr.io/onedr0p/alpine:rolling + command: ["/bin/sh", "-c", "cd /config; find . -delete"] + volumeMounts: + - name: config + mountPath: /config + securityContext: + privileged: true + resources: {} + volumes: + - name: config + persistentVolumeClaim: + claimName: ${claim} diff --git a/.taskfiles/Workstation/Archfile b/.taskfiles/Workstation/Archfile new file mode 100644 index 00000000..b1ad3160 --- /dev/null +++ b/.taskfiles/Workstation/Archfile @@ -0,0 +1,17 @@ +age +cloudflared-bin +direnv +flux-bin +go-task +go-yq +helm +helmfile +jq +kubeconform +kubectl-bin +kustomize +moreutils +sops +stern-bin +talhelper-bin +talosctl diff --git a/.taskfiles/Workstation/Brewfile b/.taskfiles/Workstation/Brewfile new file mode 100644 index 00000000..0d31dc67 --- /dev/null +++ b/.taskfiles/Workstation/Brewfile @@ -0,0 +1,20 @@ +tap "fluxcd/tap" +tap "go-task/tap" +tap "siderolabs/talos" +brew "age" +brew "cloudflared" +brew "direnv" +brew "fluxcd/tap/flux" +brew "go-task/tap/go-task" +brew "helm" +brew "helmfile" +brew "jq" +brew "kubeconform" +brew "kubernetes-cli" +brew "kustomize" +brew "moreutils" +brew "sops" +brew "stern" +brew "talhelper" +brew "talosctl" +brew "yq" diff --git a/.taskfiles/Workstation/Taskfile.yaml b/.taskfiles/Workstation/Taskfile.yaml new file mode 100644 index 00000000..09f309f6 --- /dev/null +++ b/.taskfiles/Workstation/Taskfile.yaml @@ -0,0 +1,71 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +vars: + ARCHFILE: "{{.ROOT_DIR}}/.taskfiles/Workstation/Archfile" + BREWFILE: "{{.ROOT_DIR}}/.taskfiles/Workstation/Brewfile" + GENERIC_BIN_DIR: "{{.ROOT_DIR}}/.bin" + +tasks: + + direnv: + desc: Run direnv hooks + cmd: direnv allow . + status: + - "[[ $(direnv status --json | jq '.state.foundRC.allowed') == 0 ]]" + - "[[ $(direnv status --json | jq '.state.loadedRC.allowed') == 0 ]]" + + venv: + desc: Set up virtual environment + cmds: + - "{{.PYTHON_BIN}} -m venv {{.VIRTUAL_ENV}}" + - '{{.VIRTUAL_ENV}}/bin/python3 -m pip install --upgrade pip setuptools wheel' + - '{{.VIRTUAL_ENV}}/bin/python3 -m pip install --upgrade --requirement "{{.PIP_REQUIREMENTS_FILE}}"' + sources: + - "{{.PIP_REQUIREMENTS_FILE}}" + generates: + - "{{.VIRTUAL_ENV}}/pyvenv.cfg" + preconditions: + - { msg: "Missing Pip requirements file", sh: "test -f {{.PIP_REQUIREMENTS_FILE}}" } + + brew: + desc: Install workstation dependencies with Brew + cmd: brew bundle --file {{.BREWFILE}} + preconditions: + - { msg: "Missing Homebrew", sh: "command -v brew" } + - { msg: "Missing Brewfile", sh: "test -f {{.BREWFILE}}" } + + arch: + desc: Install Arch workstation dependencies with Paru Or Yay + cmd: "{{.helper}} -Syu --needed --noconfirm --noprogressbar $(cat {{.ARCHFILE}} | xargs)" + vars: + helper: + sh: "command -v yay || command -v paru" + preconditions: + - { msg: "Missing Archfile", sh: "test -f {{.ARCHFILE}}" } + + generic-linux: + desc: Install CLI tools into the projects .bin directory using curl + dir: "{{.GENERIC_BIN_DIR}}" + platforms: ["linux/amd64", "linux/arm64"] + cmds: + - for: + - budimanjojo/talhelper?as=talhelper&type=script + - cloudflare/cloudflared?as=cloudflared&type=script + - FiloSottile/age?as=age&type=script + - fluxcd/flux2?as=flux&type=script + - getsops/sops?as=sops&type=script + - helmfile/helmfile?as=helmfile&type=script + - jqlang/jq?as=jq&type=script + - kubernetes-sigs/kustomize?as=kustomize&type=script + - siderolabs/talos?as=talosctl&type=script + - yannh/kubeconform?as=kubeconform&type=script + - mikefarah/yq?as=yq&type=script + cmd: curl -fsSL "https://i.jpillora.com/{{.ITEM}}" | bash + - cmd: curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + platforms: ["linux/amd64"] + - cmd: curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl" + platforms: ["linux/arm64"] + - cmd: chmod +x kubectl + - cmd: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | USE_SUDO="false" HELM_INSTALL_DIR="." bash diff --git a/README.md b/README.md new file mode 100644 index 00000000..14766af4 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +
+ + + +# My Homelab automation Repository + +_... managed with FluxCD_ 🤖 + +
+ +--- + +## 🍼 Overview + +👋 Welcome to my Kubernetes Homelab Cluster repository! This project serves as a practical learning environment for +exploring Kubernetes and Infrastructure as Code (IaC) practices using tools like [FluxCD](https://fluxcd.io), +[Renovate](https://github.com/renovatebot/renovate), [go-task](https://github.com/go-task/task) and other + +## 📖 Table of contents + +- [🍼 Overview](#-overview) + - [📖 Table of contents](#-table-of-contents) + - [📚 Documentation](#-documentation) + - [🖥️ Technological Stack](#-technological-stack) + - [🔧 Hardware](#-hardware) + - [☁️ External Dependencies](#-external-dependencies) + - [🤖 Automation](#-automation) + - [🤝 Thanks](#-thanks) + +## 📚 Documentation + +## 🖥️ Technological Stack + +| | Name | Description | +|--------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| +| | [Proxmox](https://www.proxmox.com) | Virtualization platform | +| | [Kubernetes](https://kubernetes.io/) | An open-source system for automating deployment, scaling, and management of containerized applications | +| | [Helm](https://helm.sh) | The Kubernetes package manager | +| | [FluxCD](https://fluxcd.io/) | GitOps tool for deploying applications to Kubernetes | +| | [Talos Linux](https://www.talos.dev/) | Talos Linux is Linux designed for Kubernetes | +| | [Cert Manager](https://cert-manager.io/) | X.509 certificate management for Kubernetes | +| | [Cilium](https://cilium.io/) | Internal Kubernetes container networking interface. | +| | [Ingress-nginx](https://github.com/kubernetes/ingress-nginx) | Kubernetes ingress controller using NGINX as a reverse proxy and load balancer. | +| | [Cloudflared](https://github.com/cloudflare/cloudflared) | Enables Cloudflare secure access to certain ingresses. | +| | [CoreDNS](https://coredns.io/) | Cluster DNS server | +| | [Spegel](https://github.com/spegel-org/spegel) | Stateless cluster local OCI registry mirror. | +| | [External-dns](https://github.com/kubernetes-sigs/external-dns/tree/master) | Automatically syncs ingress DNS records to a DNS provider. | +| | [External Secrets](https://github.com/external-secrets/external-secrets) | Managed Kubernetes secrets using [1Password Connect](https://github.com/1Password/connect). | +| | [Sops](https://github.com/getsops/sops) | Managed secrets for Kubernetes and which are commited to Git. | +| | [Longhorn](https://longhorn.io) | Cloud native distributed block storage for Kubernetes | +| | [VolSync](https://github.com/backube/volsync) | Backup and recovery of persistent volume claims. | +| | [Prometheus](https://prometheus.io) | Monitoring system and time series database | +| | [Thanos](https://thanos.io) | Highly available Prometheus setup with long-term storage capabilities | +| | [Grafana](https://grafana.com) | Data and logs visualization | +| | [Loki](https://grafana.com/oss/loki/) | Horizontally-scalable, highly-available, multi-tenant log aggregation system | +| | [Vector](https://github.com/vectordotdev/vector) | Collects, transform and routes logs to Loki | + + +## 🔧 Hardware + +
+ Rack photo + + rack +
+ +| Device | Count | Disk Size | RAM | OS | Purpose | +|----------------------------|-------|-----------|------|---------|-------------------------| +| Lenovo M910Q Tiny i5-6500T | 3 | 256G | 32GB | Talos | Kubernetes Master Nodes | +| Raspberry Pi 5 | 1 | | 8GB | RpiOS | DNS, SmartHome | +| Synology RS422+ | 1 | 4x16TB | 2GB | DSM | NAS | +| UPS 5UTRA91227 | 1 | | | | UPS | +| UniFi UDM Pro | 1 | | | UnifiOS | Router | +| UniFi USW PRO 24 Gen2 | 1 | | | | Switch | +| UniFi USW Lite 8 | 1 | | | | Switch | +| UniFi U6 In-Wall | 1 | | | | Access Point | +| UniFi U6 Mesh | 1 | | | | Access Point | + +## ☁️ External Dependencies + +This list does not include cloud services that I use for personal reasons and don't yet want to migrate to self-hosted, +such as Google (Gmail, Photos, Drive), streaming services, Apple, and some applications. Legacy cloud services listed +at the bottom are remnants from previous attempts to set up smart home observability dashboards and will be migrated +and shut down ~~never~~ as soon as I have time to transfer all the configurations. + +| Service | Description | Costs | +|-------------------------------------------|------------------------------------------------------------------------------|------------------| +| [1Password](https://1password.com) | Secrets managements | 76$/year | +| [Cloudflare](https://www.cloudflare.com/) | Domain and DNS | Free | +| [GitHub](https://github.com/) | Repository Hosting | Free | +| [Discord](https://discord.com) | Notifications | Free | +| [Let's Encrypt](https://discord.com) | Certificates | Free | +| [Notifiarr](https://notifiarr.com) | Notifications push | 5$ one time | +| [AWS Route 53](https://aws.amazon.com/) | Domain | 0,5$/month | +| [AWS EC2 ](https://aws.amazon.com/) | (Legacy) Grafana, InfluxDB hosting for smart home analytics. Need to migrate | ~15$/month | +| [InfluxDB Cloud](https://aws.amazon.com/) | (Legacy) Smart home data storage. Need to migrate | ~14$/month | +| [AWS Other ](https://aws.amazon.com/) | (Legacy) Email hosting. Need to migrate | ~10$/month | +| | | Total: 45$/month | + + + +## 🤝 Thanks + +This project was mostly ~~copypasted from~~ inspired by a [onedr0p/home-ops](https://github.com/onedr0p/home-ops) +and [onedr0p/cluster-template](https://github.com/onedr0p/cluster-template) repositories. +A big thanks to the members of the [Home Operations](https://discord.gg/home-operations) community +for their support and for sharing their repositories. +Additional thanks to the [Kubesearch](https://kubesearch.dev/) project for ability to search for different configurations. diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 00000000..3f7dd6f5 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,80 @@ +--- +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3" + +vars: + # Directories + ANSIBLE_DIR: "{{.ROOT_DIR}}/ansible" + BOOTSTRAP_DIR: "{{.ROOT_DIR}}/bootstrap" + KUBERNETES_DIR: "{{.ROOT_DIR}}/kubernetes" + PRIVATE_DIR: "{{.ROOT_DIR}}/.private" + SCRIPTS_DIR: "{{.ROOT_DIR}}/scripts" + # Files + AGE_FILE: "{{.ROOT_DIR}}/age.key" + BOOTSTRAP_CONFIG_FILE: "{{.ROOT_DIR}}/config.yaml" + KUBECONFIG_FILE: "{{.ROOT_DIR}}/kubeconfig" + MAKEJINJA_CONFIG_FILE: "{{.ROOT_DIR}}/makejinja.toml" + PIP_REQUIREMENTS_FILE: "{{.ROOT_DIR}}/requirements.txt" + # Binaries + PYTHON_BIN: python3 + +env: + KUBECONFIG: "{{.KUBECONFIG_FILE}}" + PYTHONDONTWRITEBYTECODE: "1" + SOPS_AGE_KEY_FILE: "{{.AGE_FILE}}" + VIRTUAL_ENV: "{{.ROOT_DIR}}/.venv" + +includes: + kubernetes: + aliases: ["k8s"] + taskfile: .taskfiles/Kubernetes/Taskfile.yaml + flux: .taskfiles/Flux/Taskfile.yaml + repository: + aliases: ["repo"] + taskfile: .taskfiles/Repository/Taskfile.yaml + talos: .taskfiles/Talos/Taskfile.yaml + sops: .taskfiles/Sops/Taskfile.yaml + workstation: .taskfiles/Workstation/Taskfile.yaml + volsync: .taskfiles/VolSync/Taskfile.yaml + secrets: .taskfiles/ExternalSecrets/Taskfile.yaml + +tasks: + + default: task -l + + init: + desc: Initialize configuration files + cmds: + - mkdir -p {{.PRIVATE_DIR}} + - cp -n {{.BOOTSTRAP_CONFIG_FILE | replace ".yaml" ".sample.yaml"}} {{.BOOTSTRAP_CONFIG_FILE}} + - cmd: echo === Configuration file copied === + silent: true + - cmd: echo Proceed with updating the configuration files... + silent: true + - cmd: echo {{.BOOTSTRAP_CONFIG_FILE}} + silent: true + status: + - test -f "{{.BOOTSTRAP_CONFIG_FILE}}" + + configure: + desc: Configure repository from bootstrap vars + prompt: Any conflicting config in the root kubernetes and ansible directories will be overwritten... continue? + deps: ["workstation:direnv", "workstation:venv", "sops:age-keygen", "init"] + cmds: + - task: .template + - task: sops:encrypt + - task: .validate + + .template: + internal: true + cmd: "{{.VIRTUAL_ENV}}/bin/makejinja" + preconditions: + - { msg: "Missing virtual environment", sh: "test -d {{.VIRTUAL_ENV}}" } + - { msg: "Missing Makejinja config file", sh: "test -f {{.MAKEJINJA_CONFIG_FILE}}" } + - { msg: "Missing Makejinja plugin file", sh: "test -f {{.BOOTSTRAP_DIR}}/scripts/plugin.py" } + - { msg: "Missing bootstrap config file", sh: "test -f {{.BOOTSTRAP_CONFIG_FILE}}" } + + .validate: + internal: true + cmds: + - task: kubernetes:kubeconform diff --git a/bootstrap/overrides/readme.partial.yaml.j2 b/bootstrap/overrides/readme.partial.yaml.j2 new file mode 100644 index 00000000..36dac44d --- /dev/null +++ b/bootstrap/overrides/readme.partial.yaml.j2 @@ -0,0 +1,5 @@ +<% Place user jinja template overrides in this file's directory %> +<% Docs: https://mirkolenz.github.io/makejinja/makejinja.html %> +<% Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/makejinja.toml %> +<% Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input1/not-empty.yaml.jinja %> +<% Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input2/not-empty.yaml.jinja %> diff --git a/bootstrap/scripts/plugin.py b/bootstrap/scripts/plugin.py new file mode 100644 index 00000000..8944f38d --- /dev/null +++ b/bootstrap/scripts/plugin.py @@ -0,0 +1,63 @@ +import importlib.util +import sys +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from typing import Any +from netaddr import IPNetwork + +import makejinja +import validation + + +def nthhost(value: str, query: int) -> str: + value = IPNetwork(value) + try: + nth = int(query) + if value.size > nth: + return str(value[nth]) + except ValueError: + return False + return value + + +def import_filter(file: Path) -> Callable[[dict[str, Any]], bool]: + module_path = file.relative_to(Path.cwd()).with_suffix("") + module_name = str(module_path).replace("/", ".") + spec = importlib.util.spec_from_file_location(module_name, file) + assert spec is not None + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + assert spec.loader is not None + spec.loader.exec_module(module) + return module.main + + +class Plugin(makejinja.plugin.Plugin): + def __init__(self, data: dict[str, Any], config: makejinja.config.Config): + self._data = data + self._config = config + + self._excluded_dirs: set[Path] = set() + for input_path in config.inputs: + for filter_file in input_path.rglob(".mjfilter.py"): + filter_func = import_filter(filter_file) + if filter_func(data) is False: + self._excluded_dirs.add(filter_file.parent) + + validation.validate(data) + + + def filters(self) -> makejinja.plugin.Filters: + return [nthhost] + + + def path_filters(self): + return [self._mjfilter_func] + + + def _mjfilter_func(self, path: Path) -> bool: + return not any( + path.is_relative_to(excluded_dir) for excluded_dir in self._excluded_dirs + ) diff --git a/bootstrap/scripts/validation.py b/bootstrap/scripts/validation.py new file mode 100644 index 00000000..b3a75a07 --- /dev/null +++ b/bootstrap/scripts/validation.py @@ -0,0 +1,113 @@ +from functools import wraps +from shutil import which +from typing import Callable, cast +from zoneinfo import available_timezones +import netaddr +import re +import socket +import sys + +GLOBAL_CLI_TOOLS = ["age", "flux", "helmfile", "sops", "jq", "kubeconform", "kustomize", "talosctl", "talhelper"] +CLOUDFLARE_TOOLS = ["cloudflared"] + + +def required(*keys: str): + def wrapper_outter(func: Callable): + @wraps(func) + def wrapper(data: dict, *_, **kwargs) -> None: + for key in keys: + if data.get(key) is None: + raise ValueError(f"Missing required key {key}") + return func(*[data[key] for key in keys], **kwargs) + + return wrapper + + return wrapper_outter + + +def validate_python_version() -> None: + required_version = (3, 11, 0) + if sys.version_info < required_version: + raise ValueError(f"Python {sys.version_info} is below 3.11. Please upgrade.") + + +def validate_ip(ip: str) -> str: + try: + netaddr.IPAddress(ip) + except netaddr.core.AddrFormatError as e: + raise ValueError(f"Invalid IP address {ip}") from e + return ip + + +def validate_network(cidr: str, family: int) -> str: + try: + network = netaddr.IPNetwork(cidr) + if network.version != family: + raise ValueError(f"Invalid CIDR family {network.version}") + except netaddr.core.AddrFormatError as e: + raise ValueError(f"Invalid CIDR {cidr}") from e + return cidr + + +def validate_node(node: dict, node_cidr: str) -> None: + if not node.get("name"): + raise ValueError(f"A node is missing a name") + if not re.match(r"^[a-z0-9-]+$", node.get('name')): + raise ValueError(f"Node {node.get('name')} has an invalid name") + if not node.get("disk"): + raise ValueError(f"Node {node.get('name')} is missing disk") + if not node.get("mac_addr"): + raise ValueError(f"Node {node.get('name')} is missing mac_addr") + if not re.match(r"(?:[0-9a-fA-F]:?){12}", node.get("mac_addr")): + raise ValueError(f"Node {node.get('name')} has an invalid mac_addr, is this a MAC address?") + if node.get("address"): + ip = validate_ip(node.get("address")) + if netaddr.IPAddress(ip, 4) not in netaddr.IPNetwork(node_cidr): + raise ValueError(f"Node {node.get('name')} is not in the node CIDR {node_cidr}") + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(5) + result = sock.connect_ex((ip, 50000)) + if result != 0: + raise ValueError(f"Node {node.get('name')} port 50000 is not open") + + +@required("bootstrap_cloudflare") +def validate_cli_tools(cloudflare: dict, **_) -> None: + for tool in GLOBAL_CLI_TOOLS: + if not which(tool): + raise ValueError(f"Missing required CLI tool {tool}") + for tool in CLOUDFLARE_TOOLS if cloudflare.get("enabled", False) else []: + if not which(tool): + raise ValueError(f"Missing required CLI tool {tool}") + + +@required("bootstrap_sops_age_pubkey") +def validate_age(key: str, **_) -> None: + if not re.match(r"^age1[a-z0-9]{0,58}$", key): + raise ValueError(f"Invalid Age public key {key}") + + +@required("bootstrap_node_network", "bootstrap_node_inventory") +def validate_nodes(node_cidr: str, nodes: dict[list], **_) -> None: + node_cidr = validate_network(node_cidr, 4) + + controllers = [node for node in nodes if node.get('controller') == True] + if len(controllers) < 1: + raise ValueError(f"Must have at least one controller node") + if len(controllers) % 2 == 0: + raise ValueError(f"Must have an odd number of controller nodes") + for node in controllers: + validate_node(node, node_cidr) + + workers = [node for node in nodes if node.get('controller') == False] + for node in workers: + validate_node(node, node_cidr) + + +def validate(data: dict) -> None: + validate_python_version() + validate_cli_tools(data) + validate_age(data) + + if not data.get("skip_tests", False): + validate_nodes(data) diff --git a/bootstrap/templates/.sops.yaml.j2 b/bootstrap/templates/.sops.yaml.j2 new file mode 100644 index 00000000..cb7aa764 --- /dev/null +++ b/bootstrap/templates/.sops.yaml.j2 @@ -0,0 +1,12 @@ +--- +creation_rules: + - # IMPORTANT: This rule MUST be above the others + path_regex: talos/.*\.sops\.ya?ml + key_groups: + - age: + - "#{ bootstrap_sops_age_pubkey }#" + - path_regex: kubernetes/.*\.sops\.ya?ml + encrypted_regex: "^(data|stringData)$" + key_groups: + - age: + - "#{ bootstrap_sops_age_pubkey }#" diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..fb668ce6 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/helmrelease.yaml.j2 @@ -0,0 +1,36 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2beta2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cert-manager +spec: + interval: 30m + chart: + spec: + chart: cert-manager + version: v1.14.5 + sourceRef: + kind: HelmRepository + name: jetstack + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + installCRDs: true + dns01RecursiveNameservers: 1.1.1.1:53,9.9.9.9:53 + dns01RecursiveNameserversOnly: true + podDnsPolicy: None + podDnsConfig: + nameservers: + - "1.1.1.1" + - "9.9.9.9" + prometheus: + enabled: true + servicemonitor: + enabled: true diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml.j2 new file mode 100644 index 00000000..23df7724 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/app/kustomization.yaml.j2 @@ -0,0 +1,5 @@ +--- + +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/.mjfilter.py b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/.mjfilter.py new file mode 100644 index 00000000..d9ae82b4 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/.mjfilter.py @@ -0,0 +1 @@ +main = lambda data: data.get("bootstrap_cloudflare", {}).get("enabled", False) == True diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml.j2 new file mode 100644 index 00000000..1cf7148a --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml.j2 @@ -0,0 +1,39 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: "${SECRET_ACME_EMAIL}" + privateKeySecretRef: + name: letsencrypt-production + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cert-manager-secret + key: api-token + selector: + dnsZones: + - "${SECRET_DOMAIN}" +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: "${SECRET_ACME_EMAIL}" + privateKeySecretRef: + name: letsencrypt-staging + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cert-manager-secret + key: api-token + selector: + dnsZones: + - "${SECRET_DOMAIN}" diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml.j2 new file mode 100644 index 00000000..fd43d965 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml.j2 @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./secret.sops.yaml + - ./issuers.yaml diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml.j2 new file mode 100644 index 00000000..f5bf887f --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-manager-secret +stringData: + api-token: "#{ bootstrap_cloudflare.token }#" diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 new file mode 100644 index 00000000..bd2f7357 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/cert-manager/ks.yaml.j2 @@ -0,0 +1,46 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cert-manager + namespace: flux-system +spec: + targetNamespace: cert-manager + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/cert-manager/cert-manager/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +#% if bootstrap_cloudflare.enabled %# +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cert-manager-issuers + namespace: flux-system +spec: + targetNamespace: cert-manager + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: cert-manager + path: ./kubernetes/apps/cert-manager/cert-manager/issuers + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +#% endif %# diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/kustomization.yaml.j2 new file mode 100644 index 00000000..abbe7755 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/kustomization.yaml.j2 @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./cert-manager/ks.yaml diff --git a/bootstrap/templates/kubernetes/apps/cert-manager/namespace.yaml.j2 b/bootstrap/templates/kubernetes/apps/cert-manager/namespace.yaml.j2 new file mode 100644 index 00000000..ed788350 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/cert-manager/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/bootstrap/templates/kubernetes/apps/flux-system/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/kustomization.yaml.j2 new file mode 100644 index 00000000..29d0612d --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/kustomization.yaml.j2 @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./webhooks/ks.yaml diff --git a/bootstrap/templates/kubernetes/apps/flux-system/namespace.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/namespace.yaml.j2 new file mode 100644 index 00000000..b48db452 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: flux-system + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/ingress.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/ingress.yaml.j2 new file mode 100644 index 00000000..e704eed3 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/ingress.yaml.j2 @@ -0,0 +1,25 @@ +#% if bootstrap_cloudflare.enabled %# +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: flux-webhook + annotations: + external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}" +spec: + ingressClassName: external + rules: + - host: &host "flux-webhook.${SECRET_DOMAIN}" + http: + paths: + - path: /hook/ + pathType: Prefix + backend: + service: + name: webhook-receiver + port: + number: 80 + tls: + - hosts: + - *host +#% endif %# diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/kustomization.yaml.j2 new file mode 100644 index 00000000..b40a47d6 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/kustomization.yaml.j2 @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./secret.sops.yaml + #% if bootstrap_cloudflare.enabled %# + - ./ingress.yaml + #% endif %# + - ./receiver.yaml diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2 new file mode 100644 index 00000000..cca5931b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/receiver.yaml.j2 @@ -0,0 +1,25 @@ +--- +apiVersion: notification.toolkit.fluxcd.io/v1 +kind: Receiver +metadata: + name: github-receiver +spec: + type: github + events: + - ping + - push + secretRef: + name: github-webhook-token-secret + resources: + - apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + name: home-kubernetes + namespace: flux-system + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + name: cluster + namespace: flux-system + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + name: cluster-apps + namespace: flux-system diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/secret.sops.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/secret.sops.yaml.j2 new file mode 100644 index 00000000..34ac7daf --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/github/secret.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: github-webhook-token-secret +stringData: + token: "#{ bootstrap_github_webhook_token }#" diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/kustomization.yaml.j2 new file mode 100644 index 00000000..08c1780f --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./github diff --git a/bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2 new file mode 100644 index 00000000..72081666 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/flux-system/webhooks/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app flux-webhooks + namespace: flux-system +spec: + targetNamespace: flux-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/flux-system/webhooks/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-bgp.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-bgp.yaml.j2 new file mode 100644 index 00000000..7e15b6f6 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-bgp.yaml.j2 @@ -0,0 +1,37 @@ +--- +# https://docs.cilium.io/en/latest/network/bgp-control-plane/ +apiVersion: cilium.io/v2alpha1 +kind: CiliumBGPPeeringPolicy +metadata: + name: policy +spec: + nodeSelector: + matchLabels: + kubernetes.io/os: linux + virtualRouters: + - localASN: #{ bootstrap_bgp.local_asn }# + neighbors: + #% if bootstrap_bgp.peers %# + #% for item in bootstrap_bgp.peers %# + - peerAddress: "#{ item }#/32" + peerASN: #{ bootstrap_bgp.peer_asn }# + #% endfor %# + #% else %# + #% if bootstrap_node_default_gateway %# + - peerAddress: "#{ bootstrap_node_default_gateway }#/32" + #% else %# + - peerAddress: "#{ bootstrap_node_network | nthhost(1) }#/32" + #% endif %# + peerASN: #{ bootstrap_bgp.peer_asn }# + #% endif %# + serviceSelector: + matchExpressions: + - {key: somekey, operator: NotIn, values: ['never-used-value']} +--- +apiVersion: cilium.io/v2alpha1 +kind: CiliumLoadBalancerIPPool +metadata: + name: pool +spec: + cidrs: + - cidr: "${BGP_ADVERTISED_CIDR}" diff --git a/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-l2.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-l2.yaml.j2 new file mode 100644 index 00000000..caa35cab --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/cilium-l2.yaml.j2 @@ -0,0 +1,22 @@ +--- +# https://docs.cilium.io/en/latest/network/l2-announcements +apiVersion: cilium.io/v2alpha1 +kind: CiliumL2AnnouncementPolicy +metadata: + name: policy +spec: + loadBalancerIPs: true + # NOTE: This might need to be set if you have more than one active NIC on your hosts + # interfaces: + # - ^eno[0-9]+ + nodeSelector: + matchLabels: + kubernetes.io/os: linux +--- +apiVersion: cilium.io/v2alpha1 +kind: CiliumLoadBalancerIPPool +metadata: + name: pool +spec: + cidrs: + - cidr: "${NODE_CIDR}" diff --git a/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..a7869100 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml.j2 @@ -0,0 +1,26 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cilium +spec: + interval: 30m + chart: + spec: + chart: cilium + version: 1.15.5 + sourceRef: + kind: HelmRepository + name: cilium + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + #% filter indent(width=4, first=True) %# + #% include 'partials/cilium-values-full.partial.yaml.j2' %# + #% endfilter %# diff --git a/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/kustomization.yaml.j2 new file mode 100644 index 00000000..8829dd64 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/cilium/app/kustomization.yaml.j2 @@ -0,0 +1,12 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + #% if bootstrap_bgp.enabled %# + - ./cilium-bgp.yaml + #% endif %# + #% if ((not bootstrap_bgp.enabled) and (not bootstrap_feature_gates.dual_stack_ipv4_first)) %# + - ./cilium-l2.yaml + #% endif %# + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2 new file mode 100644 index 00000000..9df69105 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/cilium/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cilium + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/cilium/app + prune: false # never should be deleted + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/.mjfilter.py b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/.mjfilter.py new file mode 100644 index 00000000..3ace63df --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/.mjfilter.py @@ -0,0 +1 @@ +main = lambda data: data.get("bootstrap_distribution", "k3s") in ["talos"] diff --git a/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..87572093 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml.j2 @@ -0,0 +1,30 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: kubelet-csr-approver +spec: + interval: 30m + chart: + spec: + chart: kubelet-csr-approver + version: 1.2.1 + sourceRef: + kind: HelmRepository + name: postfinance + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + #% filter indent(width=4, first=True) %# + #% include 'partials/kubelet-csr-approver-values.partial.yaml.j2' %# + #% endfilter %# + metrics: + enable: true + serviceMonitor: + enabled: true diff --git a/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2 new file mode 100644 index 00000000..f43156a8 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app kubelet-csr-approver + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/kubelet-csr-approver/app + prune: false # never should be deleted + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/kube-system/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/kustomization.yaml.j2 new file mode 100644 index 00000000..b4203ca2 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/kustomization.yaml.j2 @@ -0,0 +1,15 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./cilium/ks.yaml + #% if bootstrap_distribution in ["talos"] %# + - ./kubelet-csr-approver/ks.yaml + #% endif %# + - ./metrics-server/ks.yaml + #% if bootstrap_distribution in ["talos"] %# + - ./spegel/ks.yaml + #% endif %# + - ./reloader/ks.yaml diff --git a/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..ff9e8e0d --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml.j2 @@ -0,0 +1,32 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: metrics-server +spec: + interval: 30m + chart: + spec: + chart: metrics-server + version: 3.12.1 + sourceRef: + kind: HelmRepository + name: metrics-server + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + args: + - --kubelet-insecure-tls + - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname + - --kubelet-use-node-status-port + - --metric-resolution=15s + metrics: + enabled: true + serviceMonitor: + enabled: true diff --git a/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 new file mode 100644 index 00000000..6a21d99c --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/metrics-server/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app metrics-server + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/metrics-server/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/kube-system/namespace.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/namespace.yaml.j2 new file mode 100644 index 00000000..5eeb2c91 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kube-system + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/bootstrap/templates/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..f5cd4317 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml.j2 @@ -0,0 +1,29 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: reloader +spec: + interval: 30m + chart: + spec: + chart: reloader + version: 1.0.97 + sourceRef: + kind: HelmRepository + name: stakater + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + fullnameOverride: reloader + reloader: + readOnlyRootFileSystem: true + podMonitor: + enabled: true + namespace: "{{ .Release.Namespace }}" diff --git a/bootstrap/templates/kubernetes/apps/kube-system/reloader/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/reloader/app/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/reloader/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2 new file mode 100644 index 00000000..0aae5261 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/reloader/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app reloader + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/reloader/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/kube-system/spegel/.mjfilter.py b/bootstrap/templates/kubernetes/apps/kube-system/spegel/.mjfilter.py new file mode 100644 index 00000000..3ace63df --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/spegel/.mjfilter.py @@ -0,0 +1 @@ +main = lambda data: data.get("bootstrap_distribution", "k3s") in ["talos"] diff --git a/bootstrap/templates/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..d03f9c5d --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml.j2 @@ -0,0 +1,31 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: spegel +spec: + interval: 30m + chart: + spec: + chart: spegel + version: v0.0.18 + sourceRef: + kind: HelmRepository + name: xenitab + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + spegel: + containerdSock: /run/containerd/containerd.sock + containerdRegistryConfigPath: /etc/cri/conf.d/hosts + service: + registry: + hostPort: 29999 + serviceMonitor: + enabled: true diff --git a/bootstrap/templates/kubernetes/apps/kube-system/spegel/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/spegel/app/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/spegel/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2 new file mode 100644 index 00000000..8f129bd6 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/kube-system/spegel/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app spegel + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/spegel/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/network/.mjfilter.py b/bootstrap/templates/kubernetes/apps/network/.mjfilter.py new file mode 100644 index 00000000..d9ae82b4 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/.mjfilter.py @@ -0,0 +1 @@ +main = lambda data: data.get("bootstrap_cloudflare", {}).get("enabled", False) == True diff --git a/bootstrap/templates/kubernetes/apps/network/cloudflared/app/configs/config.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/configs/config.yaml.j2 new file mode 100644 index 00000000..05bcef5c --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/configs/config.yaml.j2 @@ -0,0 +1,10 @@ +--- +originRequest: + originServerName: "external.${SECRET_DOMAIN}" + +ingress: + - hostname: "${SECRET_DOMAIN}" + service: https://ingress-nginx-external-controller.network.svc.cluster.local:443 + - hostname: "*.${SECRET_DOMAIN}" + service: https://ingress-nginx-external-controller.network.svc.cluster.local:443 + - service: http_status:404 diff --git a/bootstrap/templates/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml.j2 new file mode 100644 index 00000000..43d7d7b2 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml.j2 @@ -0,0 +1,10 @@ +--- +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: cloudflared +spec: + endpoints: + - dnsName: "external.${SECRET_DOMAIN}" + recordType: CNAME + targets: ["${SECRET_CLOUDFLARE_TUNNEL_ID}.cfargotunnel.com"] diff --git a/bootstrap/templates/kubernetes/apps/network/cloudflared/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..562fbfad --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/helmrelease.yaml.j2 @@ -0,0 +1,113 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cloudflared +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + controllers: + cloudflared: + replicas: 2 + strategy: RollingUpdate + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: docker.io/cloudflare/cloudflared + tag: 2024.5.0 + env: + NO_AUTOUPDATE: true + TUNNEL_CRED_FILE: /etc/cloudflared/creds/credentials.json + TUNNEL_METRICS: 0.0.0.0:8080 + TUNNEL_ORIGIN_ENABLE_HTTP2: true + TUNNEL_TRANSPORT_PROTOCOL: quic + TUNNEL_POST_QUANTUM: true + TUNNEL_ID: + valueFrom: + secretKeyRef: + name: cloudflared-secret + key: TUNNEL_ID + args: + - tunnel + - --config + - /etc/cloudflared/config/config.yaml + - run + - "$(TUNNEL_ID)" + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /ready + port: &port 8080 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + startup: + enabled: false + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + seccompProfile: + type: RuntimeDefault + resources: + requests: + cpu: 10m + limits: + memory: 256Mi + pod: + securityContext: + runAsUser: 65534 + runAsGroup: 65534 + runAsNonRoot: true + service: + app: + controller: cloudflared + ports: + http: + port: *port + serviceMonitor: + app: + serviceName: cloudflared + endpoints: + - port: http + scheme: http + path: /metrics + interval: 1m + scrapeTimeout: 10s + persistence: + config: + type: configMap + name: cloudflared-configmap + globalMounts: + - path: /etc/cloudflared/config/config.yaml + subPath: config.yaml + readOnly: true + creds: + type: secret + name: cloudflared-secret + globalMounts: + - path: /etc/cloudflared/creds/credentials.json + subPath: credentials.json + readOnly: true diff --git a/bootstrap/templates/kubernetes/apps/network/cloudflared/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/kustomization.yaml.j2 new file mode 100644 index 00000000..37b1f4e4 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/kustomization.yaml.j2 @@ -0,0 +1,14 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./dnsendpoint.yaml + - ./secret.sops.yaml + - ./helmrelease.yaml +configMapGenerator: + - name: cloudflared-configmap + files: + - ./configs/config.yaml +generatorOptions: + disableNameSuffixHash: true diff --git a/bootstrap/templates/kubernetes/apps/network/cloudflared/app/secret.sops.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/secret.sops.yaml.j2 new file mode 100644 index 00000000..67d169ed --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/cloudflared/app/secret.sops.yaml.j2 @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cloudflared-secret +stringData: + TUNNEL_ID: "#{ bootstrap_cloudflare.tunnel.id }#" + credentials.json: | + { + "AccountTag": "#{ bootstrap_cloudflare.tunnel.account_id }#", + "TunnelSecret": "#{ bootstrap_cloudflare.tunnel.secret }#", + "TunnelID": "#{ bootstrap_cloudflare.tunnel.id }#" + } diff --git a/bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2 new file mode 100644 index 00000000..deb7873e --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/cloudflared/ks.yaml.j2 @@ -0,0 +1,23 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cloudflared + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-dns + path: ./kubernetes/apps/network/cloudflared/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/network/echo-server/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/echo-server/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..0993f6cd --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/echo-server/app/helmrelease.yaml.j2 @@ -0,0 +1,95 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: echo-server +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + controllers: + echo-server: + strategy: RollingUpdate + containers: + app: + image: + repository: ghcr.io/mendhak/http-https-echo + tag: 33 + env: + HTTP_PORT: &port 8080 + LOG_WITHOUT_NEWLINE: true + LOG_IGNORE_PATH: /healthz + PROMETHEUS_ENABLED: true + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /healthz + port: *port + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + seccompProfile: + type: RuntimeDefault + resources: + requests: + cpu: 10m + limits: + memory: 64Mi + pod: + securityContext: + runAsUser: 65534 + runAsGroup: 65534 + runAsNonRoot: true + service: + app: + controller: echo-server + ports: + http: + port: *port + serviceMonitor: + app: + serviceName: echo-server + endpoints: + - port: http + scheme: http + path: /metrics + interval: 1m + scrapeTimeout: 10s + ingress: + app: + className: external + annotations: + external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}" + hosts: + - host: &host "{{ .Release.Name }}.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + tls: + - hosts: + - *host diff --git a/bootstrap/templates/kubernetes/apps/network/echo-server/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/echo-server/app/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/echo-server/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2 new file mode 100644 index 00000000..0cfc7559 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/echo-server/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app echo-server + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/network/echo-server/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/network/external-dns/app/dnsendpoint-crd.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/external-dns/app/dnsendpoint-crd.yaml.j2 new file mode 100644 index 00000000..9254f89d --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/external-dns/app/dnsendpoint-crd.yaml.j2 @@ -0,0 +1,93 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.5.0 + api-approved.kubernetes.io: "https://github.com/kubernetes-sigs/external-dns/pull/2007" + creationTimestamp: null + name: dnsendpoints.externaldns.k8s.io +spec: + group: externaldns.k8s.io + names: + kind: DNSEndpoint + listKind: DNSEndpointList + plural: dnsendpoints + singular: dnsendpoint + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DNSEndpointSpec defines the desired state of DNSEndpoint + properties: + endpoints: + items: + description: Endpoint is a high-level way of a connection between a service and an IP + properties: + dnsName: + description: The hostname of the DNS record + type: string + labels: + additionalProperties: + type: string + description: Labels stores labels defined for the Endpoint + type: object + providerSpecific: + description: ProviderSpecific stores provider specific config + items: + description: ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers + properties: + name: + type: string + value: + type: string + type: object + type: array + recordTTL: + description: TTL for the record + format: int64 + type: integer + recordType: + description: RecordType type of record, e.g. CNAME, A, SRV, TXT etc + type: string + setIdentifier: + description: Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple') + type: string + targets: + description: The targets the DNS record points to + items: + type: string + type: array + type: object + type: array + type: object + status: + description: DNSEndpointStatus defines the observed state of DNSEndpoint + properties: + observedGeneration: + description: The generation observed by the external-dns controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/bootstrap/templates/kubernetes/apps/network/external-dns/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/external-dns/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..8a6e5d7e --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/external-dns/app/helmrelease.yaml.j2 @@ -0,0 +1,45 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app external-dns +spec: + interval: 30m + chart: + spec: + chart: external-dns + version: 1.14.4 + sourceRef: + kind: HelmRepository + name: external-dns + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + fullnameOverride: *app + provider: cloudflare + env: + - name: CF_API_TOKEN + valueFrom: + secretKeyRef: + name: external-dns-secret + key: api-token + extraArgs: + - --ingress-class=external + - --cloudflare-proxied + - --crd-source-apiversion=externaldns.k8s.io/v1alpha1 + - --crd-source-kind=DNSEndpoint + policy: sync + sources: ["crd", "ingress"] + txtPrefix: k8s. + txtOwnerId: default + domainFilters: ["${SECRET_DOMAIN}"] + serviceMonitor: + enabled: true + podAnnotations: + secret.reloader.stakater.com/reload: external-dns-secret diff --git a/bootstrap/templates/kubernetes/apps/network/external-dns/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/external-dns/app/kustomization.yaml.j2 new file mode 100644 index 00000000..406c4615 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/external-dns/app/kustomization.yaml.j2 @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./dnsendpoint-crd.yaml + - ./secret.sops.yaml + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/network/external-dns/app/secret.sops.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/external-dns/app/secret.sops.yaml.j2 new file mode 100644 index 00000000..c067b329 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/external-dns/app/secret.sops.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: external-dns-secret +stringData: + api-token: "#{ bootstrap_cloudflare.token }#" diff --git a/bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2 new file mode 100644 index 00000000..5c554ef1 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/external-dns/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app external-dns + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/network/external-dns/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml.j2 new file mode 100644 index 00000000..87bf7948 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml.j2 @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./staging.yaml + #% if bootstrap_cloudflare.acme.production %# + - ./production.yaml + #% endif %# diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/production.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/production.yaml.j2 new file mode 100644 index 00000000..b5afdf41 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/production.yaml.j2 @@ -0,0 +1,14 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: "${SECRET_DOMAIN/./-}-production" +spec: + secretName: "${SECRET_DOMAIN/./-}-production-tls" + issuerRef: + name: letsencrypt-production + kind: ClusterIssuer + commonName: "${SECRET_DOMAIN}" + dnsNames: + - "${SECRET_DOMAIN}" + - "*.${SECRET_DOMAIN}" diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml.j2 new file mode 100644 index 00000000..9c869425 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml.j2 @@ -0,0 +1,14 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: "${SECRET_DOMAIN/./-}-staging" +spec: + secretName: "${SECRET_DOMAIN/./-}-staging-tls" + issuerRef: + name: letsencrypt-staging + kind: ClusterIssuer + commonName: "${SECRET_DOMAIN}" + dnsNames: + - "${SECRET_DOMAIN}" + - "*.${SECRET_DOMAIN}" diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml.j2 new file mode 100644 index 00000000..60b83c6b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml.j2 @@ -0,0 +1,91 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: ingress-nginx-external +spec: + interval: 30m + chart: + spec: + chart: ingress-nginx + version: 4.10.1 + sourceRef: + kind: HelmRepository + name: ingress-nginx + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + dependsOn: + - name: cloudflared + namespace: network + values: + fullnameOverride: ingress-nginx-external + controller: + replicaCount: 1 + service: + annotations: + external-dns.alpha.kubernetes.io/hostname: "external.${SECRET_DOMAIN}" + io.cilium/lb-ipam-ips: "#{ bootstrap_cloudflare.tunnel.ingress_vip }#" + externalTrafficPolicy: Cluster + ingressClassResource: + name: external + default: false + controllerValue: k8s.io/external + admissionWebhooks: + objectSelector: + matchExpressions: + - key: ingress-class + operator: In + values: ["external"] + config: + client-body-buffer-size: 100M + client-body-timeout: 120 + client-header-timeout: 120 + enable-brotli: "true" + enable-real-ip: "true" + hsts-max-age: 31449600 + keep-alive-requests: 10000 + keep-alive: 120 + log-format-escape-json: "true" + log-format-upstream: > + {"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for", + "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, + "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", + "request_length": $request_length, "duration": $request_time, "method": "$request_method", "http_referrer": "$http_referer", + "http_user_agent": "$http_user_agent"} + proxy-body-size: 0 + proxy-buffer-size: 16k + ssl-protocols: TLSv1.3 TLSv1.2 + metrics: + enabled: true + serviceMonitor: + enabled: true + namespaceSelector: + any: true + extraArgs: + #% if bootstrap_cloudflare.acme.production %# + default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-production-tls" + #% else %# + default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-staging-tls" + #% endif %# + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx-external + app.kubernetes.io/component: controller + resources: + requests: + cpu: 100m + limits: + memory: 500Mi + defaultBackend: + enabled: false diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml.j2 new file mode 100644 index 00000000..045eed32 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml.j2 @@ -0,0 +1,88 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: ingress-nginx-internal + namespace: network +spec: + interval: 30m + chart: + spec: + chart: ingress-nginx + version: 4.10.1 + sourceRef: + kind: HelmRepository + name: ingress-nginx + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + fullnameOverride: ingress-nginx-internal + controller: + replicaCount: 1 + service: + annotations: + io.cilium/lb-ipam-ips: "#{ bootstrap_cloudflare.ingress_vip }#" + externalTrafficPolicy: Cluster + ingressClassResource: + name: internal + default: true + controllerValue: k8s.io/internal + admissionWebhooks: + objectSelector: + matchExpressions: + - key: ingress-class + operator: In + values: ["internal"] + config: + client-body-buffer-size: 100M + client-body-timeout: 120 + client-header-timeout: 120 + enable-brotli: "true" + enable-real-ip: "true" + hsts-max-age: 31449600 + keep-alive-requests: 10000 + keep-alive: 120 + log-format-escape-json: "true" + log-format-upstream: > + {"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for", + "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, + "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", + "request_length": $request_length, "duration": $request_time, "method": "$request_method", "http_referrer": "$http_referer", + "http_user_agent": "$http_user_agent"} + proxy-body-size: 0 + proxy-buffer-size: 16k + ssl-protocols: TLSv1.3 TLSv1.2 + metrics: + enabled: true + serviceMonitor: + enabled: true + namespaceSelector: + any: true + extraArgs: + #% if bootstrap_cloudflare.acme.production %# + default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-production-tls" + #% else %# + default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-staging-tls" + #% endif %# + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx-internal + app.kubernetes.io/component: controller + resources: + requests: + cpu: 100m + limits: + memory: 500Mi + defaultBackend: + enabled: false diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2 new file mode 100644 index 00000000..4121eab5 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/ingress-nginx/ks.yaml.j2 @@ -0,0 +1,69 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app ingress-nginx-certificates + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: cert-manager-issuers + path: ./kubernetes/apps/network/ingress-nginx/certificates + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app ingress-nginx-internal + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: ingress-nginx-certificates + path: ./kubernetes/apps/network/ingress-nginx/internal + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app ingress-nginx-external + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: ingress-nginx-certificates + path: ./kubernetes/apps/network/ingress-nginx/external + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..b8e7db69 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml.j2 @@ -0,0 +1,32 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: k8s-gateway +spec: + interval: 30m + chart: + spec: + chart: k8s-gateway + version: 2.4.0 + sourceRef: + kind: HelmRepository + name: k8s-gateway + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + fullnameOverride: k8s-gateway + domain: "${SECRET_DOMAIN}" + ttl: 1 + service: + type: LoadBalancer + port: 53 + annotations: + io.cilium/lb-ipam-ips: "#{ bootstrap_cloudflare.gateway_vip }#" + externalTrafficPolicy: Cluster diff --git a/bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml.j2 new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 new file mode 100644 index 00000000..6709e768 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/k8s-gateway/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app k8s-gateway + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/network/k8s-gateway/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/network/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/kustomization.yaml.j2 new file mode 100644 index 00000000..2dc9a0db --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/kustomization.yaml.j2 @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./cloudflared/ks.yaml + - ./echo-server/ks.yaml + - ./external-dns/ks.yaml + - ./ingress-nginx/ks.yaml + - ./k8s-gateway/ks.yaml diff --git a/bootstrap/templates/kubernetes/apps/network/namespace.yaml.j2 b/bootstrap/templates/kubernetes/apps/network/namespace.yaml.j2 new file mode 100644 index 00000000..4d78d7b1 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/network/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: network + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/.mjfilter.py b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/.mjfilter.py new file mode 100644 index 00000000..0979f9a6 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/.mjfilter.py @@ -0,0 +1 @@ +main = lambda data: data.get("bootstrap_distribution", "k3s") in ["k3s"] diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/kustomization.yaml.j2 new file mode 100644 index 00000000..9afab41b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./plan.yaml diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/plan.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/plan.yaml.j2 new file mode 100644 index 00000000..5412ea57 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/app/plan.yaml.j2 @@ -0,0 +1,50 @@ +--- +apiVersion: upgrade.cattle.io/v1 +kind: Plan +metadata: + name: controllers +spec: + version: "${KUBE_VERSION}" + upgrade: + image: rancher/k3s-upgrade + serviceAccountName: system-upgrade + concurrency: 1 + cordon: true + nodeSelector: + matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: Exists + tolerations: + - effect: NoSchedule + operator: Exists + - effect: NoExecute + operator: Exists + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + operator: Exists + - key: node-role.kubernetes.io/master + effect: NoSchedule + operator: Exists + - key: node-role.kubernetes.io/etcd + effect: NoExecute + operator: Exists + - key: CriticalAddonsOnly + operator: Exists +--- +apiVersion: upgrade.cattle.io/v1 +kind: Plan +metadata: + name: workers +spec: + version: "${KUBE_VERSION}" + serviceAccountName: system-upgrade + concurrency: 1 + nodeSelector: + matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: DoesNotExist + prepare: + image: rancher/k3s-upgrade + args: ["prepare", "server"] + upgrade: + image: rancher/k3s-upgrade diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/ks.yaml.j2 new file mode 100644 index 00000000..4c7c55a4 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/k3s/ks.yaml.j2 @@ -0,0 +1,27 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app system-upgrade-k3s + namespace: flux-system +spec: + targetNamespace: system-upgrade + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: system-upgrade-controller + path: ./kubernetes/apps/system-upgrade/k3s/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + # renovate: datasource=github-releases depName=k3s-io/k3s + KUBE_VERSION: v1.30.0+k3s1 diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/kustomization.yaml.j2 new file mode 100644 index 00000000..dd2adbef --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/kustomization.yaml.j2 @@ -0,0 +1,15 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + #% if bootstrap_distribution in ['k3s', 'talos'] %# + - ./system-upgrade-controller/ks.yaml + #% endif %# + #% if bootstrap_distribution in ["k3s"] %# + - ./k3s/ks.yaml + #% endif %# + #% if bootstrap_distribution in ["talos"] and bootstrap_talos.schematic_id %# + - ./talos/ks.yaml + #% endif %# diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/namespace.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/namespace.yaml.j2 new file mode 100644 index 00000000..5ea024dd --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/namespace.yaml.j2 @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: system-upgrade + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/.mjfilter.py b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/.mjfilter.py new file mode 100644 index 00000000..394f9d1e --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/.mjfilter.py @@ -0,0 +1 @@ +main = lambda data: data.get("bootstrap_distribution", "k3s") in ["k3s", "talos"] diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/helmrelease.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/helmrelease.yaml.j2 new file mode 100644 index 00000000..d5f72848 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/helmrelease.yaml.j2 @@ -0,0 +1,102 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app system-upgrade-controller +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + controllers: + system-upgrade-controller: + strategy: RollingUpdate + containers: + app: + image: + repository: docker.io/rancher/system-upgrade-controller + tag: v0.13.4 + env: + SYSTEM_UPGRADE_CONTROLLER_DEBUG: false + SYSTEM_UPGRADE_CONTROLLER_THREADS: 2 + SYSTEM_UPGRADE_JOB_ACTIVE_DEADLINE_SECONDS: 900 + SYSTEM_UPGRADE_JOB_BACKOFF_LIMIT: 99 + SYSTEM_UPGRADE_JOB_IMAGE_PULL_POLICY: IfNotPresent + SYSTEM_UPGRADE_JOB_KUBECTL_IMAGE: registry.k8s.io/kubectl:v1.30.1 + SYSTEM_UPGRADE_JOB_PRIVILEGED: true + SYSTEM_UPGRADE_JOB_TTL_SECONDS_AFTER_FINISH: 900 + SYSTEM_UPGRADE_PLAN_POLLING_INTERVAL: 15m + SYSTEM_UPGRADE_CONTROLLER_NAME: *app + SYSTEM_UPGRADE_CONTROLLER_NAMESPACE: + valueFrom: + fieldRef: + fieldPath: metadata.namespace + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + seccompProfile: + type: RuntimeDefault + pod: + securityContext: + runAsUser: 65534 + runAsGroup: 65534 + runAsNonRoot: true + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: Exists + tolerations: + - key: CriticalAddonsOnly + operator: Exists + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + serviceAccount: + create: true + name: system-upgrade + persistence: + tmp: + type: emptyDir + globalMounts: + - path: /tmp + etc-ssl: + type: hostPath + hostPath: /etc/ssl + hostPathType: DirectoryOrCreate + globalMounts: + - path: /etc/ssl + readOnly: true + etc-pki: + type: hostPath + hostPath: /etc/pki + hostPathType: DirectoryOrCreate + globalMounts: + - path: /etc/pki + readOnly: true + etc-ca-certificates: + type: hostPath + hostPath: /etc/ca-certificates + hostPathType: DirectoryOrCreate + globalMounts: + - path: /etc/ca-certificates + readOnly: true diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/kustomization.yaml.j2 new file mode 100644 index 00000000..b27bf573 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/kustomization.yaml.j2 @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + # renovate: datasource=github-releases depName=rancher/system-upgrade-controller + - https://github.com/rancher/system-upgrade-controller/releases/download/v0.13.4/crd.yaml + - helmrelease.yaml + - rbac.yaml diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/rbac.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/rbac.yaml.j2 new file mode 100644 index 00000000..ddc6127f --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/app/rbac.yaml.j2 @@ -0,0 +1,23 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system-upgrade +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: system-upgrade + namespace: system-upgrade +#% if bootstrap_distribution in ["talos"] %# +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: talos +spec: + roles: + - os:admin +#% endif %# diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/ks.yaml.j2 new file mode 100644 index 00000000..212eccec --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/system-upgrade-controller/ks.yaml.j2 @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app system-upgrade-controller + namespace: flux-system +spec: + targetNamespace: system-upgrade + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/system-upgrade/system-upgrade-controller/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/talos/.mjfilter.py b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/.mjfilter.py new file mode 100644 index 00000000..82712ee6 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/.mjfilter.py @@ -0,0 +1,4 @@ +main = lambda data: ( + data.get("bootstrap_distribution", "k3s") in ["talos"] and + data.get("talos", {}).get("schematic_id", {}) +) diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/kustomization.yaml.j2 new file mode 100644 index 00000000..9afab41b --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/kustomization.yaml.j2 @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./plan.yaml diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/plan.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/plan.yaml.j2 new file mode 100644 index 00000000..88228c88 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/app/plan.yaml.j2 @@ -0,0 +1,93 @@ +--- +apiVersion: upgrade.cattle.io/v1 +kind: Plan +metadata: + name: kubernetes +spec: + version: "${KUBE_VERSION}" + serviceAccountName: system-upgrade + secrets: + - name: talos + path: /var/run/secrets/talos.dev + ignoreUpdates: true + concurrency: 1 + exclusive: true + nodeSelector: + matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: Exists + prepare: &prepare + image: "ghcr.io/siderolabs/talosctl:${SYSTEM_VERSION}" + envs: + - name: NODE_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + args: + - --nodes=$(NODE_IP) + - health + - --server=false + upgrade: + <<: *prepare + args: + - --nodes=$(NODE_IP) + - upgrade-k8s + - --to=$(SYSTEM_UPGRADE_PLAN_LATEST_VERSION) +--- +apiVersion: upgrade.cattle.io/v1 +kind: Plan +metadata: + name: talos +spec: + version: "${SYSTEM_VERSION}" + serviceAccountName: system-upgrade + secrets: + - name: talos + path: /var/run/secrets/talos.dev + ignoreUpdates: true + concurrency: 1 + cordon: true + drain: + deleteLocalData: true + disableEviction: false + ignoreDaemonSets: true + exclusive: true + nodeSelector: + matchExpressions: + - key: kubernetes.io/os + operator: In + values: ["linux"] + tolerations: + - key: CriticalAddonsOnly + operator: Exists + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/controlplane + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/etcd + operator: Exists + effect: NoSchedule + prepare: &prepare + image: ghcr.io/siderolabs/talosctl + envs: + - name: NODE_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + args: + - --nodes=$(NODE_IP) + - health + - --server=false + upgrade: + <<: *prepare + args: + - --nodes=$(NODE_IP) + - upgrade + - --image=factory.talos.dev/installer/#{ bootstrap_talos.schematic_id }#:$(SYSTEM_UPGRADE_PLAN_LATEST_VERSION) + - --preserve=true + - --wait=false diff --git a/bootstrap/templates/kubernetes/apps/system-upgrade/talos/ks.yaml.j2 b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/ks.yaml.j2 new file mode 100644 index 00000000..7dd32833 --- /dev/null +++ b/bootstrap/templates/kubernetes/apps/system-upgrade/talos/ks.yaml.j2 @@ -0,0 +1,29 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app system-upgrade-talos + namespace: flux-system +spec: + targetNamespace: system-upgrade + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: system-upgrade-controller + path: ./kubernetes/apps/system-upgrade/talos/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + # renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet + KUBE_VERSION: v1.30.1 + # renovate: datasource=docker depName=ghcr.io/siderolabs/installer + SYSTEM_VERSION: v1.7.2 diff --git a/bootstrap/templates/kubernetes/bootstrap/flux/github-deploy-key.sops.yaml.j2 b/bootstrap/templates/kubernetes/bootstrap/flux/github-deploy-key.sops.yaml.j2 new file mode 100644 index 00000000..0ef1f6e8 --- /dev/null +++ b/bootstrap/templates/kubernetes/bootstrap/flux/github-deploy-key.sops.yaml.j2 @@ -0,0 +1,17 @@ +#% if bootstrap_github_private_key %# +--- +apiVersion: v1 +kind: Secret +metadata: + name: github-deploy-key + namespace: flux-system +stringData: + identity: | + #% filter indent(width=4, first=False) %# + #{ bootstrap_github_private_key }# + #%- endfilter %# + known_hosts: | + github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl + github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= + github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk= +#% endif %# diff --git a/bootstrap/templates/kubernetes/bootstrap/flux/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/bootstrap/flux/kustomization.yaml.j2 new file mode 100644 index 00000000..1d9ad47f --- /dev/null +++ b/bootstrap/templates/kubernetes/bootstrap/flux/kustomization.yaml.j2 @@ -0,0 +1,62 @@ +# IMPORTANT: This file is not tracked by flux and should never be. Its +# purpose is to only install the Flux components and CRDs into your cluster. +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - github.com/fluxcd/flux2/manifests/install?ref=v2.3.0 +patches: + # Remove the default network policies + - patch: |- + $patch: delete + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: not-used + target: + group: networking.k8s.io + kind: NetworkPolicy + # Resources renamed to match those installed by oci://ghcr.io/fluxcd/flux-manifests + - target: + kind: ResourceQuota + name: critical-pods + patch: | + - op: replace + path: /metadata/name + value: critical-pods-flux-system + - target: + kind: ClusterRoleBinding + name: cluster-reconciler + patch: | + - op: replace + path: /metadata/name + value: cluster-reconciler-flux-system + - target: + kind: ClusterRoleBinding + name: crd-controller + patch: | + - op: replace + path: /metadata/name + value: crd-controller-flux-system + - target: + kind: ClusterRole + name: crd-controller + patch: | + - op: replace + path: /metadata/name + value: crd-controller-flux-system + - target: + kind: ClusterRole + name: flux-edit + patch: | + - op: replace + path: /metadata/name + value: flux-edit-flux-system + - target: + kind: ClusterRole + name: flux-view + patch: | + - op: replace + path: /metadata/name + value: flux-view-flux-system diff --git a/bootstrap/templates/kubernetes/bootstrap/talos/helmfile.yaml.j2 b/bootstrap/templates/kubernetes/bootstrap/talos/helmfile.yaml.j2 new file mode 100644 index 00000000..f97743bd --- /dev/null +++ b/bootstrap/templates/kubernetes/bootstrap/talos/helmfile.yaml.j2 @@ -0,0 +1,59 @@ +--- +repositories: + - name: cilium + url: https://helm.cilium.io + - name: coredns + url: https://coredns.github.io/helm + - name: postfinance + url: https://postfinance.github.io/kubelet-csr-approver + +helmDefaults: + wait: true + waitForJobs: true + timeout: 600 + recreatePods: true + force: true + +releases: + - name: prometheus-operator-crds + namespace: observability + chart: oci://ghcr.io/prometheus-community/charts/prometheus-operator-crds + version: 11.0.0 + - name: cilium + namespace: kube-system + chart: cilium/cilium + version: 1.15.5 + values: + - ../../apps/kube-system/cilium/app/helm-values.yaml + needs: + - observability/prometheus-operator-crds + - name: coredns + namespace: kube-system + chart: coredns/coredns + version: 1.29.0 + values: + - ../../apps/kube-system/coredns/app/helm-values.yaml + needs: + - observability/prometheus-operator-crds + - kube-system/cilium + - name: kubelet-csr-approver + namespace: kube-system + chart: postfinance/kubelet-csr-approver + version: 1.2.1 + values: + - ../../apps/kube-system/kubelet-csr-approver/app/helm-values.yaml + needs: + - observability/prometheus-operator-crds + - kube-system/cilium + - kube-system/coredns + - name: spegel + namespace: kube-system + chart: oci://ghcr.io/spegel-org/helm-charts/spegel + version: v0.0.22 + values: + - ../../apps/kube-system/spegel/app/helm-values.yaml + needs: + - observability/prometheus-operator-crds + - kube-system/cilium + - kube-system/coredns + - kube-system/kubelet-csr-approver diff --git a/bootstrap/templates/kubernetes/bootstrap/talos/talconfig.yaml.j2 b/bootstrap/templates/kubernetes/bootstrap/talos/talconfig.yaml.j2 new file mode 100644 index 00000000..3dff2814 --- /dev/null +++ b/bootstrap/templates/kubernetes/bootstrap/talos/talconfig.yaml.j2 @@ -0,0 +1,251 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/budimanjojo/talhelper/master/pkg/config/schemas/talconfig.json +--- +# renovate: datasource=docker depName=ghcr.io/siderolabs/installer +talosVersion: v1.7.2 +# renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet +kubernetesVersion: v1.30.1 + +clusterName: "#{ bootstrap_cluster_name | default('home-kubernetes', true) }#" +endpoint: https://#{ bootstrap_controller_vip }#:6443 +clusterPodNets: + - "#{ bootstrap_pod_network.split(',')[0] }#" +clusterSvcNets: + - "#{ bootstrap_service_network.split(',')[0] }#" +additionalApiServerCertSans: &sans + - "#{ bootstrap_controller_vip }#" + - 127.0.0.1 # KubePrism + #% for item in bootstrap_tls_sans %# + - "#{ item }#" + #% endfor %# +additionalMachineCertSans: *sans + +# Disable built-in Flannel to use Cilium +cniConfig: + name: none + +nodes: + #% for item in bootstrap_node_inventory %# + - hostname: "#{ item.name }#" + ipAddress: "#{ item.address }#" + #% if item.disk.startswith('/') %# + installDisk: "#{ item.disk }#" + #% else %# + installDiskSelector: + serial: "#{ item.disk }#" + #% endif %# + #% if bootstrap_secureboot.enabled %# + machineSpec: + secureboot: true + talosImageURL: factory.talos.dev/installer-secureboot/#{ bootstrap_schematic_id }# + #% else %# + talosImageURL: factory.talos.dev/installer/#{ bootstrap_schematic_id }# + #% endif %# + controlPlane: #{ (item.controller) | string | lower }# + networkInterfaces: + - deviceSelector: + hardwareAddr: "#{ item.mac_addr | lower }#" + #% if bootstrap_vlan %# + vlans: + - vlanId: #{ bootstrap_vlan }# + addresses: + - "#{ item.address }#/#{ bootstrap_node_network.split('/') | last }#" + mtu: #{ item.mtu | default(1500) }# + routes: + - network: 0.0.0.0/0 + #% if bootstrap_node_default_gateway %# + gateway: "#{ bootstrap_node_default_gateway }#" + #% else %# + gateway: "#{ bootstrap_node_network | nthhost(1) }#" + #% endif %# + #% if item.controller %# + vip: + ip: "#{ bootstrap_controller_vip }#" + #% endif %# + #% else %# + #% if item.address %# + dhcp: false + addresses: + - "#{ item.address }#/#{ bootstrap_node_network.split('/') | last }#" + routes: + - network: 0.0.0.0/0 + #% if bootstrap_node_default_gateway %# + gateway: "#{ bootstrap_node_default_gateway }#" + #% else %# + gateway: "#{ bootstrap_node_network | nthhost(1) }#" + #% endif %# + #% else %# + dhcp: true + #% endif %# + mtu: #{ item.mtu | default(1500) }# + #% if item.controller %# + vip: + ip: "#{ bootstrap_controller_vip }#" + #% endif %# + #% endif %# + #% if bootstrap_user_patches %# + patches: + - "@./patches/node_#{ item.name }#.yaml" + #% endif %# + #% endfor %# + +patches: + # Configure containerd + - |- + machine: + files: + - op: create + path: /etc/cri/conf.d/20-customization.part + content: |- + [plugins."io.containerd.grpc.v1.cri"] + enable_unprivileged_ports = true + enable_unprivileged_icmp = true + [plugins."io.containerd.grpc.v1.cri".containerd] + discard_unpacked_layers = false + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] + discard_unpacked_layers = false + + # Disable search domain everywhere + - |- + machine: + network: + disableSearchDomain: true + + # Enable cluster discovery + - |- + cluster: + discovery: + registries: + kubernetes: + disabled: false + service: + disabled: false + + # Configure kubelet + - |- + machine: + kubelet: + extraArgs: + rotate-server-certificates: true + nodeIP: + validSubnets: + - #{ bootstrap_node_network }# + + #% if bootstrap_dns_servers | length %# + # Force nameserver + - |- + machine: + network: + nameservers: + #% for item in bootstrap_dns_servers %# + - #{ item }# + #% endfor %# + #% endif %# + + #% if bootstrap_ntp_servers | length %# + # Configure NTP + - |- + machine: + time: + disabled: false + servers: + #% for item in bootstrap_ntp_servers %# + - #{ item }# + #% endfor %# + #% endif %# + + # Custom sysctl settings + - |- + machine: + sysctls: + fs.inotify.max_queued_events: "65536" + fs.inotify.max_user_watches: "524288" + fs.inotify.max_user_instances: "8192" + net.core.rmem_max: "2500000" + net.core.wmem_max: "2500000" + + # Mount openebs-hostpath in kubelet + - |- + machine: + kubelet: + extraMounts: + - destination: /var/openebs/local + type: bind + source: /var/openebs/local + options: + - bind + - rshared + - rw + + #% if bootstrap_secureboot.enabled and bootstrap_secureboot.encrypt_disk_with_tpm %# + # Encrypt system disk with TPM + - |- + machine: + systemDiskEncryption: + ephemeral: + provider: luks2 + keys: + - slot: 0 + tpm: {} + state: + provider: luks2 + keys: + - slot: 0 + tpm: {} + #% endif %# + + #% if bootstrap_user_patches %# + # User specified global patches + - "@./patches/global.yaml" + #% endif %# + +controlPlane: + patches: + # Cluster configuration + - |- + cluster: + allowSchedulingOnControlPlanes: true + controllerManager: + extraArgs: + bind-address: 0.0.0.0 + coreDNS: + disabled: true + proxy: + disabled: true + scheduler: + extraArgs: + bind-address: 0.0.0.0 + + # ETCD configuration + - |- + cluster: + etcd: + extraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + advertisedSubnets: + - #{ bootstrap_node_network }# + + # Disable default API server admission plugins. + - |- + - op: remove + path: /cluster/apiServer/admissionControl + + # Enable K8s Talos API Access + - |- + machine: + features: + kubernetesTalosAPIAccess: + enabled: true + allowedRoles: + - os:admin + allowedKubernetesNamespaces: + - system-upgrade + #% if bootstrap_user_patches %# + # User specified controlPlane patches + - "@./patches/controlPlane.yaml" + #% endif %# +#% if ((bootstrap_user_patches) and (bootstrap_node_inventory | selectattr('controller', 'equalto', False) | list | length)) %# +worker: + patches: + # User specified worker patches + - "@./patches/worker.yaml" +#% endif %# diff --git a/bootstrap/templates/kubernetes/flux/apps.yaml.j2 b/bootstrap/templates/kubernetes/flux/apps.yaml.j2 new file mode 100644 index 00000000..6d260916 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/apps.yaml.j2 @@ -0,0 +1,57 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cluster-apps + namespace: flux-system +spec: + interval: 30m + path: ./kubernetes/apps + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + decryption: + provider: sops + secretRef: + name: sops-age + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-settings + - kind: Secret + name: cluster-secrets + - kind: ConfigMap + name: cluster-settings-user + optional: true + - kind: Secret + name: cluster-secrets-user + optional: true + patches: + - patch: |- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: not-used + spec: + decryption: + provider: sops + secretRef: + name: sops-age + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-settings + - kind: Secret + name: cluster-secrets + - kind: ConfigMap + name: cluster-settings-user + optional: true + - kind: Secret + name: cluster-secrets-user + optional: true + target: + group: kustomize.toolkit.fluxcd.io + kind: Kustomization + labelSelector: substitution.flux.home.arpa/disabled notin (true) diff --git a/bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2 b/bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2 new file mode 100644 index 00000000..06057f4f --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/config/cluster.yaml.j2 @@ -0,0 +1,46 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/gitrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: home-kubernetes + namespace: flux-system +spec: + interval: 30m + url: "#{ bootstrap_github_address }#" + #% if bootstrap_github_private_key %# + secretRef: + name: github-deploy-key + #% endif %# + ref: + branch: "#{ bootstrap_github_branch|default('main', true) }#" + ignore: | + # exclude all + /* + # include kubernetes directory + !/kubernetes +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cluster + namespace: flux-system +spec: + interval: 30m + path: ./kubernetes/flux + prune: true + wait: false + sourceRef: + kind: GitRepository + name: home-kubernetes + decryption: + provider: sops + secretRef: + name: sops-age + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-settings + - kind: Secret + name: cluster-secrets diff --git a/bootstrap/templates/kubernetes/flux/config/flux.yaml.j2 b/bootstrap/templates/kubernetes/flux/config/flux.yaml.j2 new file mode 100644 index 00000000..fb1f3f7f --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/config/flux.yaml.j2 @@ -0,0 +1,88 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/ocirepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: flux-manifests + namespace: flux-system +spec: + interval: 10m + url: oci://ghcr.io/fluxcd/flux-manifests + ref: + tag: v2.3.0 +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: flux + namespace: flux-system +spec: + interval: 10m + path: ./ + prune: true + wait: true + sourceRef: + kind: OCIRepository + name: flux-manifests + patches: + # Remove the network policies + - patch: | + $patch: delete + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: not-used + target: + group: networking.k8s.io + kind: NetworkPolicy + # Increase the number of reconciliations that can be performed in parallel and bump the resources limits + # https://fluxcd.io/flux/cheatsheets/bootstrap/#increase-the-number-of-workers + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --concurrent=8 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --kube-api-qps=500 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --kube-api-burst=1000 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --requeue-dependency=5s + target: + kind: Deployment + name: (kustomize-controller|helm-controller|source-controller) + - patch: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: not-used + spec: + template: + spec: + containers: + - name: manager + resources: + limits: + cpu: 2000m + memory: 2Gi + target: + kind: Deployment + name: (kustomize-controller|helm-controller|source-controller) + # Enable Helm near OOM detection + # https://fluxcd.io/flux/cheatsheets/bootstrap/#enable-helm-near-oom-detection + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --feature-gates=OOMWatch=true + - op: add + path: /spec/template/spec/containers/0/args/- + value: --oom-watch-memory-threshold=95 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --oom-watch-interval=500ms + target: + kind: Deployment + name: helm-controller diff --git a/bootstrap/templates/kubernetes/flux/config/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/flux/config/kustomization.yaml.j2 new file mode 100644 index 00000000..2ff3c784 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/config/kustomization.yaml.j2 @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./flux.yaml + - ./cluster.yaml diff --git a/bootstrap/templates/kubernetes/flux/repositories/git/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/git/kustomization.yaml.j2 new file mode 100644 index 00000000..8fb7c142 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/git/kustomization.yaml.j2 @@ -0,0 +1,5 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: [] diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/bjw-s.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/bjw-s.yaml.j2 new file mode 100644 index 00000000..c32ccd8d --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/bjw-s.yaml.j2 @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: bjw-s + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/bjw-s/helm diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/cilium.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/cilium.yaml.j2 new file mode 100644 index 00000000..d6736ba4 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/cilium.yaml.j2 @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: cilium + namespace: flux-system +spec: + interval: 1h + url: https://helm.cilium.io diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/coredns.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/coredns.yaml.j2 new file mode 100644 index 00000000..bf97567c --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/coredns.yaml.j2 @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: coredns + namespace: flux-system +spec: + interval: 1h + url: https://coredns.github.io/helm diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/external-dns.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/external-dns.yaml.j2 new file mode 100644 index 00000000..725cf4dd --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/external-dns.yaml.j2 @@ -0,0 +1,12 @@ +#% if bootstrap_cloudflare.enabled %# +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: external-dns + namespace: flux-system +spec: + interval: 1h + url: https://kubernetes-sigs.github.io/external-dns +#% endif %# diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/ingress-nginx.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/ingress-nginx.yaml.j2 new file mode 100644 index 00000000..da3f6023 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/ingress-nginx.yaml.j2 @@ -0,0 +1,12 @@ +#% if bootstrap_cloudflare.enabled %# +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: ingress-nginx + namespace: flux-system +spec: + interval: 1h + url: https://kubernetes.github.io/ingress-nginx +#% endif %# diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/jetstack.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/jetstack.yaml.j2 new file mode 100644 index 00000000..654e0e58 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/jetstack.yaml.j2 @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: jetstack + namespace: flux-system +spec: + interval: 1h + url: https://charts.jetstack.io diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/k8s-gateway.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/k8s-gateway.yaml.j2 new file mode 100644 index 00000000..b043b89a --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/k8s-gateway.yaml.j2 @@ -0,0 +1,12 @@ +#% if bootstrap_cloudflare.enabled %# +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: k8s-gateway + namespace: flux-system +spec: + interval: 1h + url: https://ori-edge.github.io/k8s_gateway +#% endif %# diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/kustomization.yaml.j2 new file mode 100644 index 00000000..f4b825b5 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/kustomization.yaml.j2 @@ -0,0 +1,19 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./bjw-s.yaml + - ./cilium.yaml + - ./coredns.yaml + #% if bootstrap_cloudflare.enabled %# + - ./external-dns.yaml + - ./ingress-nginx.yaml + - ./k8s-gateway.yaml + #% endif %# + - ./jetstack.yaml + - ./metrics-server.yaml + - ./postfinance.yaml + - ./prometheus-community.yaml + - ./spegel.yaml + - ./stakater.yaml diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/metrics-server.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/metrics-server.yaml.j2 new file mode 100644 index 00000000..1d93ab19 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/metrics-server.yaml.j2 @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: metrics-server + namespace: flux-system +spec: + interval: 1h + url: https://kubernetes-sigs.github.io/metrics-server diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/postfinance.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/postfinance.yaml.j2 new file mode 100644 index 00000000..cd629ceb --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/postfinance.yaml.j2 @@ -0,0 +1,12 @@ +#% if bootstrap_distribution in ["talos"] %# +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: postfinance + namespace: flux-system +spec: + interval: 1h + url: https://postfinance.github.io/kubelet-csr-approver +#% endif %# diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/prometheus-community.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/prometheus-community.yaml.j2 new file mode 100644 index 00000000..8a127039 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/prometheus-community.yaml.j2 @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: prometheus-community + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/prometheus-community/charts diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/spegel.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/spegel.yaml.j2 new file mode 100644 index 00000000..6ccbe8f6 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/spegel.yaml.j2 @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: spegel + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/spegel-org/helm-charts diff --git a/bootstrap/templates/kubernetes/flux/repositories/helm/stakater.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/helm/stakater.yaml.j2 new file mode 100644 index 00000000..017f4ca7 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/helm/stakater.yaml.j2 @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: stakater + namespace: flux-system +spec: + interval: 1h + url: https://stakater.github.io/stakater-charts diff --git a/bootstrap/templates/kubernetes/flux/repositories/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/kustomization.yaml.j2 new file mode 100644 index 00000000..ae7e0ad4 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/kustomization.yaml.j2 @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./git + - ./helm + - ./oci diff --git a/bootstrap/templates/kubernetes/flux/repositories/oci/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/flux/repositories/oci/kustomization.yaml.j2 new file mode 100644 index 00000000..8fb7c142 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/repositories/oci/kustomization.yaml.j2 @@ -0,0 +1,5 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: [] diff --git a/bootstrap/templates/kubernetes/flux/vars/cluster-secrets.sops.yaml.j2 b/bootstrap/templates/kubernetes/flux/vars/cluster-secrets.sops.yaml.j2 new file mode 100644 index 00000000..bdcb9f57 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/vars/cluster-secrets.sops.yaml.j2 @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: cluster-secrets + namespace: flux-system +stringData: + #% if bootstrap_cloudflare.enabled %# + SECRET_DOMAIN: "#{ bootstrap_cloudflare.domain }#" + SECRET_ACME_EMAIL: "#{ bootstrap_cloudflare.acme.email }#" + SECRET_CLOUDFLARE_TUNNEL_ID: "#{ bootstrap_cloudflare.tunnel.id }#" + #% endif %# diff --git a/bootstrap/templates/kubernetes/flux/vars/cluster-settings.yaml.j2 b/bootstrap/templates/kubernetes/flux/vars/cluster-settings.yaml.j2 new file mode 100644 index 00000000..f176c7f5 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/vars/cluster-settings.yaml.j2 @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cluster-settings + namespace: flux-system +data: + TIMEZONE: "#{ bootstrap_timezone }#" + CLUSTER_CIDR: "#{ bootstrap_pod_network.split(',')[0] }#" + NODE_CIDR: "#{ bootstrap_node_network }#" + #% if bootstrap_feature_gates.dual_stack_ipv4_first %# + CLUSTER_CIDR_V6: "#{ bootstrap_pod_network.split(',')[1] }#" + #% endif %# + #% if bootstrap_bgp.enabled %# + BGP_ADVERTISED_CIDR: "#{ bootstrap_bgp.advertised_network }#" + #% endif %# diff --git a/bootstrap/templates/kubernetes/flux/vars/kustomization.yaml.j2 b/bootstrap/templates/kubernetes/flux/vars/kustomization.yaml.j2 new file mode 100644 index 00000000..9ea91972 --- /dev/null +++ b/bootstrap/templates/kubernetes/flux/vars/kustomization.yaml.j2 @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./cluster-settings.yaml + - ./cluster-secrets.sops.yaml diff --git a/config.sample.yaml b/config.sample.yaml new file mode 100644 index 00000000..ec3b4c7d --- /dev/null +++ b/config.sample.yaml @@ -0,0 +1,207 @@ +--- + +# +# 1. (Required) Cluster details - Cluster represents the Kubernetes cluster layer and any additional customizations +# + +# (Required) Timezone is your IANA formatted timezone (e.g. America/New_York) +bootstrap_timezone: "" + +# (Required) Distribution can either be k3s or talos +bootstrap_distribution: k3s + +# (Required: Talos) Talos Specific Options +bootstrap_talos: + # (Required: Talos) If you need any additional System Extensions, and/or add kernel arguments generate a schematic ID. + # Go to https://factory.talos.dev/ and choose the System Extensions, and/or add kernel arguments. + schematic_id: "" + # (Optional: Talos) Add vlan tag to network master device + # See: https://www.talos.dev/latest/advanced/advanced-networking/#vlans + vlan: "" + # (Optional: Talos) Secureboot and TPM-based disk encryption + secureboot: + # (Optional) Enable secureboot on UEFI systems. Not supported on x86 platforms in BIOS mode. + # See: https://www.talos.dev/latest/talos-guides/install/bare-metal-platforms/secureboot + enabled: false + # (Optional) Enable TPM-based disk encryption. Requires TPM 2.0 + # See: https://www.talos.dev/v1.6/talos-guides/install/bare-metal-platforms/secureboot/#disk-encryption-with-tpm + encrypt_disk_with_tpm: false + # (Optional) Add includes for user provided patches to generated talconfig.yaml. + # See: https://github.com/budimanjojo/talhelper/blob/179ba9ed42f70069c7842109bea24f769f7af6eb/example/extraKernelArgs-patch.yaml + # Patches are applied in this order. (global overrides cp/worker which overrides node-specific). + # Create these files to allow talos:bootstrap-genconfig to complete (empty files are ok). + # kubernetes/bootstrap/talos/patches/node_.yaml # Patches for individual nodes + # kubernetes/bootstrap/talos/patches/controlPlane.yaml # Patches for controlplane nodes + # kubernetes/bootstrap/talos/patches/worker.yaml # Patches for worker nodes + # kubernetes/bootstrap/talos/patches/global.yaml # Patches for ALL nodes + user_patches: false + +# (Required) The CIDR your nodes are on (e.g. 192.168.1.0/24) +bootstrap_node_network: "" + +# (Optional) The default gateway for the nodes +# Default is .1 derrived from bootstrap_node_network: 'x.x.x.1' +bootstrap_node_default_gateway: "" + +# (Required) Use only 1, 3 or more ODD number of controller nodes, recommended is 3 +# Worker nodes are optional +bootstrap_node_inventory: [] + # - name: "" # Name of the node (must match [a-z0-9-\.]+) + # address: "" # IP address of the node + # controller: true # (Required) Set to true if this is a controller node + # ssh_user: "" # (Required: k3s) SSH username of the node + # talos_disk: "" # (Required: Talos) Device path or serial number of the disk for this node + # ... + +# (Optional) The DNS server to use for the cluster, this can be an existing +# local DNS server or a public one. +# Default is ["1.1.1.1", "1.0.0.1"] +# If using a local DNS server make sure it meets the following requirements: +# 1. your nodes can reach it +# 2. it is configured to forward requests to a public DNS server +# 3. you are not force redirecting DNS requests to it - this will break cert generation over DNS01 +# If using multiple DNS servers make sure they are setup the same way, there is no +# guarantee that the first DNS server will always be used for every lookup. +bootstrap_dns_servers: [] + +# (Optional) The DNS search domain to use for the nodes. +# Default is "." +# Use the default or leave empty to avoid possible DNS issues inside the cluster. +bootstrap_search_domain: "" + +# (Required) The pod CIDR for the cluster, this must NOT overlap with any +# existing networks and is usually a /16 (64K IPs). +# If you want to use IPv6 check the advanced flags below +bootstrap_pod_network: "10.69.0.0/16" + +# (Required) The service CIDR for the cluster, this must NOT overlap with any +# existing networks and is usually a /16 (64K IPs). +# If you want to use IPv6 check the advanced flags below +bootstrap_service_network: "10.96.0.0/16" + +# (Required) The IP address of the Kube API, choose an available IP in +# your nodes host network that is NOT being used. This is announced over L2. +# For k3s kube-vip is used, built-in functionality is used with Talos +bootstrap_controllers_vip: "" + +# (Optional) Add additional SANs to the Kube API cert, this is useful +# if you want to call the Kube API by hostname rather than IP +bootstrap_tls_sans: [] + +# (Required) Age Public Key (e.g. age1...) +# 1. Generate a new key with the following command: +# > task sops:age-keygen +# 2. Copy the public key and paste it below +bootstrap_sops_age_pubkey: "" + +# (Optional) Use cilium BGP control plane when L2 announcements won't traverse VLAN network segments. +# Needs a BGP capable router setup with the node IPs as peers. +# See: https://docs.cilium.io/en/latest/network/bgp-control-plane/ +bootstrap_bgp: + enabled: false + # (Optional) If using multiple BGP peers add them here. + # Default is .1 derrived from host_network: ['x.x.x.1'] + peers: [] + # (Required) Set the BGP Autonomous System Number for the router(s) and nodes. + # If these match, iBGP will be used. If not, eBGP will be used. + peer_asn: "" # Router(s) AS + local_asn: "" # Node(s) AS + # (Required) The advertised CIDR for the cluster, this must NOT overlap with any + # existing networks and is usually a /16 (64K IPs). + # If you want to use IPv6 check the advanced flags below + advertised_network: "" + +# +# 2. (Required) Flux details - Flux is used to manage the cluster configuration. +# + +# (Required) GitHub repository URL (for private repos use the ssh:// URL) +bootstrap_github_address: "" + +# (Required) GitHub repository branch +bootstrap_github_branch: "main" + +# (Required) Token for GitHub push-based sync +# 1. Generate a new token with the following command: +# > openssl rand -hex 16 +# 2. Copy the token and paste it below +bootstrap_github_webhook_token: "" + +# (Optional) Private key for Flux to access the GitHub repository +# 1. Generate a new key with the following command: +# > ssh-keygen -t ecdsa -b 521 -C "github-deploy-key" -f github-deploy.key -q -P "" +# 2. Make sure to paste public key from "github-deploy.key.pub" into +# the deploy keys section of your repository settings. +# 3. Uncomment and paste the private key below +# 4. Optionally set your repository on GitHub to private +# bootstrap_github_private_key: | +# -----BEGIN OPENSSH PRIVATE KEY----- +# ... +# -----END OPENSSH PRIVATE KEY----- + +# +# 3. (Optional) Cloudflare details - Cloudflare is used for DNS, TLS certificates and tunneling. +# + +bootstrap_cloudflare: + # (Required) Disable to use a different DNS provider + enabled: false + # (Required) Cloudflare Domain + domain: "" + # (Required) Cloudflare API Token (NOT API Key) + # 1. Head over to Cloudflare and create a API Token by going to + # https://dash.cloudflare.com/profile/api-tokens + # 2. Under the `API Tokens` section click the blue `Create Token` button. + # 3. Click the blue `Use template` button for the `Edit zone DNS` template. + # 4. Name your token something like `home-kubernetes` + # 5. Under `Permissions`, click `+ Add More` and add each permission below: + # `Zone - DNS - Edit` + # `Account - Cloudflare Tunnel - Read` + # 6. Limit the permissions to a specific account and zone resources. + # 7. Click the blue `Continue to Summary` button and then the blue `Create Token` button. + # 8. Copy the token and paste it below. + token: "" + # (Required) Optionals for Cloudflare Acme + acme: + # (Required) Any email you want to be associated with the ACME account (used for TLS certs via letsencrypt.org) + email: "" + # (Required) Use the ACME production server when requesting the wildcard certificate. + # By default the ACME staging server is used. This is to prevent being rate-limited. + # Update this option to `true` when you have verified the staging certificate + # works and then re-run `task configure` and push your changes to Github. + production: false + # (Required) Provide LAN access to the cluster ingresses for internal ingress classes + # The Load balancer IP for internal ingress, choose an available IP + # in your nodes host network that is NOT being used. This is announced over L2. + ingress_vip: "" + # (Required) Gateway is used for providing DNS to your cluster on LAN + # The Load balancer IP for k8s_gateway, choose an available IP + # in your nodes host network that is NOT being used. This is announced over L2. + gateway_vip: "" + # (Required) Options for Cloudflare Tunnel + # 1. Authenticate cloudflared to your domain + # > cloudflared tunnel login + # 2. Create the tunnel + # > cloudflared tunnel create k8s + # 3. Copy the AccountTag, TunnelID, and TunnelSecret from the tunnel configuration file and paste them below + tunnel: + # (Required) Cloudflare Account ID (cat ~/.cloudflared/*.json | jq -r .AccountTag) + account_id: "" + # (Required) Cloudflared Tunnel ID (cat ~/.cloudflared/*.json | jq -r .TunnelID) + id: "" + # (Required) Cloudflared Tunnel Secret (cat ~/.cloudflared/*.json | jq -r .TunnelSecret) + secret: "" + # (Required) Provide WAN access to the cluster ingresses for external ingress classes + # The Load balancer IP for external ingress, choose an available IP + # in your nodes host network that is NOT being used. This is announced over L2. + ingress_vip: "" + +# (Optional) Feature gates are used to enable experimental features +# bootstrap_feature_gates: +# # Enable Dual Stack IPv4 first +# # IMPORTANT: I am looking for people to help maintain IPv6 support since I cannot test it. +# # Ref: https://github.com/onedr0p/cluster-template/issues/1148 +# # Keep in mind that Cilium does not currently support IPv6 L2 announcements. +# # Make sure you set cluster.pod_cidr and cluster.service_cidr +# # to a valid dual stack CIDRs, e.g. "10.42.0.0/16,fd00:10:244::/64" +# dual_stack_ipv4_first: false diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..05162e5dbd2db0ac4a1f070af49defb611d11e9b GIT binary patch literal 18691 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¬mXA-Mll3lH%^D4C2(3rjJq#EZEug8e=72I(+&k~!~9~zwJ2YFl^^girP z2nA%bw9Rx0-ABF|T_73@{6wac<~A@<<`gfipw6yR-^5O(k2fomGOW2)m4ZO&hwl)Z zX3FP8PKSw+VV?4_61G>FSFc!|NkPXv#-2zN^zSI_`Dy_F0NJea%4O8faTk3cbj9|W zs=P#h>o7XRD^j;@TtwYO)-DEPd_>Mn%FaJ>t+!p}rg?&^2(8@BX5zhme{fMj8$$!T z4;VJ6;iSJMS7d4}h5CfA2zg-CiPnkc_RK)G{6UtecOcFL>xzylGq%rJUYpiu!=AB0 z${tyVBkKxulO0d?36M*NMdn+ru`3+Pw>Q$|*Ep|eopM0re9B5|aN*F(^_x3%=@Pf| z8qPBk+~yb+)T|WWHQE*2fuF?Ddye`|(|3Jw1Dg1ajaDO*XNY3){zkW7sa3`G={vZU zZtU$SzH`?VD<6(0I=Yp~iLj<4;PJg#mJFbHWvGi3?`>!>QS8wIne4FSsReX1`n+q#U7n{2N! z&^x4ohSTJMPpmraqN~3baiCM@NsLRpPng|7_fO7affhW0J>_nKo_?Y+S5ZHkC zV~7a_Y!|p1ay>8gDo~+);FuY{YR+a37cFEA!>qwCFQh7{aP*pK!vp7<)ZGJo)x>ZG z@hA##JtLv}{pIGr#A@_e2GbhmA!BtbdSiE-GP6#7BmJ6``1!3H0A{3%ES_u}}hoqq3iY^I)g9I?iOz^T&lp&S_GNW;qRd#?52gLB84H@why1VUj zmF?PGQACS6%uf(8!RT0$)E-$|0i{}2*j3*W1r(w{heKsj>H(f?!4RTzBLwJWn%Tq2S^NG(@p>&fxM0pt-J> zQQp}q50jV)@{w2OWQ1R;fx`rF!G?O=a+m_>bWzL(O|7)~E)a`VBImyG9ipwGu7+%! z9Rfvl7=D#1+1cc&eB2F_$f=EvD5JU7BOhBQrXoPziWDBCZc^|7aNTZP#nU0=sar-V z&o_wHyauJ7l^Q$>0*l%_SOHd!Z!ve-LN$FQ&6ANja})6^fIZgwW-c%lR@t@8R4N?N z(dU_+TwZ$%<%@HJAB&An(6T}4e6Xe4W~?3H`%)=U)xMCN^`LwgmSg9XVe8CpQJ{rgh}16Zk3{Fl z&1=UII|jmvb@32Zx`khYC@IQP0PEs0DB4_h7n&gTjX6QH=5&7I9``TKI08Pg>k?oQ zeJ%#Vj*!2ZOJI74`(44bCt$^Pj)MK*b-Cd-ioJ{ZfQm|Or|wkf*C0p=+PhRqwOnMD zT!3`UqBLRZl?p)UlC4sC)JWvBiRC>bb~C7s+Ua8kK(+Rr{{VSj--+M%mN-g?rGnrs z6j(Emjtcl1tEKL6mkUf_m(+K!LX#aSYKJiN|hbdNMk)}Q!TBHniCOw zin5DHp_5Rh6%l%v8p1VosD#W8g1zFo!Y-)VU!0g{Mr=$VEufb$iy+-tB{fwV+Ny|M z7=aN;fwYRsK&GjcIOH$WcJ<);u*8Yvh61GP*3_qAQPVkJc)I*L&;?6tJu$vW zNqL2*m1<)r6w&sW(`x|mRtZiVqX6z=C$u+lpA|3RN}wl|hYj%I73)2Gftc-;x)0_R06A-z4%4j=`H7-tt#95X z$>O<1c%)L@}Uzl#G{x?4V01wo>racxuAoM;m{vzfH>V2UfetshNecdJS zQ;YqO`aT!)71Qee;mog*-!jHKxWgIK0D=2N_1aZm5sCol&%{Bl^da_+SUuDbHjYvG zmdje|{{S%bZ*={zNqqqRWe~nCan_D{--$^z_kR)E^B<-cpYDH%=9{4SsFKip&&0)S zJs8zUhj?c1-Z~XK#g!gy3}h*gLcsu zZRf;Lwg*Nz<`XEN}OgjHHl}7i@s8vpA!w(iRmwshgK(~ZY-;X zO+?wm-v@{>VJUJ%3I^cYDj+zhCTlT)BTyV6aU2r9eI*A!31W)9*EI}GZXnG{)dB4T z#^*q&^lDYYdP+QEBK0b^5|gXOAd_52BJNQTDU~)$h)q!vG{x;u>SpRxFn^IDNl_TY zUO+S2CbVCO-$C@ePIMSTVULBwcVnuENT(3o7tYkP`m zl-2LFGj>-=yJMTYtO|zS@oFtItT?VujmrDL4a){}h;%X7vOY-jS{$RNjF=z7wTSVYb%ne)z3jDx|)w(a{6wmYXHB!6S zpiX0fNAve88Ai7hziCgqKdL)z&ZYQ)2ESUp*dmq#vjePEEfwz{ zxZsJ(#rjKVDegh8zEBs8fX^TH6yrB4Rem9MP`H8K;Mp30HFipZs0gm6U>7ZTn6#{M z4)c`Emj%i?ID*#6vv)1&6lY`|>Y*m`lIxGWUnZdTir9+YBCe_yFls9Df3oOVxu;u{ z;=X1k1H^R_s%DmZMz{}&pBbJvV*1;7sg}Cv3P?T zi@_!gk!FZ)NYjjQDJzCri^agsIF*ZK*2tG)YF1;aV!V+O)@u9Ljlon)$P}@{WHv)>Pm2XHpI&M&Z zumCy4K&s+6Cl)})U|TDsZP|%QLgU0wTZ`C$by3NguzJH{C;XX8#8YEZb9-8jIH$#;ti0xE%=JJnk~N&Oe^^55wLiFTvuu@m+c%Z z{y2%Z9ZcdvbU~F}dj9~Z{nyzanY9lBeq*|wgFf*<$J{@do=f@eCZubj{$gAgVoPhl zfgBHU{-vg?jPK$uueN@r{*XUhM(zC%#7Ts5HvC4WgZSwez;{>iEINJ2xqv&L+$VL& z{lx+=U-dIxe(UdX<{J9H+^;UKsQktm7u_fu-cc=-FNZ%60vI37L{fz6kHl8R{b%AO zpdH_+*|%?B>I+uw{>ZzkT?gs`j{rZ2ZTLo4N3?iC&LW2ty&@AOK%h!%h`KcdVdvf; z^DNmax}|}6-LM3#H6Gn&0*W}PavWC?=gmQzC0OIU4-BqvnS#8`jFNzJ3veNoP$)Zk zK$`#~za#fEnT(uhm0>&&v)Dl}DFsI`?bt3h`Lyk>QX zsNXCSzMGbeKZupZeqpl_3W3@s;e(tp$l0h0-C9Pxejs(0NO1eg^k81L#Bp#{V{yyU z1sa)Q+%+7ShlzghGm{ZNt_F8k9$;A4%pHEvI`b1Pu?qQymIGB=iIVw+0(63}o9z}m z#|+-$VCMayX8oiJuf5Ae(U) zWlC;T6mrc&a6ChPV%<=~orTNhnp%MiO+@jxNIBhJpbfVYG;T@I9T(_96 z301sBqm)+IR0@te@f>e4>G-l=L>L;Rr6Q|Fp@;bg7+~#}>j$qA;coB~dY4Z328x2q z=H-mMkrjw5S8+#CRNHW++c+bbvF+9ny<97(vu(?r#~%=2uxB^sHcpcbGp*dzt#^%~ zRu>*3#0DFm?m|>pt4G|%o{6GNZ;x#=WZ#k|Bd&ENi`p3Q|-Ct;jI43^)sdibQ z#LLY_x12zA&uLTrh+tQO31f`RG~>hoi_@84HCHKqBUg}tM(Pxm(xoVp7hUg_hS*7?wN4Qn+9jHQqI9<=QXTvCnuv@S4g|nw4-hM6A>ryNkx*jk%5# zeWKT-$rX8ru{D>Np?Rn#D?}QKa7MWM%sI?#^d;7_2FUN4@h-xctKG)< zmi3p?Dbp3TnHYhh(fS;E)UvB1L(N(rU4*L%D7mjUCuR=2+@p;6YlA4DJoy zyTRSmw6YFzok5_^qSM61&ZiJ~mpP63+` 0 + for: 15m + labels: + severity: critical + annotations: + description: + "Depending on the rate limit, cert-manager may be unable to generate + certificates for up to a week." + runbook_url: https://gitlab.com/uneeq-oss/cert-manager-mixin/-/blob/master/RUNBOOK.md#certmanagerhittingratelimits + summary: "Cert manager hitting LetsEncrypt rate limits." diff --git a/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml b/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml new file mode 100644 index 00000000..1cf7148a --- /dev/null +++ b/kubernetes/apps/cert-manager/cert-manager/issuers/issuers.yaml @@ -0,0 +1,39 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: "${SECRET_ACME_EMAIL}" + privateKeySecretRef: + name: letsencrypt-production + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cert-manager-secret + key: api-token + selector: + dnsZones: + - "${SECRET_DOMAIN}" +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: "${SECRET_ACME_EMAIL}" + privateKeySecretRef: + name: letsencrypt-staging + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cert-manager-secret + key: api-token + selector: + dnsZones: + - "${SECRET_DOMAIN}" diff --git a/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml b/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml new file mode 100644 index 00000000..fd43d965 --- /dev/null +++ b/kubernetes/apps/cert-manager/cert-manager/issuers/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./secret.sops.yaml + - ./issuers.yaml diff --git a/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml b/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml new file mode 100644 index 00000000..eb1a98c9 --- /dev/null +++ b/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: cert-manager-secret +stringData: + api-token: ENC[AES256_GCM,data:V/OeW+bpuNGXDAiNZ2WmawliZ8JakYzZvSqNhuLRCif3e1nXDXXL+Q==,iv:yq3rE8ZsK2ih6FMNtFRvak7xNNTTB/VCz0+Mp8CiJ5M=,tag:2eY19fzMjg99TAlbC44ntw==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnSm52UU95ZVJMUE52cjc3 + THplYTFpbFd4ZDJSV2RIaDNLWVRxZFd6TEVzCi9OSmIvYUhvVUhHQldoalJzMEpX + dXNPV3AreUowSHBBY1NYUlh5b24wZDAKLS0tIHVwVkViazFmVmhqQjBqNkJiVlN4 + VGFML3ZMZzk4WlF3NjJ6SXpobzJPMlEKheLxsJRKPxsPwGOKZ8kb5viGJ07RT9eq + id87ugUEST/+c5l0YE4Q5DDRpikoiT3uoDS7X+PfIGHgQWiQUq4uNQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-02-17T21:45:25Z" + mac: ENC[AES256_GCM,data:bKwwkxj+C5/dPsKsiFi599+d31RpAbcQQ5HugHBNIANGT0nwmYx9Cj8gDGcAeY4OBs9fWzZ2uHVW9ZbgrzyOdsSH2VdurPvOruJZ2kuWZ1BYZm1pbsFXRWhuWxaaJLTK9mP4YlOEQ76uYVMaaXORS7Pt4AHmliDReOyGJF4X+lI=,iv:SVsKQ7xOkLcODOWe7A/IFoQpIMB4Cbb7p8P4st3lZjo=,tag:Wq6M1In3czCDwUXsFBHMGQ==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.7.3 diff --git a/kubernetes/apps/cert-manager/cert-manager/ks.yaml b/kubernetes/apps/cert-manager/cert-manager/ks.yaml new file mode 100644 index 00000000..6d180255 --- /dev/null +++ b/kubernetes/apps/cert-manager/cert-manager/ks.yaml @@ -0,0 +1,44 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cert-manager + namespace: flux-system +spec: + targetNamespace: cert-manager + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/cert-manager/cert-manager/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cert-manager-issuers + namespace: flux-system +spec: + targetNamespace: cert-manager + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: cert-manager + path: ./kubernetes/apps/cert-manager/cert-manager/issuers + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/cert-manager/kustomization.yaml b/kubernetes/apps/cert-manager/kustomization.yaml new file mode 100644 index 00000000..39593fb7 --- /dev/null +++ b/kubernetes/apps/cert-manager/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./cert-manager/ks.yaml diff --git a/kubernetes/apps/cert-manager/namespace.yaml b/kubernetes/apps/cert-manager/namespace.yaml new file mode 100644 index 00000000..ed788350 --- /dev/null +++ b/kubernetes/apps/cert-manager/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/database/alert.yaml b/kubernetes/apps/database/alert.yaml new file mode 100644 index 00000000..f14a6377 --- /dev/null +++ b/kubernetes/apps/database/alert.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: database +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: database +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: HelmRelease + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/database/cloudnative-pg/app/externalsecret.yaml b/kubernetes/apps/database/cloudnative-pg/app/externalsecret.yaml new file mode 100644 index 00000000..4aac2e27 --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/app/externalsecret.yaml @@ -0,0 +1,34 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: cloudnative-pg +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: cloudnative-pg-secret + template: + engineVersion: v2 + metadata: + labels: + cnpg.io/reload: "true" + data: + - secretKey: username + remoteRef: + key: cloudnative-pg + property: POSTGRES_SUPER_USER + - secretKey: password + remoteRef: + key: cloudnative-pg + property: POSTGRES_SUPER_PASS + - secretKey: aws-access-key-id + remoteRef: + key: minio + property: MINIO_ROOT_USER + - secretKey: aws-secret-access-key + remoteRef: + key: minio + property: MINIO_ROOT_PASSWORD diff --git a/kubernetes/apps/database/cloudnative-pg/app/helmrelease.yaml b/kubernetes/apps/database/cloudnative-pg/app/helmrelease.yaml new file mode 100644 index 00000000..57730973 --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/app/helmrelease.yaml @@ -0,0 +1,36 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cloudnative-pg +spec: + interval: 30m + chart: + spec: + chart: cloudnative-pg + version: 0.21.2 + sourceRef: + kind: HelmRepository + name: cloudnative-pg + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + dependsOn: + - name: local-path-provisioner + namespace: storage + values: + crds: + create: true + config: + data: + INHERITED_ANNOTATIONS: kyverno.io/ignore + monitoring: + podMonitorEnabled: false + grafanaDashboard: + create: true diff --git a/kubernetes/apps/database/cloudnative-pg/app/kustomization.yaml b/kubernetes/apps/database/cloudnative-pg/app/kustomization.yaml new file mode 100644 index 00000000..c59808bf --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/app/kustomization.yaml @@ -0,0 +1,19 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml + - ./prometheusrule.yaml +configMapGenerator: + - name: cloudnative-pg-dashboard + files: + - cloudnative-pg-dashboard.json=https://raw.githubusercontent.com/cloudnative-pg/grafana-dashboards/main/charts/cluster/grafana-dashboard.json +generatorOptions: + disableNameSuffixHash: true + annotations: + kustomize.toolkit.fluxcd.io/substitute: disabled + labels: + grafana_dashboard: "true" diff --git a/kubernetes/apps/database/cloudnative-pg/app/prometheusrule.yaml b/kubernetes/apps/database/cloudnative-pg/app/prometheusrule.yaml new file mode 100644 index 00000000..9c1d6a8d --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/app/prometheusrule.yaml @@ -0,0 +1,67 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/prometheusrule_v1.json +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: cloudnative-pg-rules + labels: + prometheus: k8s + role: alert-rules +spec: + groups: + - name: cloudnative-pg.rules + rules: + - alert: LongRunningTransaction + annotations: + description: Pod {{ $labels.pod }} is taking more than 5 minutes (300 seconds) for a query. + summary: A query is taking longer than 5 minutes. + expr: |- + cnpg_backends_max_tx_duration_seconds > 300 + for: 1m + labels: + severity: warning + - alert: BackendsWaiting + annotations: + description: Pod {{ $labels.pod }} has been waiting for longer than 5 minutes + summary: If a backend is waiting for longer than 5 minutes + expr: |- + cnpg_backends_waiting_total > 300 + for: 1m + labels: + severity: warning + - alert: PGDatabase + annotations: + description: Over 150,000,000 transactions from frozen xid on pod {{ $labels.pod }} + summary: Number of transactions from the frozen XID to the current one + expr: |- + cnpg_pg_database_xid_age > 150000000 + for: 1m + labels: + severity: warning + - alert: PGReplication + annotations: + description: Standby is lagging behind by over 300 seconds (5 minutes) + summary: The standby is lagging behind the primary + expr: |- + cnpg_pg_replication_lag > 300 + for: 1m + labels: + severity: warning + - alert: LastFailedArchiveTime + annotations: + description: Archiving failed for {{ $labels.pod }} + summary: Checks the last time archiving failed. Will be < 0 when it has not failed. + expr: |- + (cnpg_pg_stat_archiver_last_failed_time - cnpg_pg_stat_archiver_last_archived_time) > 1 + for: 1m + labels: + severity: warning + - alert: DatabaseDeadlockConflicts + annotations: + description: There are over 10 deadlock conflicts in {{ $labels.pod }} + summary: Checks the number of database conflicts + expr: |- + cnpg_pg_stat_database_deadlocks > 10 + for: 1m + labels: + severity: warning diff --git a/kubernetes/apps/database/cloudnative-pg/cluster/cluster.yaml b/kubernetes/apps/database/cloudnative-pg/cluster/cluster.yaml new file mode 100644 index 00000000..67600607 --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/cluster/cluster.yaml @@ -0,0 +1,54 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/postgresql.cnpg.io/cluster_v1.json +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: postgres-cluster +spec: + instances: 3 + storage: + size: 9Gi + storageClass: local-hostpath + superuserSecret: + name: cloudnative-pg-secret + enableSuperuserAccess: true + nodeMaintenanceWindow: + inProgress: false + reusePVC: true + monitoring: + enablePodMonitor: true + # Ref: https://github.com/cloudnative-pg/cloudnative-pg/issues/2501 + podMonitorMetricRelabelings: + - { sourceLabels: [ "cluster" ], targetLabel: cnpg_cluster, action: replace } + - { regex: cluster, action: labeldrop } + backup: + retentionPolicy: 30d + barmanObjectStore: &barmanObjectStore + data: + compression: bzip2 + wal: + compression: bzip2 + maxParallel: 8 + destinationPath: s3://cloudnative-pg/ + endpointURL: http://${NAS_URL}:9000 + # Note: serverName version needs to be inclemented + # when recovering from an existing cnpg cluster + serverName: ¤tCluster postgres-v2 + s3Credentials: + accessKeyId: + name: cloudnative-pg-secret + key: aws-access-key-id + secretAccessKey: + name: cloudnative-pg-secret + key: aws-secret-access-key + # Note: previousCluster needs to be set to the name of the previous + # cluster when recovering from an existing cnpg cluster + bootstrap: + recovery: + source: &previousCluster postgres-v1 + # Note: externalClusters is needed when recovering from an existing cnpg cluster + externalClusters: + - name: *previousCluster + barmanObjectStore: + <<: *barmanObjectStore + serverName: *previousCluster diff --git a/kubernetes/apps/database/cloudnative-pg/cluster/gatus.yaml b/kubernetes/apps/database/cloudnative-pg/cluster/gatus.yaml new file mode 100644 index 00000000..f000099e --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/cluster/gatus.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-gatus-ep + labels: + gatus.io/enabled: "true" +data: + config.yaml: | + endpoints: + - name: postgres + group: infrastructure + url: tcp://postgres-cluster-rw.database.svc.cluster.local:5432 + interval: 1m + ui: + hide-url: true + hide-hostname: true + conditions: + - "[CONNECTED] == true" + alerts: + - type: discord diff --git a/kubernetes/apps/database/cloudnative-pg/cluster/kustomization.yaml b/kubernetes/apps/database/cloudnative-pg/cluster/kustomization.yaml new file mode 100644 index 00000000..f620a3c7 --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/cluster/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: default +resources: + - ./cluster.yaml + - ./gatus.yaml + - ./scheduledbackup.yaml diff --git a/kubernetes/apps/database/cloudnative-pg/cluster/scheduledbackup.yaml b/kubernetes/apps/database/cloudnative-pg/cluster/scheduledbackup.yaml new file mode 100644 index 00000000..e56347d8 --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/cluster/scheduledbackup.yaml @@ -0,0 +1,12 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/postgresql.cnpg.io/scheduledbackup_v1.json +apiVersion: postgresql.cnpg.io/v1 +kind: ScheduledBackup +metadata: + name: postgres +spec: + schedule: "@daily" + immediate: true + backupOwnerReference: self + cluster: + name: postgres-cluster diff --git a/kubernetes/apps/database/cloudnative-pg/ks.yaml b/kubernetes/apps/database/cloudnative-pg/ks.yaml new file mode 100644 index 00000000..be90bfb1 --- /dev/null +++ b/kubernetes/apps/database/cloudnative-pg/ks.yaml @@ -0,0 +1,46 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cloudnative-pg + namespace: flux-system +spec: + targetNamespace: database + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + path: ./kubernetes/apps/database/cloudnative-pg/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cloudnative-pg-cluster + namespace: flux-system +spec: + targetNamespace: database + commonMetadata: + labels: + app.kubernetes.io/name: cloudnative-pg + dependsOn: + - name: cloudnative-pg + path: ./kubernetes/apps/database/cloudnative-pg/cluster + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/database/dragonfly/app/helmrelease.yaml b/kubernetes/apps/database/dragonfly/app/helmrelease.yaml new file mode 100644 index 00000000..16d84483 --- /dev/null +++ b/kubernetes/apps/database/dragonfly/app/helmrelease.yaml @@ -0,0 +1,102 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app dragonfly-operator +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + values: + controllers: + dragonfly-operator: + strategy: RollingUpdate + containers: + app: + image: + repository: ghcr.io/dragonflydb/operator + tag: v1.1.2@sha256:f0d76725950095ac65b36252e0042d339d1db9b181b1d068f4b6686ea93055e4 + command: ["/manager"] + args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8080 + probes: + liveness: + enabled: true + custom: true + spec: + httpGet: + path: /healthz + port: &port 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: + enabled: true + custom: true + spec: + httpGet: + path: /readyz + port: *port + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + resources: + requests: + cpu: 10m + limits: + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + seccompProfile: { type: RuntimeDefault } + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: *app + service: + app: + controller: *app + ports: + http: + port: *port + metrics: + port: 8080 + serviceMonitor: + app: + serviceName: *app + endpoints: + - port: metrics + scheme: http + path: /metrics + interval: 1m + scrapeTimeout: 10s + serviceAccount: + create: true + name: *app diff --git a/kubernetes/apps/database/dragonfly/app/kustomization.yaml b/kubernetes/apps/database/dragonfly/app/kustomization.yaml new file mode 100644 index 00000000..639c55db --- /dev/null +++ b/kubernetes/apps/database/dragonfly/app/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + # renovate: datasource=github-releases depName=dragonflydb/dragonfly-operator + - https://raw.githubusercontent.com/dragonflydb/dragonfly-operator/v1.1.2/manifests/crd.yaml + - ./helmrelease.yaml + - ./rbac.yaml diff --git a/kubernetes/apps/database/dragonfly/app/rbac.yaml b/kubernetes/apps/database/dragonfly/app/rbac.yaml new file mode 100644 index 00000000..6e1e0920 --- /dev/null +++ b/kubernetes/apps/database/dragonfly/app/rbac.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dragonfly-operator +rules: + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["events"] + verbs: ["create", "patch"] + - apiGroups: [""] + resources: ["pods", "services"] + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] + - apiGroups: ["apps"] + resources: ["statefulsets"] + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] + - apiGroups: ["dragonflydb.io"] + resources: ["dragonflies"] + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] + - apiGroups: ["dragonflydb.io"] + resources: ["dragonflies/finalizers"] + verbs: ["update"] + - apiGroups: ["dragonflydb.io"] + resources: ["dragonflies/status"] + verbs: ["get", "patch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dragonfly-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dragonfly-operator +subjects: + - kind: ServiceAccount + name: dragonfly-operator + namespace: database diff --git a/kubernetes/apps/database/dragonfly/cluster/cluster.yaml b/kubernetes/apps/database/dragonfly/cluster/cluster.yaml new file mode 100644 index 00000000..97984b2b --- /dev/null +++ b/kubernetes/apps/database/dragonfly/cluster/cluster.yaml @@ -0,0 +1,25 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/dragonflydb.io/dragonfly_v1alpha1.json +apiVersion: dragonflydb.io/v1alpha1 +kind: Dragonfly +metadata: + name: dragonfly +spec: + image: ghcr.io/dragonflydb/dragonfly:v1.18.1 + replicas: 3 + env: + - name: MAX_MEMORY + valueFrom: + resourceFieldRef: + resource: limits.memory + divisor: 1Mi + args: + - --maxmemory=$(MAX_MEMORY)Mi + - --proactor_threads=2 + - --cluster_mode=emulated + - --lock_on_hashtags + resources: + requests: + cpu: 100m + limits: + memory: 512Mi diff --git a/kubernetes/apps/database/dragonfly/cluster/kustomization.yaml b/kubernetes/apps/database/dragonfly/cluster/kustomization.yaml new file mode 100644 index 00000000..6f0f305d --- /dev/null +++ b/kubernetes/apps/database/dragonfly/cluster/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./cluster.yaml + - ./podmonitor.yaml diff --git a/kubernetes/apps/database/dragonfly/cluster/podmonitor.yaml b/kubernetes/apps/database/dragonfly/cluster/podmonitor.yaml new file mode 100644 index 00000000..b26a770d --- /dev/null +++ b/kubernetes/apps/database/dragonfly/cluster/podmonitor.yaml @@ -0,0 +1,13 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/podmonitor_v1.json +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: dragonfly +spec: + selector: + matchLabels: + app: dragonfly + podTargetLabels: ["app"] + podMetricsEndpoints: + - port: admin diff --git a/kubernetes/apps/database/dragonfly/ks.yaml b/kubernetes/apps/database/dragonfly/ks.yaml new file mode 100644 index 00000000..90e97232 --- /dev/null +++ b/kubernetes/apps/database/dragonfly/ks.yaml @@ -0,0 +1,44 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app dragonfly + namespace: flux-system +spec: + targetNamespace: database + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/database/dragonfly/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app dragonfly-cluster + namespace: flux-system +spec: + targetNamespace: database + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: dragonfly + path: ./kubernetes/apps/database/dragonfly/cluster + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/database/kustomization.yaml b/kubernetes/apps/database/kustomization.yaml new file mode 100644 index 00000000..2f2d257a --- /dev/null +++ b/kubernetes/apps/database/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./cloudnative-pg/ks.yaml + - ./dragonfly/ks.yaml diff --git a/kubernetes/apps/database/namespace.yaml b/kubernetes/apps/database/namespace.yaml new file mode 100644 index 00000000..5cad2860 --- /dev/null +++ b/kubernetes/apps/database/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: database + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/default/alert.yaml b/kubernetes/apps/default/alert.yaml new file mode 100644 index 00000000..bf897bae --- /dev/null +++ b/kubernetes/apps/default/alert.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: default +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: default +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: HelmRelease + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/default/homepage/app/configmap.yaml b/kubernetes/apps/default/homepage/app/configmap.yaml new file mode 100644 index 00000000..71d59b32 --- /dev/null +++ b/kubernetes/apps/default/homepage/app/configmap.yaml @@ -0,0 +1,81 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: homepage-config + labels: + app.kubernetes.io/name: homepage +data: + bookmarks.yaml: | + - Communitcate: + - Discord: + - icon: discord.png + href: https://discord.com/app + - Media: + - YouTube: + - icon: youtube.png + href: https://youtube.com/feed/subscriptions + - Reading: + - Reddit: + - icon: reddit.png + href: https://reddit.com + - Git: + - kubesearch: + - icon: kubernetes-dashboard.png + href: https://kubesearch.dev + - flux-cluster-template: + - icon: github.png + href: https://github.com/onedr0p/flux-cluster-template + docker.yaml: "" + kubernetes.yaml: | + mode: cluster + services.yaml: | + - Network: + - Cloudflared: + href: https://dash.cloudflare.com + icon: cloudflare-zero-trust.png + description: Cloudflared Tunnel + widget: + type: cloudflared + accountid: "{{HOMEPAGE_VAR_CLOUDFLARED_ACCOUNTID}}" + tunnelid: "{{HOMEPAGE_VAR_CLOUDFLARED_TUNNELID}}" + key: "{{HOMEPAGE_VAR_CLOUDFLARED_API_TOKEN}}" + settings.yaml: | + title: Pohulanka home + theme: dark + color: slate + headerStyle: clean + layout: + Home: + style: column + icon: mdi-home-analytics + Network: + style: row + columns: 3 + icon: mdi-server + Observability: + style: column + icon: mdi-chart-line + providers: + longhorn: + url: http://longhorn-frontend.storage.svc.cluster.local + widgets.yaml: | + - resources: + backend: kubernetes + cpu: true + expanded: true + memory: true + - greeting: + text_size: xl + text: "Welcome!" + - datetime: + text_size: l + format: + dateStyle: long + timeStyle: short + hourCycle: h23 + - longhorn: + expanded: true + total: true + labels: true + nodes: true diff --git a/kubernetes/apps/default/homepage/app/externalsecret.yaml b/kubernetes/apps/default/homepage/app/externalsecret.yaml new file mode 100644 index 00000000..524e8b1c --- /dev/null +++ b/kubernetes/apps/default/homepage/app/externalsecret.yaml @@ -0,0 +1,69 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: homepage +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: homepage-secret + template: + engineVersion: v2 + data: + ## Non Cluster resources + HOMEPAGE_VAR_CLOUDFLARED_ACCOUNTID: "{{ .CLOUDFLARE_ACCOUNT_TAG }}" + HOMEPAGE_VAR_CLOUDFLARED_TUNNELID: "{{ .CLUSTER_CLOUDFLARE_TUNNEL_ID }}" + HOMEPAGE_VAR_CLOUDFLARED_API_TOKEN: "{{ .CLOUDFLARE_HOMEPAGE_TUNNEL_SECRET }}" + HOMEPAGE_VAR_SYNOLOGY_USERNAME: "{{ .HOMEPAGE_SYNOLOGY_USERNAME }}" + HOMEPAGE_VAR_SYNOLOGY_PASSWORD: "{{ .HOMEPAGE_SYNOLOGY_PASSWORD }}" + HOMEPAGE_VAR_PROXMOX_USERNAME: "{{ .HOMEPAGE_PROXMOX_USERNAME }}" + HOMEPAGE_VAR_PROXMOX_PASSWORD: "{{ .HOMEPAGE_PROXMOX_PASSWORD }}" + HOMEPAGE_VAR_PI_HOLE_TOKEN: "{{ .HOMEPAGE_PI_HOLE_TOKEN }}" + ## Default + HOMEPAGE_VAR_QBITTORRENT_USERNAME: "{{ .homepage_qbittorrent_username }}" + HOMEPAGE_VAR_QBITTORRENT_PASSWORD: "{{ .homepage_qbittorrent_password }}" + HOMEPAGE_VAR_RADARR_TOKEN: "{{ .RADARR_API_KEY }}" + HOMEPAGE_VAR_SONARR_TOKEN: "{{ .SONARR_API_KEY }}" + HOMEPAGE_VAR_PROWLARR_TOKEN: "{{ .PROWLARR_API_KEY }}" + HOMEPAGE_VAR_PLEX_TOKEN: "{{ .PLEX_TOKEN }}" + HOMEPAGE_VAR_OVERSEERR_TOKEN: "{{ .OVERSEERR_TOKEN }}" + ## Observability + HOMEPAGE_VAR_PORTAINER_TOKEN: "{{ .HOMEPAGE_PORTAINER_TOKEN }}" + HOMEPAGE_VAR_GRAFANA_USER: "{{ .grafana_username }}" + HOMEPAGE_VAR_GRAFANA_PASSWORD: "{{ .grafana_password }}" + dataFrom: + - extract: + key: cloudflare + - extract: + key: synology + - extract: + key: proxmox + - extract: + key: pihole + - extract: + key: qbittorrent + rewrite: + - regexp: + source: "(.*)" + target: "homepage_qbittorrent_$1" + - extract: + key: radarr + - extract: + key: sonarr + - extract: + key: prowlarr + - extract: + key: plex + - extract: + key: overseerr + - extract: + key: portainer + - extract: + key: grafana + rewrite: + - regexp: + source: "(.*)" + target: "grafana_$1" diff --git a/kubernetes/apps/default/homepage/app/helmrelease.yaml b/kubernetes/apps/default/homepage/app/helmrelease.yaml new file mode 100644 index 00000000..587cd222 --- /dev/null +++ b/kubernetes/apps/default/homepage/app/helmrelease.yaml @@ -0,0 +1,77 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: homepage +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + interval: 30m + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + strategy: uninstall + values: + controllers: + homepage: + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: ghcr.io/gethomepage/homepage + tag: v0.8.13 + env: + TZ: "${TIMEZONE}" + envFrom: + - secretRef: + name: homepage-secret + service: + app: + controller: homepage + ports: + http: + port: 3000 + ingress: + app: + className: internal + hosts: + - host: home.${SECRET_DOMAIN} + paths: + - path: / + pathType: Prefix + service: + identifier: app + port: http + persistence: + config: + type: configMap + name: homepage-config + globalMounts: + - subPath: bookmarks.yaml + path: /app/config/bookmarks.yaml + - subPath: docker.yaml + path: /app/config/docker.yaml + - subPath: kubernetes.yaml + path: /app/config/kubernetes.yaml + - subPath: services.yaml + path: /app/config/services.yaml + - subPath: settings.yaml + path: /app/config/settings.yaml + - subPath: widgets.yaml + path: /app/config/widgets.yaml + serviceAccount: + create: true diff --git a/kubernetes/apps/default/homepage/app/kustomization.yaml b/kubernetes/apps/default/homepage/app/kustomization.yaml new file mode 100644 index 00000000..a7e30d64 --- /dev/null +++ b/kubernetes/apps/default/homepage/app/kustomization.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./configmap.yaml + - ./helmrelease.yaml + - ./rbac.yaml + - ../../../../templates/gatus/internal diff --git a/kubernetes/apps/default/homepage/app/rbac.yaml b/kubernetes/apps/default/homepage/app/rbac.yaml new file mode 100644 index 00000000..c1df51e2 --- /dev/null +++ b/kubernetes/apps/default/homepage/app/rbac.yaml @@ -0,0 +1,63 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: &app homepage + labels: + app.kubernetes.io/instance: *app + app.kubernetes.io/name: *app +rules: + - apiGroups: + - "" + resources: + - namespaces + - pods + - nodes + verbs: + - get + - list + - apiGroups: + - extensions + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - apiGroups: + - traefik.containo.us + resources: + - ingressroutes + verbs: + - get + - list + - apiGroups: + - metrics.k8s.io + resources: + - nodes + - pods + verbs: + - get + - list + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: &app homepage + labels: + app.kubernetes.io/instance: *app + app.kubernetes.io/name: *app +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: homepage +subjects: + - kind: ServiceAccount + name: *app + namespace: default # keep diff --git a/kubernetes/apps/default/homepage/ks.yaml b/kubernetes/apps/default/homepage/ks.yaml new file mode 100644 index 00000000..8536917c --- /dev/null +++ b/kubernetes/apps/default/homepage/ks.yaml @@ -0,0 +1,26 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app homepage + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + path: ./kubernetes/apps/default/homepage/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app diff --git a/kubernetes/apps/default/kustomization.yaml b/kubernetes/apps/default/kustomization.yaml new file mode 100644 index 00000000..336f50e4 --- /dev/null +++ b/kubernetes/apps/default/kustomization.yaml @@ -0,0 +1,16 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./qbittorrent/ks.yaml + - ./radarr/ks.yaml + - ./sonarr/ks.yaml + - ./prowlarr/ks.yaml + - ./plex/ks.yaml + - ./notifiarr/ks.yaml + - ./overseerr/ks.yaml + - ./homepage/ks.yaml + - ./nodered/ks.yaml diff --git a/kubernetes/apps/default/namespace.yaml b/kubernetes/apps/default/namespace.yaml new file mode 100644 index 00000000..f659b055 --- /dev/null +++ b/kubernetes/apps/default/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: default + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/default/nodered/app/configs/settings.js b/kubernetes/apps/default/nodered/app/configs/settings.js new file mode 100644 index 00000000..4f776678 --- /dev/null +++ b/kubernetes/apps/default/nodered/app/configs/settings.js @@ -0,0 +1,542 @@ +/** + * This is the default settings file provided by Node-RED. + * + * It can contain any valid JavaScript code that will get run when Node-RED + * is started. + * + * Lines that start with // are commented out. + * Each entry should be separated from the entries above and below by a comma ',' + * + * For more information about individual settings, refer to the documentation: + * https://nodered.org/docs/user-guide/runtime/configuration + * + * The settings are split into the following sections: + * - Flow File and User Directory Settings + * - Security + * - Server Settings + * - Runtime Settings + * - Editor Settings + * - Node Settings + * + **/ + +module.exports = { + + /******************************************************************************* + * Flow File and User Directory Settings + * - flowFile + * - credentialSecret + * - flowFilePretty + * - userDir + * - nodesDir + ******************************************************************************/ + + /** The file containing the flows. If not set, defaults to flows_.json **/ + flowFile: 'flows.json', + + /** By default, credentials are encrypted in storage using a generated key. To + * specify your own secret, set the following property. + * If you want to disable encryption of credentials, set this property to false. + * Note: once you set this property, do not change it - doing so will prevent + * node-red from being able to decrypt your existing credentials and they will be + * lost. + */ + credentialSecret: process.env.NODE_RED_CREDENTIAL_SECRET, + + /** By default, the flow JSON will be formatted over multiple lines making + * it easier to compare changes when using version control. + * To disable pretty-printing of the JSON set the following property to false. + */ + flowFilePretty: true, + + /** By default, all user data is stored in a directory called `.node-red` under + * the user's home directory. To use a different location, the following + * property can be used + */ + //userDir: '/home/nol/.node-red/', + + /** Node-RED scans the `nodes` directory in the userDir to find local node files. + * The following property can be used to specify an additional directory to scan. + */ + //nodesDir: '/home/nol/.node-red/nodes', + + /******************************************************************************* + * Security + * - adminAuth + * - https + * - httpsRefreshInterval + * - requireHttps + * - httpNodeAuth + * - httpStaticAuth + ******************************************************************************/ + + /** To password protect the Node-RED editor and admin API, the following + * property can be used. See http://nodered.org/docs/security.html for details. + */ + //adminAuth: { + // type: "credentials", + // users: [{ + // username: "admin", + // password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.", + // permissions: "*" + // }] + //}, + + /** The following property can be used to enable HTTPS + * This property can be either an object, containing both a (private) key + * and a (public) certificate, or a function that returns such an object. + * See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + * for details of its contents. + */ + + /** Option 1: static object */ + //https: { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + //}, + + /** Option 2: function that returns the HTTP configuration object */ + // https: function() { + // // This function should return the options object, or a Promise + // // that resolves to the options object + // return { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // } + // }, + + /** If the `https` setting is a function, the following setting can be used + * to set how often, in hours, the function will be called. That can be used + * to refresh any certificates. + */ + //httpsRefreshInterval : 12, + + /** The following property can be used to cause insecure HTTP connections to + * be redirected to HTTPS. + */ + //requireHttps: true, + + /** To password protect the node-defined HTTP endpoints (httpNodeRoot), + * including node-red-dashboard, or the static content (httpStatic), the + * following properties can be used. + * The `pass` field is a bcrypt hash of the password. + * See http://nodered.org/docs/security.html#generating-the-password-hash + */ + //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + + /******************************************************************************* + * Server Settings + * - uiPort + * - uiHost + * - apiMaxLength + * - httpServerOptions + * - httpAdminRoot + * - httpAdminMiddleware + * - httpNodeRoot + * - httpNodeCors + * - httpNodeMiddleware + * - httpStatic + * - httpStaticRoot + ******************************************************************************/ + + /** the tcp port that the Node-RED web server is listening on */ + uiPort: process.env.PORT || 1880, + + /** By default, the Node-RED UI accepts connections on all IPv4 interfaces. + * To listen on all IPv6 addresses, set uiHost to "::", + * The following property can be used to listen on a specific interface. For + * example, the following would only allow connections from the local machine. + */ + //uiHost: "127.0.0.1", + + /** The maximum size of HTTP request that will be accepted by the runtime api. + * Default: 5mb + */ + //apiMaxLength: '5mb', + + /** The following property can be used to pass custom options to the Express.js + * server used by Node-RED. For a full list of available options, refer + * to http://expressjs.com/en/api.html#app.settings.table + */ + //httpServerOptions: { }, + + /** By default, the Node-RED UI is available at http://localhost:1880/ + * The following property can be used to specify a different root path. + * If set to false, this is disabled. + */ + //httpAdminRoot: '/admin', + + /** The following property can be used to add a custom middleware function + * in front of all admin http routes. For example, to set custom http + * headers. It can be a single function or an array of middleware functions. + */ + // httpAdminMiddleware: function(req,res,next) { + // // Set the X-Frame-Options header to limit where the editor + // // can be embedded + // //res.set('X-Frame-Options', 'sameorigin'); + // next(); + // }, + + + /** Some nodes, such as HTTP In, can be used to listen for incoming http requests. + * By default, these are served relative to '/'. The following property + * can be used to specifiy a different root path. If set to false, this is + * disabled. + */ + //httpNodeRoot: '/red-nodes', + + /** The following property can be used to configure cross-origin resource sharing + * in the HTTP nodes. + * See https://github.com/troygoode/node-cors#configuration-options for + * details on its contents. The following is a basic permissive set of options: + */ + //httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + //}, + + /** If you need to set an http proxy please set an environment variable + * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. + * For example - http_proxy=http://myproxy.com:8080 + * (Setting it here will have no effect) + * You may also specify no_proxy (or NO_PROXY) to supply a comma separated + * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk + */ + + /** The following property can be used to add a custom middleware function + * in front of all http in nodes. This allows custom authentication to be + * applied to all http in nodes, or any other sort of common request processing. + * It can be a single function or an array of middleware functions. + */ + //httpNodeMiddleware: function(req,res,next) { + // // Handle/reject the request, or pass it on to the http in node by calling next(); + // // Optionally skip our rawBodyParser by setting this to true; + // //req.skipRawBodyParser = true; + // next(); + //}, + + /** When httpAdminRoot is used to move the UI to a different root path, the + * following property can be used to identify a directory of static content + * that should be served at http://localhost:1880/. + * When httpStaticRoot is set differently to httpAdminRoot, there is no need + * to move httpAdminRoot + */ + //httpStatic: '/home/nol/node-red-static/', //single static source + /* OR multiple static sources can be created using an array of objects... */ + //httpStatic: [ + // {path: '/home/nol/pics/', root: "/img/"}, + // {path: '/home/nol/reports/', root: "/doc/"}, + //], + + /** + * All static routes will be appended to httpStaticRoot + * e.g. if httpStatic = "/home/nol/docs" and httpStaticRoot = "/static/" + * then "/home/nol/docs" will be served at "/static/" + * e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}] + * and httpStaticRoot = "/static/" + * then "/home/nol/pics/" will be served at "/static/img/" + */ + //httpStaticRoot: '/static/', + + /******************************************************************************* + * Runtime Settings + * - lang + * - runtimeState + * - diagnostics + * - logging + * - contextStorage + * - exportGlobalContextKeys + * - externalModules + ******************************************************************************/ + + /** Uncomment the following to run node-red in your preferred language. + * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko + * Some languages are more complete than others. + */ + // lang: "de", + + /** Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor + */ + diagnostics: { + /** enable or disable diagnostics endpoint. Must be set to `false` to disable */ + enabled: true, + /** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, + }, + /** Configure runtimeState options + * - enabled: When `enabled` is `true` flows runtime can be Started/Stoped + * by POSTing to available at http://localhost:1880/flows/state + * - ui: When `ui` is `true`, the action `core:start-flows` and + * `core:stop-flows` will be available to logged in users of node-red editor + * Also, the deploy menu (when set to default) will show a stop or start button + */ + runtimeState: { + /** enable or disable flows/state endpoint. Must be set to `false` to disable */ + enabled: false, + /** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ + ui: false, + }, + /** Configure the logging output */ + logging: { + /** Only console logging is currently supported */ + console: { + /** Level of logging to be recorded. Options are: + * fatal - only those errors which make the application unusable should be recorded + * error - record errors which are deemed fatal for a particular request + fatal errors + * warn - record problems which are non fatal + errors + fatal errors + * info - record information about the general running of the application + warn + error + fatal errors + * debug - record information which is more verbose than info + info + warn + error + fatal errors + * trace - record very detailed logging + debug + info + warn + error + fatal errors + * off - turn off all logging (doesn't affect metrics or audit) + */ + level: "info", + /** Whether or not to include metric events in the log output */ + metrics: false, + /** Whether or not to include audit events in the log output */ + audit: false + } + }, + + /** Context Storage + * The following property can be used to enable context storage. The configuration + * provided here will enable file-based context that flushes to disk every 30 seconds. + * Refer to the documentation for further options: https://nodered.org/docs/api/context/ + */ + //contextStorage: { + // default: { + // module:"localfilesystem" + // }, + //}, + + /** `global.keys()` returns a list of all properties set in global context. + * This allows them to be displayed in the Context Sidebar within the editor. + * In some circumstances it is not desirable to expose them to the editor. The + * following property can be used to hide any property set in `functionGlobalContext` + * from being list by `global.keys()`. + * By default, the property is set to false to avoid accidental exposure of + * their values. Setting this to true will cause the keys to be listed. + */ + exportGlobalContextKeys: false, + + /** Configure how the runtime will handle external npm modules. + * This covers: + * - whether the editor will allow new node modules to be installed + * - whether nodes, such as the Function node are allowed to have their + * own dynamically configured dependencies. + * The allow/denyList options can be used to limit what modules the runtime + * will install/load. It can use '*' as a wildcard that matches anything. + */ + externalModules: { + // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ + // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ + // palette: { /** Configuration for the Palette Manager */ + // allowInstall: true, /** Enable the Palette Manager in the editor */ + // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ + // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ + // allowList: ['*'], + // denyList: [], + // allowUpdateList: ['*'], + // denyUpdateList: [] + // }, + // modules: { /** Configuration for node-specified modules */ + // allowInstall: true, + // allowList: [], + // denyList: [] + // } + }, + + + /******************************************************************************* + * Editor Settings + * - disableEditor + * - editorTheme + ******************************************************************************/ + + /** The following property can be used to disable the editor. The admin API + * is not affected by this option. To disable both the editor and the admin + * API, use either the httpRoot or httpAdminRoot properties + */ + //disableEditor: false, + + /** Customising the editor + * See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes + * for all available options. + */ + editorTheme: { + /** The following property can be used to set a custom theme for the editor. + * See https://github.com/node-red-contrib-themes/theme-collection for + * a collection of themes to chose from. + */ + //theme: "", + + /** To disable the 'Welcome to Node-RED' tour that is displayed the first + * time you access the editor for each release of Node-RED, set this to false + */ + //tours: false, + + palette: { + /** The following property can be used to order the categories in the editor + * palette. If a node's category is not in the list, the category will get + * added to the end of the palette. + * If not set, the following default order is used: + */ + //categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], + }, + + projects: { + /** To enable the Projects feature, set this value to true */ + enabled: false, + workflow: { + /** Set the default projects workflow mode. + * - manual - you must manually commit changes + * - auto - changes are automatically committed + * This can be overridden per-user from the 'Git config' + * section of 'User Settings' within the editor + */ + mode: "manual" + } + }, + + codeEditor: { + /** Select the text editor component used by the editor. + * As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired + */ + lib: "monaco", + options: { + /** The follow options only apply if the editor is set to "monaco" + * + * theme - must match the file name of a theme in + * packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme + * e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme" + */ + // theme: "vs", + /** other overrides can be set e.g. fontSize, fontFamily, fontLigatures etc. + * for the full list, see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html + */ + //fontSize: 14, + //fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", + //fontLigatures: true, + } + } + }, + + /******************************************************************************* + * Node Settings + * - fileWorkingDirectory + * - functionGlobalContext + * - functionExternalModules + * - nodeMessageBufferMaxLength + * - ui (for use with Node-RED Dashboard) + * - debugUseColors + * - debugMaxLength + * - execMaxBufferSize + * - httpRequestTimeout + * - mqttReconnectTime + * - serialReconnectTime + * - socketReconnectTime + * - socketTimeout + * - tcpMsgQueueSize + * - inboundWebSocketTimeout + * - tlsConfigDisableLocalFiles + * - webSocketNodeVerifyClient + ******************************************************************************/ + + /** The working directory to handle relative file paths from within the File nodes + * defaults to the working directory of the Node-RED process. + */ + //fileWorkingDirectory: "", + + /** Allow the Function node to load additional npm modules directly */ + functionExternalModules: true, + + /** The following property can be used to set predefined values in Global Context. + * This allows extra node modules to be made available with in Function node. + * For example, the following: + * functionGlobalContext: { os:require('os') } + * will allow the `os` module to be accessed in a Function node using: + * global.get("os") + */ + functionGlobalContext: { + // os:require('os'), + }, + + /** The maximum number of messages nodes will buffer internally as part of their + * operation. This applies across a range of nodes that operate on message sequences. + * defaults to no limit. A value of 0 also means no limit is applied. + */ + //nodeMessageBufferMaxLength: 0, + + /** If you installed the optional node-red-dashboard you can set it's path + * relative to httpNodeRoot + * Other optional properties include + * readOnly:{boolean}, + * middleware:{function or array}, (req,res,next) - http middleware + * ioMiddleware:{function or array}, (socket,next) - socket.io middleware + */ + //ui: { path: "ui" }, + + /** Colourise the console output of the debug node */ + //debugUseColors: true, + + /** The maximum length, in characters, of any message sent to the debug sidebar tab */ + debugMaxLength: 1000, + + /** Maximum buffer size for the exec node. Defaults to 10Mb */ + //execMaxBufferSize: 10000000, + + /** Timeout in milliseconds for HTTP request connections. Defaults to 120s */ + //httpRequestTimeout: 120000, + + /** Retry time in milliseconds for MQTT connections */ + mqttReconnectTime: 15000, + + /** Retry time in milliseconds for Serial port connections */ + serialReconnectTime: 15000, + + /** Retry time in milliseconds for TCP socket connections */ + //socketReconnectTime: 10000, + + /** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */ + //socketTimeout: 120000, + + /** Maximum number of messages to wait in queue while attempting to connect to TCP socket + * defaults to 1000 + */ + //tcpMsgQueueSize: 2000, + + /** Timeout in milliseconds for inbound WebSocket connections that do not + * match any configured node. Defaults to 5000 + */ + //inboundWebSocketTimeout: 5000, + + /** To disable the option for using local files for storing keys and + * certificates in the TLS configuration node, set this to true. + */ + //tlsConfigDisableLocalFiles: true, + + /** The following property can be used to verify websocket connection attempts. + * This allows, for example, the HTTP request headers to be checked to ensure + * they include valid authentication information. + */ + //webSocketNodeVerifyClient: function(info) { + // /** 'info' has three properties: + // * - origin : the value in the Origin header + // * - req : the HTTP request + // * - secure : true if req.connection.authorized or req.connection.encrypted is set + // * + // * The function should return true if the connection should be accepted, false otherwise. + // * + // * Alternatively, if this function is defined to accept a second argument, callback, + // * it can be used to verify the client asynchronously. + // * The callback takes three arguments: + // * - result : boolean, whether to accept the connection or not + // * - code : if result is false, the HTTP error status to return + // * - reason: if result is false, the HTTP reason string to return + // */ + //}, +} diff --git a/kubernetes/apps/default/nodered/app/externalsecret.yaml b/kubernetes/apps/default/nodered/app/externalsecret.yaml new file mode 100644 index 00000000..87b6ec9c --- /dev/null +++ b/kubernetes/apps/default/nodered/app/externalsecret.yaml @@ -0,0 +1,19 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: nodered +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: nodered-secret + template: + engineVersion: v2 + data: + NODE_RED_CREDENTIAL_SECRET: "{{ .CREDENTIAL_SECRET }}" + dataFrom: + - extract: + key: nodered diff --git a/kubernetes/apps/default/nodered/app/helmrelease.yaml b/kubernetes/apps/default/nodered/app/helmrelease.yaml new file mode 100644 index 00000000..7b21b236 --- /dev/null +++ b/kubernetes/apps/default/nodered/app/helmrelease.yaml @@ -0,0 +1,91 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app nodered + namespace: default +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + controllers: + nodered: + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: docker.io/nodered/node-red + tag: 3.1.9@sha256:df827e6ee450221ff68e2edd3bf9b43a991f6abb49121b0db95b744ad4e17a8c + env: + PUID: 1000 + PGID: 1000 + UMASK: 002 + TZ: "${TIMEZONE}" + envFrom: + - secretRef: + name: nodered-secret + pod: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + service: + app: + controller: nodered + ports: + http: + port: 1880 + ingress: + app: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: node-red.png + gethomepage.dev/name: Node-red + gethomepage.dev/description: Visual programming for automation + gethomepage.dev/group: Home + hosts: + - host: "nodered.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: +# config: +# existingClaim: *app +# globalMounts: +# - path: /data +# settings: +# type: configMap +# name: node-red-configmap +# globalMounts: +# - path: /data/settings.js +# subPath: settings.js +# readOnly: true + downloads: + type: nfs + server: "${NAS_URL}" + path: "/volume1/rpi/docker/Appdata/nodered" + globalMounts: + - path: /data diff --git a/kubernetes/apps/default/nodered/app/kustomization.yaml b/kubernetes/apps/default/nodered/app/kustomization.yaml new file mode 100644 index 00000000..fcbef500 --- /dev/null +++ b/kubernetes/apps/default/nodered/app/kustomization.yaml @@ -0,0 +1,15 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/internal +configMapGenerator: + - name: node-red-configmap + files: + - ./configs/settings.js +generatorOptions: + disableNameSuffixHash: true diff --git a/kubernetes/apps/default/nodered/ks.yaml b/kubernetes/apps/default/nodered/ks.yaml new file mode 100644 index 00000000..6cb864b4 --- /dev/null +++ b/kubernetes/apps/default/nodered/ks.yaml @@ -0,0 +1,27 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app nodered + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + path: ./kubernetes/apps/default/nodered/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 1Gi diff --git a/kubernetes/apps/default/notifiarr/app/externalsecret.yaml b/kubernetes/apps/default/notifiarr/app/externalsecret.yaml new file mode 100644 index 00000000..4e226f96 --- /dev/null +++ b/kubernetes/apps/default/notifiarr/app/externalsecret.yaml @@ -0,0 +1,48 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: notifiarr +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: notifiarr-secret + template: + engineVersion: v2 + data: + DN_API_KEY: "{{ .notifiarr_api_key }}" + DN_UI_PASSWORD: "{{ .notifiarr_password }}" + DN_PLEX_URL: "http://plex.default.svc.cluster.local:32400" + DN_PLEX_TOKEN: "{{ .PLEX_TOKEN }}" + DN_QBIT_0_NAME: "qbittorrent-kube" + DN_QBIT_0_URL: "http://qbittorrent.default.svc.cluster.local:8080" + DN_QBIT_0_USER: "{{ .qbittorrent_username }}" + DN_QBIT_0_PASS: "{{ .qbittorrent_password }}" + DN_RADARR_0_NAME: "radarr-kube" + DN_RADARR_0_URL: "http://radarr.default.svc.cluster.local:8080" + DN_RADARR_0_API_KEY: "{{ .RADARR_API_KEY }}" + DN_SONARR_0_NAME: "sonarr-kube" + DN_SONARR_0_URL: "http://sonarr.default.svc.cluster.local:8080" + DN_SONARR_0_API_KEY: "{{ .SONARR_API_KEY }}" + dataFrom: + - extract: + key: qbittorrent + rewrite: + - regexp: + source: "(.*)" + target: "qbittorrent_$1" + - extract: + key: notifiarr + rewrite: + - regexp: + source: "(.*)" + target: "notifiarr_$1" + - extract: + key: radarr + - extract: + key: sonarr + - extract: + key: plex diff --git a/kubernetes/apps/default/notifiarr/app/helmrelease.yaml b/kubernetes/apps/default/notifiarr/app/helmrelease.yaml new file mode 100644 index 00000000..15ba4683 --- /dev/null +++ b/kubernetes/apps/default/notifiarr/app/helmrelease.yaml @@ -0,0 +1,75 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: notifiarr + namespace: default +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + strategy: uninstall + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + annotations: + reloader.stakater.com/auto: "true" + defaultPodOptions: + hostname: notifiarr-kube + controllers: + notifiarr: + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: golift/notifiarr + tag: 0.7@sha256:c7a21fcf4ae2d5035c3302debde257bf6b3338b768d5678efb59e093a246c515 + env: + PUID: 1000 + PGID: 1000 + UMASK: 002 + TZ: "${TIMEZONE}" + envFrom: + - secretRef: + name: notifiarr-secret + service: + app: + controller: notifiarr + ports: + http: + port: 5454 + ingress: + app: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: notifiarr.png + gethomepage.dev/name: notifiarr + gethomepage.dev/group: Media + hosts: + - host: "notifiarr.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http diff --git a/kubernetes/apps/default/notifiarr/app/kustomization.yaml b/kubernetes/apps/default/notifiarr/app/kustomization.yaml new file mode 100644 index 00000000..17eb4a00 --- /dev/null +++ b/kubernetes/apps/default/notifiarr/app/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml + - ../../../../templates/gatus/internal diff --git a/kubernetes/apps/default/notifiarr/ks.yaml b/kubernetes/apps/default/notifiarr/ks.yaml new file mode 100644 index 00000000..dad4abd9 --- /dev/null +++ b/kubernetes/apps/default/notifiarr/ks.yaml @@ -0,0 +1,26 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app notifiarr + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + path: ./kubernetes/apps/default/notifiarr/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app diff --git a/kubernetes/apps/default/overseerr/app/helmrelease.yaml b/kubernetes/apps/default/overseerr/app/helmrelease.yaml new file mode 100644 index 00000000..0cd40d8b --- /dev/null +++ b/kubernetes/apps/default/overseerr/app/helmrelease.yaml @@ -0,0 +1,115 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app overseerr +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + strategy: uninstall + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + controllers: + overseerr: + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: ghcr.io/sct/overseerr + tag: 1.33.2@sha256:714ea6db2bc007a2262d112bef7eec74972eb33d9c72bddb9cbd98b8742de950 + env: + TZ: "${TIMEZONE}" + LOG_LEVEL: "info" + PORT: &port 80 + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /api/v1/status + port: *port + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + startup: + enabled: false + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + resources: + requests: + cpu: 10m + limits: + memory: 2Gi + pod: + securityContext: + runAsUser: 568 + runAsGroup: 568 + runAsNonRoot: true + fsGroup: 568 + fsGroupChangePolicy: OnRootMismatch + service: + app: + controller: overseerr + ports: + http: + port: *port + ingress: + app: + className: external + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/group: Media + gethomepage.dev/name: Overseerr + gethomepage.dev/icon: overseerr.png + gethomepage.dev/description: Media Request Management + gethomepage.dev/widget.type: overseerr + gethomepage.dev/widget.url: http://overseerr.default.svc.cluster.local + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_OVERSEERR_TOKEN}}" + external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}" + hosts: + - host: "overseerr.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + existingClaim: *app + globalMounts: + - path: /app/config + cache: + existingClaim: overseerr-cache + globalMounts: + - path: /app/config/cache + logs: + type: emptyDir + globalMounts: + - path: /app/config/logs + tmp: + type: emptyDir diff --git a/kubernetes/apps/default/overseerr/app/kustomization.yaml b/kubernetes/apps/default/overseerr/app/kustomization.yaml new file mode 100644 index 00000000..a3f6169d --- /dev/null +++ b/kubernetes/apps/default/overseerr/app/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./pvc.yaml + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/external diff --git a/kubernetes/apps/default/overseerr/app/pvc.yaml b/kubernetes/apps/default/overseerr/app/pvc.yaml new file mode 100644 index 00000000..75f45ae8 --- /dev/null +++ b/kubernetes/apps/default/overseerr/app/pvc.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: overseerr-cache +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 3Gi + storageClassName: longhorn + reclaimPolicy: Delete diff --git a/kubernetes/apps/default/overseerr/ks.yaml b/kubernetes/apps/default/overseerr/ks.yaml new file mode 100644 index 00000000..d37b1274 --- /dev/null +++ b/kubernetes/apps/default/overseerr/ks.yaml @@ -0,0 +1,25 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app overseerr + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/overseerr/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 1Gi diff --git a/kubernetes/apps/default/paperless/app/helmrelease.yaml b/kubernetes/apps/default/paperless/app/helmrelease.yaml new file mode 100644 index 00000000..dca4cdae --- /dev/null +++ b/kubernetes/apps/default/paperless/app/helmrelease.yaml @@ -0,0 +1,87 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app paperless + namespace: default +spec: + interval: 30m + driftDetection: + mode: enabled + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + controllers: + paperless: + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: ghcr.io/paperless-ngx/paperless-ngx + tag: 2.7.2@sha256:703c990a790dfd4d25fb56df3afec27b13cb0926a3818bf265edac9c71311647 + env: + PAPERLESS_TIME_ZONE: "${TIMEZONE}" + resources: + requests: + cpu: 11m + memory: 500Mi + limits: + memory: 2000Mi + service: + app: + controller: paperless + ports: + http: + port: *port + ingress: + app: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: paperless-ngx.png + gethomepage.dev/name: Paperless + gethomepage.dev/group: Storage + gethomepage.dev/description: Document management + gethomepage.dev/widget.type: paperless + gethomepage.dev/widget.url: http://paperless.default.svc.cluster.local:8080 + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_RADARR_TOKEN}}" + hosts: + - host: "paperless.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + existingClaim: *app + globalMounts: + - path: /data/local + downloads: + type: nfs + server: "${NAS_URL}" + path: "${NAS_PATH}/paperless" + globalMounts: + - path: /data/nas diff --git a/kubernetes/apps/default/paperless/app/kustomization.yaml b/kubernetes/apps/default/paperless/app/kustomization.yaml new file mode 100644 index 00000000..82c34407 --- /dev/null +++ b/kubernetes/apps/default/paperless/app/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/internal diff --git a/kubernetes/apps/default/paperless/ks.yaml b/kubernetes/apps/default/paperless/ks.yaml new file mode 100644 index 00000000..14f312cb --- /dev/null +++ b/kubernetes/apps/default/paperless/ks.yaml @@ -0,0 +1,27 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app paperless + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/paperless/app + prune: true + dependsOn: + - name: external-secrets-stores + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 1Gi diff --git a/kubernetes/apps/default/plex/app/helmrelease.yaml b/kubernetes/apps/default/plex/app/helmrelease.yaml new file mode 100644 index 00000000..e9f84927 --- /dev/null +++ b/kubernetes/apps/default/plex/app/helmrelease.yaml @@ -0,0 +1,125 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app plex +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + strategy: uninstall + dependsOn: + - name: intel-device-plugin-gpu + namespace: kube-system + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + controllers: + plex: + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: ghcr.io/onedr0p/plex + tag: 1.40.0.7998-c29d4c0c8@sha256:7c4501799f0d5f4f94fcb95a8a47b883528354c779a182a9ae4af118a1fc6b10 + env: + TZ: "${TIMEZONE}" + PLEX_ADVERTISE_URL: "https://plex.${SECRET_DOMAIN}:443,http://${CLUSTER_LB_PLEX}:32400" + PLEX_NO_AUTH_NETWORKS: 192.168.10.0/24 + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /identity + port: 32400 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + startup: + enabled: false + resources: + requests: + cpu: 100m + limits: + gpu.intel.com/i915: 1 + memory: 8Gi + pod: + nodeSelector: + intel.feature.node.kubernetes.io/gpu: "true" + securityContext: + runAsUser: 568 + runAsGroup: 568 + fsGroup: 568 + supplementalGroups: [ 44, 107, 1000 ] + service: + app: + controller: plex + type: LoadBalancer + annotations: + io.cilium/lb-ipam-ips: "${CLUSTER_LB_PLEX}" + ports: + http: + port: 32400 + ingress: + app: + className: external + annotations: + external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}" + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + gethomepage.dev/enabled: "true" + gethomepage.dev/group: Media + gethomepage.dev/name: Plex + gethomepage.dev/icon: plex.png + gethomepage.dev/description: Media Player + gethomepage.dev/widget.type: plex + gethomepage.dev/widget.url: http://plex.default.svc.cluster.local:32400 + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_PLEX_TOKEN}}" + hosts: + - host: "plex.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + existingClaim: *app + globalMounts: + - path: /config + # Separate PVC for cache to avoid backing up cache files + plex-cache: + existingClaim: plex-cache + globalMounts: + - path: /config/Library/Application Support/Plex Media Server/Cache + tmp: + type: emptyDir + transcode: + type: emptyDir + media: + type: nfs + server: "${NAS_URL}" + path: "${NAS_PATH}/media" + globalMounts: + - path: /media + readOnly: true diff --git a/kubernetes/apps/default/plex/app/kustomization.yaml b/kubernetes/apps/default/plex/app/kustomization.yaml new file mode 100644 index 00000000..a3f6169d --- /dev/null +++ b/kubernetes/apps/default/plex/app/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./pvc.yaml + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/external diff --git a/kubernetes/apps/default/plex/app/pvc.yaml b/kubernetes/apps/default/plex/app/pvc.yaml new file mode 100644 index 00000000..54881821 --- /dev/null +++ b/kubernetes/apps/default/plex/app/pvc.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: plex-cache +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi + storageClassName: longhorn + reclaimPolicy: Delete diff --git a/kubernetes/apps/default/plex/ks.yaml b/kubernetes/apps/default/plex/ks.yaml new file mode 100644 index 00000000..c8818812 --- /dev/null +++ b/kubernetes/apps/default/plex/ks.yaml @@ -0,0 +1,26 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app plex + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/plex/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + GATUS_PATH: /web/index.html + VOLSYNC_CAPACITY: 10Gi diff --git a/kubernetes/apps/default/prowlarr/app/externalsecret.yaml b/kubernetes/apps/default/prowlarr/app/externalsecret.yaml new file mode 100644 index 00000000..f69940c4 --- /dev/null +++ b/kubernetes/apps/default/prowlarr/app/externalsecret.yaml @@ -0,0 +1,33 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: prowlarr +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: prowlarr-secret + template: + engineVersion: v2 + data: + PROWLARR__API_KEY: "{{ .PROWLARR_API_KEY }}" + PROWLARR__POSTGRES_HOST: &dbHost postgres-cluster-rw.database.svc.cluster.local + PROWLARR__POSTGRES_PORT: "5432" + PROWLARR__POSTGRES_USER: &dbUser "{{ .PROWLARR_POSTGRES_USER }}" + PROWLARR__POSTGRES_PASSWORD: &dbPass "{{ .PROWLARR_POSTGRES_PASSWORD }}" + PROWLARR__POSTGRES_MAIN_DB: prowlarr_main + PROWLARR__POSTGRES_LOG_DB: prowlarr_log + INIT_POSTGRES_DBNAME: prowlarr_main prowlarr_log + INIT_POSTGRES_HOST: *dbHost + INIT_POSTGRES_USER: *dbUser + INIT_POSTGRES_PASS: *dbPass + INIT_POSTGRES_SUPER_USER: "{{ .POSTGRES_SUPER_USER }}" + INIT_POSTGRES_SUPER_PASS: "{{ .POSTGRES_SUPER_PASS }}" + dataFrom: + - extract: + key: prowlarr + - extract: + key: cloudnative-pg diff --git a/kubernetes/apps/default/prowlarr/app/helmrelease.yaml b/kubernetes/apps/default/prowlarr/app/helmrelease.yaml new file mode 100644 index 00000000..cea636ce --- /dev/null +++ b/kubernetes/apps/default/prowlarr/app/helmrelease.yaml @@ -0,0 +1,106 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app prowlarr + namespace: default +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + strategy: uninstall + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + defaultPodOptions: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + controllers: + prowlarr: + annotations: + reloader.stakater.com/auto: "true" + initContainers: + init-db: + image: + repository: ghcr.io/onedr0p/postgres-init + tag: 16 + envFrom: &envFrom + - secretRef: + name: prowlarr-secret + containers: + app: + image: + repository: ghcr.io/onedr0p/prowlarr + tag: 1.17@sha256:da8fba1ef93d8013b86ea4d9fc4ccea7433db5ed79dc8d7fa12fe6d4374f0412 + env: + TZ: "${TIMEZONE}" + PROWLARR__INSTANCE_NAME: Prowlarr + PROWLARR__PORT: &port 8080 + PROWLARR__LOG_LEVEL: info + PROWLARR__AUTHENTICATION_METHOD: External + PROWLARR__THEME: dark + envFrom: *envFrom + resources: + requests: + cpu: 100m + memory: 100Mi + limits: + memory: 500Mi + service: + app: + controller: prowlarr + ports: + http: + port: *port + ingress: + app: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: prowlarr.png + gethomepage.dev/name: Prowlarr + gethomepage.dev/group: Media + gethomepage.dev/description: Torrent/NZB Indexer Management + gethomepage.dev/widget.type: prowlarr + gethomepage.dev/widget.url: http://prowlarr.default.svc.cluster.local:8080 + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_PROWLARR_TOKEN}}" + hosts: + - host: "prowlarr.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + existingClaim: *app + globalMounts: + - path: /config + downloads: + type: nfs + server: "${NAS_URL}" + path: "${NAS_PATH}" + globalMounts: + - path: /data diff --git a/kubernetes/apps/default/prowlarr/app/kustomization.yaml b/kubernetes/apps/default/prowlarr/app/kustomization.yaml new file mode 100644 index 00000000..82b7f826 --- /dev/null +++ b/kubernetes/apps/default/prowlarr/app/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/internal diff --git a/kubernetes/apps/default/prowlarr/ks.yaml b/kubernetes/apps/default/prowlarr/ks.yaml new file mode 100644 index 00000000..e8cb51b5 --- /dev/null +++ b/kubernetes/apps/default/prowlarr/ks.yaml @@ -0,0 +1,28 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app prowlarr + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/prowlarr/app + prune: true + dependsOn: + - name: cloudnative-pg + - name: external-secrets-stores + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 1Gi diff --git a/kubernetes/apps/default/qbittorrent/app/helmrelease.yaml b/kubernetes/apps/default/qbittorrent/app/helmrelease.yaml new file mode 100644 index 00000000..fabe66a4 --- /dev/null +++ b/kubernetes/apps/default/qbittorrent/app/helmrelease.yaml @@ -0,0 +1,157 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app qbittorrent + namespace: default +spec: + interval: 30m + driftDetection: + mode: enabled + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + defaultPodOptions: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + controllers: + qbittorrent: + initContainers: + init-categories: + image: + repository: docker.io/library/alpine + tag: 3.19.1 + command: + - "sh" + - "-c" + - | + mkdir -p /config/qBittorrent/ && + echo '{"movies": {"save_path": "/data/torrents/movies"}, "tv": {"save_path": "/data/torrents/tv"}}' > /config/qBittorrent/categories.json + containers: + app: + image: + repository: ghcr.io/onedr0p/qbittorrent + tag: 4.6.4@sha256:53ead5ab43027d04efc5d52740aa02308a88d6b4a6eaa90cf6fd2e94fc11ba17 + env: + TZ: "${TIMEZONE}" + QBITTORRENT__PORT: &port 8080 + QBITTORRENT__BT_PORT: &port-bt 58462 + QBT_Preferences__Downloads__SavePath: /data/torrents + QBT_Preferences__WebUI__Password_PBKDF2: "@ByteArray(iXranQkCEwRqp96g0yKHHA==:2ujiTbO+e12jHqzAJccPqjrBcVmRhaTSrrMi27VRiv2rbWk50twuRcHBCc8jsX/J/oZ8JQnBzHFjNzZ2bvpZkQ==)" + QBT_Preferences__WebUI__AlternativeUIEnabled: 'true' + QBT_Preferences__WebUI__RootFolder: '/add-ons/VueTorrent' + QBT_Preferences__WebUI__LocalHostAuth: false + QBT_Preferences__WebUI__UseUPNP: false + QBT_Preferences__WebUI__CSRFProtection: false + QBT_Preferences__WebUI__ClickjackingProtection: false + QBT_Preferences__WebUI__AuthSubnetWhitelistEnabled: true + QBT_Preferences__WebUI__AuthSubnetWhitelist: |- + 10.42.0.0/16, 192.168.10.0/24, 192.168.20.0/24 + QBT_BitTorrent__Session__AlternativeGlobalDLSpeedLimit: 20000 + QBT_BitTorrent__Session__AlternativeGlobalUPSpeedLimit: 0 + QBT_BitTorrent__Session__GlobalUPSpeedLimit: 0 + QBT_BitTorrent__Session__GlobalDLSpeedLimit: 2500 + QBT_BitTorrent__Session__UseAlternativeGlobalSpeedLimit: false + QBT_BitTorrent__Session__BandwidthSchedulerEnabled: true + QBT_BitTorrent__Session__DisableAutoTMMByDefault: false + QBT_BitTorrent__Session__TempPathEnabled: false + QBT_BitTorrent__Session__DisableAutoTMMTriggers__CategorySavePathChanged: false + QBT_BitTorrent__Session__DisableAutoTMMTriggers__DefaultSavePathChanged: false + QBT_BitTorrent__Scheduler__days: true + QBT_BitTorrent__Scheduler__start_time: '@Variant(\0\0\0\xf\0\0\0\0)' + QBT_BitTorrent__Scheduler__end_time: '@Variant(\0\0\0\xf\x1\xb7t\0)' + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + memory: 3Gi + secondary: + dependsOn: app + image: + repository: registry.k8s.io/git-sync/git-sync + tag: v4.2.3 + args: + - --repo=https://github.com/WDaan/VueTorrent + - --ref=latest-release + - --period=86400s + - --root=/add-ons + resources: + requests: + cpu: 10m + memory: 25Mi + limits: + memory: 50Mi + service: + app: + controller: qbittorrent + type: LoadBalancer + annotations: + io.cilium/lb-ipam-ips: "${CLUSTER_LB_QBITTORRENT}" + ports: + http: + port: *port + bittorrent: + enabled: true + port: *port-bt + protocol: TCP + ingress: + app: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/group: Media + gethomepage.dev/name: qBittorrent + gethomepage.dev/icon: qbittorrent.png + gethomepage.dev/description: Torrent Client + gethomepage.dev/widget.type: qbittorrent + gethomepage.dev/widget.url: http://qbittorrent.default.svc.cluster.local:8080 + gethomepage.dev/widget.username: "{{HOMEPAGE_VAR_QBITTORRENT_USERNAME}}" + gethomepage.dev/widget.password: "{{HOMEPAGE_VAR_QBITTORRENT_PASSWORD}}" + hosts: + - host: "torrent.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + existingClaim: *app + globalMounts: + - path: /config + downloads: + type: nfs + server: "${NAS_URL}" + path: "${NAS_PATH}" + globalMounts: + - path: /data + add-ons: + enabled: true + type: emptyDir + globalMounts: + - path: /add-ons diff --git a/kubernetes/apps/default/qbittorrent/app/kustomization.yaml b/kubernetes/apps/default/qbittorrent/app/kustomization.yaml new file mode 100644 index 00000000..82c34407 --- /dev/null +++ b/kubernetes/apps/default/qbittorrent/app/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/internal diff --git a/kubernetes/apps/default/qbittorrent/ks.yaml b/kubernetes/apps/default/qbittorrent/ks.yaml new file mode 100644 index 00000000..7e093546 --- /dev/null +++ b/kubernetes/apps/default/qbittorrent/ks.yaml @@ -0,0 +1,25 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app qbittorrent + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/qbittorrent/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 2Gi diff --git a/kubernetes/apps/default/radarr/app/externalsecret.yaml b/kubernetes/apps/default/radarr/app/externalsecret.yaml new file mode 100644 index 00000000..fb563fe7 --- /dev/null +++ b/kubernetes/apps/default/radarr/app/externalsecret.yaml @@ -0,0 +1,33 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: radarr +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: radarr-secret + template: + engineVersion: v2 + data: + RADARR__API_KEY: "{{ .RADARR_API_KEY }}" + RADARR__POSTGRES_HOST: &dbHost postgres-cluster-rw.database.svc.cluster.local + RADARR__POSTGRES_PORT: "5432" + RADARR__POSTGRES_USER: &dbUser "{{ .RADARR_POSTGRES_USER }}" + RADARR__POSTGRES_PASSWORD: &dbPass "{{ .RADARR_POSTGRES_PASSWORD }}" + RADARR__POSTGRES_MAIN_DB: radarr_main + RADARR__POSTGRES_LOG_DB: radarr_log + INIT_POSTGRES_DBNAME: radarr_main radarr_log + INIT_POSTGRES_HOST: *dbHost + INIT_POSTGRES_USER: *dbUser + INIT_POSTGRES_PASS: *dbPass + INIT_POSTGRES_SUPER_USER: "{{ .POSTGRES_SUPER_USER }}" + INIT_POSTGRES_SUPER_PASS: "{{ .POSTGRES_SUPER_PASS }}" + dataFrom: + - extract: + key: cloudnative-pg + - extract: + key: radarr diff --git a/kubernetes/apps/default/radarr/app/helmrelease.yaml b/kubernetes/apps/default/radarr/app/helmrelease.yaml new file mode 100644 index 00000000..f5313d44 --- /dev/null +++ b/kubernetes/apps/default/radarr/app/helmrelease.yaml @@ -0,0 +1,107 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app radarr + namespace: default +spec: + interval: 30m + driftDetection: + mode: enabled + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + defaultPodOptions: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + controllers: + radarr: + annotations: + reloader.stakater.com/auto: "true" + initContainers: + init-db: + image: + repository: ghcr.io/onedr0p/postgres-init + tag: 16 + envFrom: &envFrom + - secretRef: + name: radarr-secret + containers: + app: + image: + repository: ghcr.io/onedr0p/radarr-develop + tag: 5.6.0.8846@sha256:b3137a2b451683d834627bf6997460f26eb864757b1ffb5eb6544a8ba6d432ef + env: + TZ: "${TIMEZONE}" + RADARR__INSTANCE_NAME: Radarr + RADARR__PORT: &port 8080 + RADARR__APPLICATION_URL: "https://radarr.${SECRET_DOMAIN}" + RADARR__LOG_LEVEL: info + RADARR__THEME: dark + envFrom: *envFrom + resources: + requests: + cpu: 500m + memory: 500Mi + limits: + memory: 2000Mi + service: + app: + controller: radarr + ports: + http: + port: *port + ingress: + app: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: radarr.png + gethomepage.dev/name: Radarr + gethomepage.dev/group: Media + gethomepage.dev/description: Movie Downloads + gethomepage.dev/widget.type: radarr + gethomepage.dev/widget.url: http://radarr.default.svc.cluster.local:8080 + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_RADARR_TOKEN}}" + hosts: + - host: "radarr.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + existingClaim: *app + globalMounts: + - path: /config + downloads: + type: nfs + server: "${NAS_URL}" + path: "${NAS_PATH}" + globalMounts: + - path: /data diff --git a/kubernetes/apps/default/radarr/app/kustomization.yaml b/kubernetes/apps/default/radarr/app/kustomization.yaml new file mode 100644 index 00000000..82b7f826 --- /dev/null +++ b/kubernetes/apps/default/radarr/app/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/internal diff --git a/kubernetes/apps/default/radarr/ks.yaml b/kubernetes/apps/default/radarr/ks.yaml new file mode 100644 index 00000000..7bf84148 --- /dev/null +++ b/kubernetes/apps/default/radarr/ks.yaml @@ -0,0 +1,28 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app radarr + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/radarr/app + prune: true + dependsOn: + - name: cloudnative-pg + - name: external-secrets-stores + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 1Gi diff --git a/kubernetes/apps/default/sonarr/app/externalsecret.yaml b/kubernetes/apps/default/sonarr/app/externalsecret.yaml new file mode 100644 index 00000000..3b09d9c9 --- /dev/null +++ b/kubernetes/apps/default/sonarr/app/externalsecret.yaml @@ -0,0 +1,32 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: sonarr +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: sonarr-secret + template: + engineVersion: v2 + data: + SONARR__API_KEY: "{{ .SONARR_API_KEY }}" + SONARR__POSTGRES__HOST: &dbHost postgres-cluster-rw.database.svc.cluster.local + SONARR__POSTGRES__PORT: "5432" + SONARR__POSTGRES__USER: &dbUser "{{ .SONARR_POSTGRES_USER }}" + SONARR__POSTGRES__PASSWORD: &dbPass "{{ .SONARR_POSTGRES_PASSWORD }}" + SONARR__POSTGRES__MAINDB: &dbName sonarr_main + INIT_POSTGRES_DBNAME: *dbName + INIT_POSTGRES_HOST: *dbHost + INIT_POSTGRES_USER: *dbUser + INIT_POSTGRES_PASS: *dbPass + INIT_POSTGRES_SUPER_USER: "{{ .POSTGRES_SUPER_USER }}" + INIT_POSTGRES_SUPER_PASS: "{{ .POSTGRES_SUPER_PASS }}" + dataFrom: + - extract: + key: cloudnative-pg + - extract: + key: sonarr diff --git a/kubernetes/apps/default/sonarr/app/helmrelease.yaml b/kubernetes/apps/default/sonarr/app/helmrelease.yaml new file mode 100644 index 00000000..bf022fca --- /dev/null +++ b/kubernetes/apps/default/sonarr/app/helmrelease.yaml @@ -0,0 +1,109 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app sonarr + namespace: default +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + strategy: uninstall + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + values: + defaultPodOptions: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + controllers: + sonarr: + annotations: + reloader.stakater.com/auto: "true" + initContainers: + init-db: + image: + repository: ghcr.io/onedr0p/postgres-init + tag: 16 + envFrom: &envFrom + - secretRef: + name: sonarr-secret + containers: + app: + image: + repository: ghcr.io/onedr0p/sonarr-develop + tag: 4.0.4.1668@sha256:0b89ee847f0b7e782386be22f964de18046e828c47d0d34eba5b3651c361eaa4 + env: + TZ: "${TIMEZONE}" + SONARR__AUTH__METHOD: External + SONARR__AUTH__REQUIRED: DisabledForLocalAddresses + SONARR__APP__INSTANCENAME: Sonarr + SONARR__SERVER__PORT: &port 8080 + SONARR__SERVER__URLBASE: "https://sonarr.${SECRET_DOMAIN}" + SONARR__LOG__DBENABLED: "False" + SONARR__LOG__LEVEL: info + SONARR__APP__THEME: dark + envFrom: *envFrom + resources: + requests: + cpu: 500m + memory: 500Mi + limits: + memory: 2000Mi + service: + app: + controller: sonarr + ports: + http: + port: *port + ingress: + app: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: sonarr.png + gethomepage.dev/name: Sonarr + gethomepage.dev/group: Media + gethomepage.dev/description: TV Downloads + gethomepage.dev/widget.type: sonarr + gethomepage.dev/widget.url: http://sonarr.default.svc.cluster.local:8080 + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_SONARR_TOKEN}}" + hosts: + - host: "sonarr.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + existingClaim: *app + globalMounts: + - path: /config + downloads: + type: nfs + server: "${NAS_URL}" + path: "${NAS_PATH}" + globalMounts: + - path: /data diff --git a/kubernetes/apps/default/sonarr/app/kustomization.yaml b/kubernetes/apps/default/sonarr/app/kustomization.yaml new file mode 100644 index 00000000..5a11297d --- /dev/null +++ b/kubernetes/apps/default/sonarr/app/kustomization.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml + - ../../../../templates/volsync + - ../../../../templates/gatus/internal + diff --git a/kubernetes/apps/default/sonarr/ks.yaml b/kubernetes/apps/default/sonarr/ks.yaml new file mode 100644 index 00000000..0c4b222a --- /dev/null +++ b/kubernetes/apps/default/sonarr/ks.yaml @@ -0,0 +1,28 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/kustomization-kustomize-v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app sonarr + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/sonarr/app + prune: true + dependsOn: + - name: cloudnative-pg + - name: external-secrets-stores + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 1Gi diff --git a/kubernetes/apps/external-secrets/alert.yaml b/kubernetes/apps/external-secrets/alert.yaml new file mode 100644 index 00000000..ba1970d2 --- /dev/null +++ b/kubernetes/apps/external-secrets/alert.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: external-secrets +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: external-secrets +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: HelmRelease + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/external-secrets/external-secrets/app/helmrelease.yaml b/kubernetes/apps/external-secrets/external-secrets/app/helmrelease.yaml new file mode 100644 index 00000000..51b93c73 --- /dev/null +++ b/kubernetes/apps/external-secrets/external-secrets/app/helmrelease.yaml @@ -0,0 +1,37 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: external-secrets +spec: + interval: 30m + chart: + spec: + chart: external-secrets + version: 0.9.18 + sourceRef: + kind: HelmRepository + name: external-secrets + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + values: + installCRDs: true + serviceMonitor: + enabled: true + interval: 1m + webhook: + serviceMonitor: + enabled: true + interval: 1m + certController: + serviceMonitor: + enabled: true + interval: 1m diff --git a/kubernetes/apps/external-secrets/external-secrets/app/kustomization.yaml b/kubernetes/apps/external-secrets/external-secrets/app/kustomization.yaml new file mode 100644 index 00000000..076bf83c --- /dev/null +++ b/kubernetes/apps/external-secrets/external-secrets/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./onepassword-connect.secret.sops.yaml diff --git a/kubernetes/apps/external-secrets/external-secrets/app/onepassword-connect.secret.sops.yaml b/kubernetes/apps/external-secrets/external-secrets/app/onepassword-connect.secret.sops.yaml new file mode 100644 index 00000000..40f873e5 --- /dev/null +++ b/kubernetes/apps/external-secrets/external-secrets/app/onepassword-connect.secret.sops.yaml @@ -0,0 +1,28 @@ +# yamllint disable +apiVersion: v1 +kind: Secret +metadata: + name: onepassword-connect-secret +stringData: + 1password-credentials.json: ENC[AES256_GCM,data:b1M1F133xoEeInGFZL0Gv+atGJfv8yLJhsOS2tvhdYmzcZO6Zt+5gl7PTQZNkbsdJqCTFfL8UyusElHH1pFOOA6SQGKPdCl37x/9DAvZ8dw2JpS5zRp8wi0VDEGSiv1U62HXi30ami+71rAI2DYGZUZJ3EKINsY6k9SEf1kARxlhJQ6/ilGsmCD1oogOJjkWWLz73Jp7pVW8jarFXRxIdG0ucy53smzuZ5GMzv5XZFcFXL75M4Y72IqMLdlLbM3q78G1o8k3pqX7uZ5ZSAMH2nvPLh9b8n/S28dTlV6C8KQzEqXy5Rp0lrOv2NuK8atLyy0IQsMp0dzGgly5Y2LOlW16be2JZ2IRb/AOZ6SJxsMjZj/d2dwsvCocp+vEd6LPC9atr5/6MDn1nV/WuHSjACcIHHFHflOX4tWvW0tPqDLa3up0h3z5omwSCXwVMXkK80hMD0F+T5iC/eDyMeeFGNYxpQuYBp/LoAxdVhemetIBQRBJwjfGHutrk9OpNs063sbhUNSKFLx0ZdevazbU/DyO3DTooO46jNhYfuIzuTPJpiKjjDKbXcX+bXUC96GE/zZR0QLcuDHLVaOPYFF9oXbxTW8rbJDbTy6NTDoXqhVeqNGIOLQdX/0eyuoo3jWSAMtCYps8Tv7cufJYpnscPT4adkBrBetHHPd4H5Q8/4JxceVnf852PTaUBT8OeB7tAI7/Dj2JfOi3RKqZGBRQx1LJ8bQz2aA2b6oiYIPwquTUKLp1UtO4CWGCN0EtkRwm5G6r53d1jLNnUV5eNJ1sk0WTluDQpxRgQcXDrW7jEgGNbWcZniJk/IiL43FBy5YH4JRotGJZTO043VHU+i6kXoFbY8kQ8Dv018u6NlEYGuSEkOdb3gJYzXRS0dt+65tR5+ZtcnJzK6usv+rwpvUm3e6fV5KeGvSb4P6GRwAZesW+vC7XzQ6EF5s8yQlbrum/ar8+oo7WIf+D51REvrEBLj1Dm5LxicQqFiI6HqfIq0DTY5jTEykBDn6RP+29zl0AEulgv5Kp8hmtrXfDSwvmakhHy88ERyFSzP+JKqD9rl8CCLkCLnshJBcJAqij7G1tsfrTiXNdaX4aSsUB/Ld5ufq9tt6ovi4yI3NuNBJ5o5ym8FjI5Y+LdfTbH/qLMwO08kXMGU6CrYk4KGNjYpcJxi0AJ04Yhpxexnc2+ERyXVmBDIISfqnubh+LuzTQJ2L6oGnuSxvqXx1NmZevZoBnJQe7hAJU4RnkPKD86GA8XPOcmbeZmtXn3K/v2iKW03VqG7rHth0Uo373lmcnmBqcsfRJOrdULopcmZBQo+F83vY9mXEZ5TGt4QskKYglwH//MUDQ1hLordJtAmL0p+d2FUb3NOnpw3ngCwUFpJZiYonnQ3Uybau56zcqD/2zWUUciB9+u3hImRvTiqk3PDZ309CmR7BSt+Rosh/h1ufFCL3qnWDrFmyesdn7qeRv3Gvr/TybmTPi13Ti3RL6Wa2g3dMniLIEvVGPSapwmpipThZ3tPgISC+hHbrjwRyx7R2XTvVYcGigEeTJErWtSK92Sa+HWZt20OB1/0bOrqcpEcZFXQG2GEaYkzOeicRyOlGE8Wwk0TKGmCnaAGnOXa3MBLxq06LD/8n6y5KW86zgbzWN6GTIVTslNZRrZPzSrY9mXsJ/s0zDN9ffb+MLK95+Qwa8EHAy727qf8jbdXaO4BmZRBdqNzUgSVtJDLcBTP86jjz+smedMMr1s4ejC0rg65l7IGDseZAR7AdtPcAun5fS5JSGHyKxMUYVBYpJ3Dpw7najBg9zHcB/IC1//oOcmRdNLNOrGK7IFTsrhKq7LzGj5Easy6wu8plumGPvE+5CJVQzMtrMJNJkBwCEaxF/MxfCEyP/YZzJDGhwCOuPEA+5Va0VQi4rIENrpFiELT3di6t88if0+xE=,iv:TeTAXTppPkhOMcF+h4t8Zbfz5pgAbEgvBLCiPVpp8PE=,tag:Br9vGQOu17Y2TNMqB+PdMQ==,type:str] + token: ENC[AES256_GCM,data:s9Ke8hrDLUfExiQKAGCFE6xL8jWBhqejiCB3iiTmQ8tZ70o2lZwA3vUpALiwLsH6gkHAWSu7FbM11kMChdLFY23yrQ4p03eLK5Oe7xZTvhwZiXMH9kpDt61hXEdA6kcrcR75OTC4wzHxlxq8rVuQ48PEZPUErWw1EgauLF/sMWnttQOgvQndBa/PDoVL0McKzWBydPMO2Duhwe5v1WO5h7JSHGSQyI/Ddf+5bejwNFbh02wdJYyAkzRq17ECSwS1J9vUCUHs7Ack1IiebFVf8CFb+e9FnnWsaT50quhKm1yMfyVUDH41+UtScxepmeX3zPjNMLfrlcNTiWj+h0bPr4ZGYK+TQqhyVtIf91G8Omv8HjfG3og90QiQ7qpU+VfKP72x/vN7ySAC3hWLBj9yZJCZtFSghuw2fmbPZPUTDZBzFpSPKAdw+LmWfz1EmpgqqQK88JdEuhvHCPk+8ERokFCICkL2oFqZfHR8I3pJATUTOV3Pi/Pfy40seG3ilofdYCR20EpDMgVUK9EuGemc7VAx0RtGRkbQUaOAsu2/PmFju1owrDK2JBv0ouLdhUluIr1oUSPcVo9Pq4EsOUd+9/7+1GEFyqwOMDO9eZ0IxAHQH82PcYMhoBAJLanZVjlU7Aqp3DQv/OO/iOiBLmPZ8eQjNCLgjKKf90gSRR2qiQpfC/9xX6aKuMROL2RODLAnVixhWbcC7uXrlTWWndzkhCzr6IMyT52DiehpDPJ6jRSl4KKrf6NbuvB0XuhfK+OgbN8/Kjyacx8y7ToQgSF/H7vbhNvok9Ib6FlZo6Pq6kXbjtqOsQ6eRbKcQ0k7kBf2PFj03gYRjCVg8t2Rv4HZtDyW,iv:E2+tWKberC+zPnllwResWdCL7xY7RcfpQhOvKazWxYw=,tag:edoa/jOkaqKiOGIMIr4bnw==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKWkdqL0VhNTNUUkgxNExL + Qk9rZThFZFNTQmZIc3VVZG5HNGgyMmhkQmpRClVkZDk1WkRCV0l4MUhTRVVoemJM + Q1ltdGllV0VPTzA2bTh4NTBobW5sQncKLS0tIG5rMmlQTTZ5cVkvQ2h3L0hCYURy + L3dJcnh3ZmxvKzlQUGtFKy9hc2U2MEkK15zbBy2Q/TKP6io1Ubuj8NIQBG0mawhG + edYrl4XIG94DFsUHiBkjW4Ef3XHjoFOIIqjCmbrKyNzHv4lEK+LoJg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-04-23T22:20:53Z" + mac: ENC[AES256_GCM,data:2OAPQUX5yEl4VnVyR2wn65Lq4nDqB57Di7FjSqojujqVU1fYtqBTLlVaNqbBRS6ZFuqkzPbUFBl2KWA9SAJsMjJ2zXkJQ9ch0pB/NCN/j/AlKvDAfhvnJtbKEEd8bWVwZ+AIOJ5ophSDp1ufMv8fanq9Wee+SOwxTrYxH4N/u6k=,iv:20yAJAIB8VYlO8YA818zWFTDBxbvsGcaeMADpufWm3o=,tag:au54g+6PEq1eYZxRCdDlSQ==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.8.1 diff --git a/kubernetes/apps/external-secrets/external-secrets/ks.yaml b/kubernetes/apps/external-secrets/external-secrets/ks.yaml new file mode 100644 index 00000000..8dd4a51b --- /dev/null +++ b/kubernetes/apps/external-secrets/external-secrets/ks.yaml @@ -0,0 +1,44 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app external-secrets + namespace: flux-system +spec: + targetNamespace: external-secrets + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/external-secrets/external-secrets/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app external-secrets-stores + namespace: flux-system +spec: + targetNamespace: external-secrets + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets + path: ./kubernetes/apps/external-secrets/external-secrets/store + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/external-secrets/external-secrets/store/clustersecretstore.yaml b/kubernetes/apps/external-secrets/external-secrets/store/clustersecretstore.yaml new file mode 100644 index 00000000..18b8222c --- /dev/null +++ b/kubernetes/apps/external-secrets/external-secrets/store/clustersecretstore.yaml @@ -0,0 +1,18 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/clustersecretstore_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ClusterSecretStore +metadata: + name: onepassword-connect +spec: + provider: + onepassword: + connectHost: http://onepassword-connect.external-secrets.svc.cluster.local + vaults: + homelab: 1 + auth: + secretRef: + connectTokenSecretRef: + name: onepassword-connect-secret + key: token + namespace: external-secrets diff --git a/kubernetes/apps/external-secrets/external-secrets/store/helmrelease.yaml b/kubernetes/apps/external-secrets/external-secrets/store/helmrelease.yaml new file mode 100644 index 00000000..de507d18 --- /dev/null +++ b/kubernetes/apps/external-secrets/external-secrets/store/helmrelease.yaml @@ -0,0 +1,137 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: onepassword-connect +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + values: + controllers: + onepassword-connect: + strategy: RollingUpdate + annotations: + reloader.stakater.com/auto: "true" + containers: + api: + image: + repository: docker.io/1password/connect-api + tag: 1.7.2@sha256:6aa94cf713f99c0fa58c12ffdd1b160404b4c13a7f501a73a791aa84b608c5a1 + env: + XDG_DATA_HOME: &configDir /config + OP_HTTP_PORT: &apiPort 80 + OP_BUS_PORT: 11220 + OP_BUS_PEERS: localhost:11221 + OP_SESSION: + valueFrom: + secretKeyRef: + name: onepassword-connect-secret + key: 1password-credentials.json + probes: + liveness: + enabled: true + custom: true + spec: + httpGet: + path: /heartbeat + port: *apiPort + initialDelaySeconds: 15 + periodSeconds: 30 + failureThreshold: 3 + readiness: + enabled: true + custom: true + spec: + httpGet: + path: /health + port: *apiPort + initialDelaySeconds: 15 + securityContext: &securityContext + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + resources: &resources + requests: + cpu: 10m + limits: + memory: 256M + sync: + image: + repository: docker.io/1password/connect-sync + tag: 1.7.2@sha256:fe527ed9d81f193d8dfbba4140d61f9e8c8dceb0966b3009259087504e5ff79c + env: + XDG_DATA_HOME: *configDir + OP_HTTP_PORT: &syncPort 8081 + OP_BUS_PORT: 11221 + OP_BUS_PEERS: localhost:11220 + OP_SESSION: + valueFrom: + secretKeyRef: + name: onepassword-connect-secret + key: 1password-credentials.json + probes: + liveness: + enabled: true + custom: true + spec: + httpGet: + path: /heartbeat + port: *syncPort + initialDelaySeconds: 15 + periodSeconds: 30 + failureThreshold: 3 + readiness: + enabled: true + custom: true + spec: + httpGet: + path: /health + port: *syncPort + initialDelaySeconds: 15 + securityContext: *securityContext + resources: *resources + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: { type: RuntimeDefault } + service: + app: + controller: onepassword-connect + ports: + http: + port: *apiPort + ingress: + app: + className: internal + hosts: + - host: "{{ .Release.Name }}.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + persistence: + config: + type: emptyDir + globalMounts: + - path: *configDir diff --git a/kubernetes/apps/external-secrets/external-secrets/store/kustomization.yaml b/kubernetes/apps/external-secrets/external-secrets/store/kustomization.yaml new file mode 100644 index 00000000..65df29ef --- /dev/null +++ b/kubernetes/apps/external-secrets/external-secrets/store/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./clustersecretstore.yaml diff --git a/kubernetes/apps/external-secrets/kustomization.yaml b/kubernetes/apps/external-secrets/kustomization.yaml new file mode 100644 index 00000000..8b5a7e34 --- /dev/null +++ b/kubernetes/apps/external-secrets/kustomization.yaml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + # Pre Flux-Kustomizations + - ./namespace.yaml + # Flux-Kustomizations + - ./external-secrets/ks.yaml diff --git a/kubernetes/apps/external-secrets/namespace.yaml b/kubernetes/apps/external-secrets/namespace.yaml new file mode 100644 index 00000000..26718c2a --- /dev/null +++ b/kubernetes/apps/external-secrets/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: external-secrets + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/flux-system/addons/app/kustomization.yaml b/kubernetes/apps/flux-system/addons/app/kustomization.yaml new file mode 100644 index 00000000..7b48edcd --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./monitoring + - ./webhooks diff --git a/kubernetes/apps/flux-system/addons/app/monitoring/kustomization.yaml b/kubernetes/apps/flux-system/addons/app/monitoring/kustomization.yaml new file mode 100644 index 00000000..247c0374 --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/monitoring/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: flux-system +resources: + - ./podmonitor.yaml + - ./prometheusrule.yaml diff --git a/kubernetes/apps/flux-system/addons/app/monitoring/podmonitor.yaml b/kubernetes/apps/flux-system/addons/app/monitoring/podmonitor.yaml new file mode 100644 index 00000000..8d09c127 --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/monitoring/podmonitor.yaml @@ -0,0 +1,32 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/podmonitor_v1.json +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: flux-system + namespace: flux-system + labels: + app.kubernetes.io/part-of: flux + app.kubernetes.io/component: monitoring +spec: + namespaceSelector: + matchNames: + - flux-system + selector: + matchExpressions: + - key: app + operator: In + values: + - helm-controller + - source-controller + - kustomize-controller + - notification-controller + - image-automation-controller + - image-reflector-controller + podMetricsEndpoints: + - port: http-prom + relabelings: + # Ref: https://github.com/prometheus-operator/prometheus-operator/issues/4816 + - sourceLabels: [__meta_kubernetes_pod_phase] + action: keep + regex: Running diff --git a/kubernetes/apps/flux-system/addons/app/monitoring/prometheusrule.yaml b/kubernetes/apps/flux-system/addons/app/monitoring/prometheusrule.yaml new file mode 100644 index 00000000..4257e56d --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/monitoring/prometheusrule.yaml @@ -0,0 +1,32 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/prometheusrule_v1.json +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: flux-rules + namespace: flux-system +spec: + groups: + - name: flux.rules + rules: + - alert: FluxComponentAbsent + annotations: + summary: Flux component has disappeared from Prometheus target discovery. + expr: | + absent(up{job=~".*flux-system.*"} == 1) + for: 15m + labels: + severity: critical + - alert: FluxReconciliationFailure + annotations: + summary: >- + {{ $labels.kind }} {{ $labels.namespace }}/{{ $labels.name }} reconciliation + has been failing for more than 15 minutes. + expr: | + max(gotk_reconcile_condition{status="False",type="Ready"}) by (namespace, name, kind) + + + on(namespace, name, kind) (max(gotk_reconcile_condition{status="Deleted"}) + by (namespace, name, kind)) * 2 == 1 + for: 15m + labels: + severity: critical diff --git a/kubernetes/apps/flux-system/addons/app/webhooks/github/ingress.yaml b/kubernetes/apps/flux-system/addons/app/webhooks/github/ingress.yaml new file mode 100644 index 00000000..e20604f0 --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/webhooks/github/ingress.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: flux-webhook + annotations: + external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}" +spec: + ingressClassName: external + rules: + - host: "flux-webhook.${SECRET_DOMAIN}" + http: + paths: + - path: /hook/ + pathType: Prefix + backend: + service: + name: webhook-receiver + port: + number: 80 diff --git a/kubernetes/apps/flux-system/addons/app/webhooks/github/kustomization.yaml b/kubernetes/apps/flux-system/addons/app/webhooks/github/kustomization.yaml new file mode 100644 index 00000000..5461805c --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/webhooks/github/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./secret.sops.yaml + - ./ingress.yaml + - ./receiver.yaml diff --git a/kubernetes/apps/flux-system/addons/app/webhooks/github/receiver.yaml b/kubernetes/apps/flux-system/addons/app/webhooks/github/receiver.yaml new file mode 100644 index 00000000..cca5931b --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/webhooks/github/receiver.yaml @@ -0,0 +1,25 @@ +--- +apiVersion: notification.toolkit.fluxcd.io/v1 +kind: Receiver +metadata: + name: github-receiver +spec: + type: github + events: + - ping + - push + secretRef: + name: github-webhook-token-secret + resources: + - apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + name: home-kubernetes + namespace: flux-system + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + name: cluster + namespace: flux-system + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + name: cluster-apps + namespace: flux-system diff --git a/kubernetes/apps/flux-system/addons/app/webhooks/github/secret.sops.yaml b/kubernetes/apps/flux-system/addons/app/webhooks/github/secret.sops.yaml new file mode 100644 index 00000000..520439cc --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/webhooks/github/secret.sops.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: github-webhook-token-secret +stringData: + token: ENC[AES256_GCM,data:cmOlej/6DM4ZPp4rwwJi/77Xde4z1cpN,iv:sVqWqBUqMh3hDSLlYwMqCM+53SmBdwelBObn2VfCz6A=,tag:7GRWcZhdAPuD+LMQEhn9lw==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWVUxkY3ZIRVVuYzhPU0s5 + K3VkQy9hTytxM3lFallMLzJvYnRDMS9MWmhrCm5JU3FMd3U5aGk0NDlhcUc3SDhm + VUFLTnJDUTdQLzVQdjB5MElHRmdJdjAKLS0tIHVFT2o0MVZkNGlqTWdrU3B6YXJR + R3RUV2Q1OXBFZ3gvdHk4SWkwVEhxZFkKkKlAIGgJEI8Vfkxo2Syhe3LV1/wXJQKU + Vh/tZjp/L1gbj3GA4Yunx/wQS/9RNJU7CI5+OV57Lk+NKI7/pVS+DQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-02-17T21:45:25Z" + mac: ENC[AES256_GCM,data:WTPjGJeMPdQZaRrjPL2VQqNzKkDsMd0Sk7ChgwiE4QPoEx8RZnHlwUk7QqNeAN84y7JfQFXDefJAJq6A4vSRlpZAAa1Rc/8N4n+gwSmOXVKNsX5e0dBkVYDS89gHdip164+Nr6NwEO5f7FbcI0vxNf7Lz0Zot5OIJPSX+LPe8CE=,iv:BdQgo/ATSuCPkpcrAlXKWFv8qWo8Z946IA5vnP6FrRo=,tag:zkil/5Q3ni3lU4YZ3+Jwkw==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.7.3 diff --git a/kubernetes/apps/flux-system/addons/app/webhooks/kustomization.yaml b/kubernetes/apps/flux-system/addons/app/webhooks/kustomization.yaml new file mode 100644 index 00000000..08c1780f --- /dev/null +++ b/kubernetes/apps/flux-system/addons/app/webhooks/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./github diff --git a/kubernetes/apps/flux-system/addons/ks.yaml b/kubernetes/apps/flux-system/addons/ks.yaml new file mode 100644 index 00000000..f8f0746e --- /dev/null +++ b/kubernetes/apps/flux-system/addons/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app flux-addons + namespace: flux-system +spec: + targetNamespace: flux-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/flux-system/addons/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/flux-system/alert.yaml b/kubernetes/apps/flux-system/alert.yaml new file mode 100644 index 00000000..0168beb6 --- /dev/null +++ b/kubernetes/apps/flux-system/alert.yaml @@ -0,0 +1,38 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: flux-system +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: flux-system +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: GitRepository + name: "*" + - kind: HelmRelease + name: "*" + - kind: HelmRepository + name: "*" + - kind: Kustomization + name: "*" + - kind: OCIRepository + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/flux-system/kustomization.yaml b/kubernetes/apps/flux-system/kustomization.yaml new file mode 100644 index 00000000..041136f0 --- /dev/null +++ b/kubernetes/apps/flux-system/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./addons/ks.yaml diff --git a/kubernetes/apps/flux-system/namespace.yaml b/kubernetes/apps/flux-system/namespace.yaml new file mode 100644 index 00000000..b48db452 --- /dev/null +++ b/kubernetes/apps/flux-system/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: flux-system + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/kube-system/alert.yaml b/kubernetes/apps/kube-system/alert.yaml new file mode 100644 index 00000000..33a50f33 --- /dev/null +++ b/kubernetes/apps/kube-system/alert.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: kube-system +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: kube-system +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: HelmRelease + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/kube-system/cilium/app/helm-values.yaml b/kubernetes/apps/kube-system/cilium/app/helm-values.yaml new file mode 100644 index 00000000..944dc39e --- /dev/null +++ b/kubernetes/apps/kube-system/cilium/app/helm-values.yaml @@ -0,0 +1,57 @@ +--- +autoDirectNodeRoutes: true +bpf: + masquerade: false +cgroup: + automount: + enabled: false + hostRoot: /sys/fs/cgroup +cluster: + id: 1 + name: home-kubernetes +cni: + exclusive: false +containerRuntime: + integration: containerd +# NOTE: devices might need to be set if you have more than one active NIC on your hosts +# devices: eno+ eth+ +endpointRoutes: + enabled: true +hubble: + enabled: false +ipam: + mode: kubernetes +ipv4NativeRoutingCIDR: "10.69.0.0/16" +k8sServiceHost: 127.0.0.1 +k8sServicePort: 7445 +kubeProxyReplacement: true +kubeProxyReplacementHealthzBindAddr: 0.0.0.0:10256 +l2announcements: + enabled: true +loadBalancer: + algorithm: maglev + mode: snat +localRedirectPolicy: true +operator: + replicas: 1 + rollOutPods: true +rollOutCiliumPods: true +routingMode: native +securityContext: + capabilities: + ciliumAgent: + - CHOWN + - KILL + - NET_ADMIN + - NET_RAW + - IPC_LOCK + - SYS_ADMIN + - SYS_RESOURCE + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + cleanCiliumState: + - NET_ADMIN + - SYS_ADMIN + - SYS_RESOURCE diff --git a/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml b/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml new file mode 100644 index 00000000..b1e0bcab --- /dev/null +++ b/kubernetes/apps/kube-system/cilium/app/helmrelease.yaml @@ -0,0 +1,81 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cilium +spec: + interval: 30m + chart: + spec: + chart: cilium + version: 1.15.5 + sourceRef: + kind: HelmRepository + name: cilium + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + valuesFrom: + - kind: ConfigMap + name: cilium-helm-values + values: + hubble: + enabled: true + metrics: + enabled: + - dns:query + - drop + - tcp + - flow + - port-distribution + - icmp + - http + serviceMonitor: + enabled: true + dashboards: + enabled: true + annotations: + grafana_folder: Cilium + relay: + enabled: true + rollOutPods: true + prometheus: + serviceMonitor: + enabled: true + ui: + enabled: true + rollOutPods: true + ingress: + enabled: true + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: cilium.png + gethomepage.dev/name: Hubble + gethomepage.dev/group: Observability + gethomepage.dev/description: Network Monitoring Dashboard + hosts: ["hubble.${SECRET_DOMAIN}"] + operator: + prometheus: + enabled: true + serviceMonitor: + enabled: true + dashboards: + enabled: true + annotations: + grafana_folder: Cilium + prometheus: + enabled: true + serviceMonitor: + enabled: true + trustCRDsExist: true + dashboards: + enabled: true + annotations: + grafana_folder: Cilium diff --git a/kubernetes/apps/kube-system/cilium/app/kustomization.yaml b/kubernetes/apps/kube-system/cilium/app/kustomization.yaml new file mode 100644 index 00000000..25781ef1 --- /dev/null +++ b/kubernetes/apps/kube-system/cilium/app/kustomization.yaml @@ -0,0 +1,12 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml +configMapGenerator: + - name: cilium-helm-values + files: + - values.yaml=./helm-values.yaml +configurations: + - kustomizeconfig.yaml diff --git a/kubernetes/apps/kube-system/cilium/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/cilium/app/kustomizeconfig.yaml new file mode 100644 index 00000000..58f92ba1 --- /dev/null +++ b/kubernetes/apps/kube-system/cilium/app/kustomizeconfig.yaml @@ -0,0 +1,7 @@ +--- +nameReference: + - kind: ConfigMap + version: v1 + fieldSpecs: + - path: spec/valuesFrom/name + kind: HelmRelease diff --git a/kubernetes/apps/kube-system/cilium/config/cilium-l2.yaml b/kubernetes/apps/kube-system/cilium/config/cilium-l2.yaml new file mode 100644 index 00000000..3cdeaa7d --- /dev/null +++ b/kubernetes/apps/kube-system/cilium/config/cilium-l2.yaml @@ -0,0 +1,24 @@ +--- +# https://docs.cilium.io/en/latest/network/l2-announcements +apiVersion: cilium.io/v2alpha1 +kind: CiliumL2AnnouncementPolicy +metadata: + name: l2-policy +spec: + loadBalancerIPs: true + # NOTE: interfaces might need to be set if you have more than one active NIC on your hosts + # interfaces: + # - ^eno[0-9]+ + # - ^eth[0-9]+ + nodeSelector: + matchLabels: + kubernetes.io/os: linux +--- +apiVersion: cilium.io/v2alpha1 +kind: CiliumLoadBalancerIPPool +metadata: + name: l2-pool +spec: + allowFirstLastIPs: "Yes" + blocks: + - cidr: "192.168.20.0/24" diff --git a/kubernetes/apps/kube-system/cilium/config/kustomization.yaml b/kubernetes/apps/kube-system/cilium/config/kustomization.yaml new file mode 100644 index 00000000..b0ecf0d1 --- /dev/null +++ b/kubernetes/apps/kube-system/cilium/config/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./cilium-l2.yaml diff --git a/kubernetes/apps/kube-system/cilium/ks.yaml b/kubernetes/apps/kube-system/cilium/ks.yaml new file mode 100644 index 00000000..36194e8b --- /dev/null +++ b/kubernetes/apps/kube-system/cilium/ks.yaml @@ -0,0 +1,44 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cilium + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/cilium/app + prune: false # never should be deleted + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cilium-config + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: cilium + path: ./kubernetes/apps/kube-system/cilium/config + prune: false # never should be deleted + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/kube-system/coredns/app/helm-values.yaml b/kubernetes/apps/kube-system/coredns/app/helm-values.yaml new file mode 100644 index 00000000..22da0298 --- /dev/null +++ b/kubernetes/apps/kube-system/coredns/app/helm-values.yaml @@ -0,0 +1,50 @@ +--- +fullnameOverride: coredns +k8sAppLabelOverride: kube-dns +serviceAccount: + create: true +service: + name: kube-dns + clusterIP: "10.96.0.10" +servers: + - zones: + - zone: . + scheme: dns:// + use_tcp: true + port: 53 + plugins: + - name: errors + - name: health + configBlock: |- + lameduck 5s + - name: ready + - name: log + configBlock: |- + class error + - name: prometheus + parameters: 0.0.0.0:9153 + - name: kubernetes + parameters: cluster.local in-addr.arpa ip6.arpa + configBlock: |- + pods insecure + fallthrough in-addr.arpa ip6.arpa + - name: forward + parameters: . /etc/resolv.conf + - name: cache + parameters: 30 + - name: loop + - name: reload + - name: loadbalance +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: Exists +tolerations: + - key: CriticalAddonsOnly + operator: Exists + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule diff --git a/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml b/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml new file mode 100644 index 00000000..85fd31e3 --- /dev/null +++ b/kubernetes/apps/kube-system/coredns/app/helmrelease.yaml @@ -0,0 +1,27 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: coredns +spec: + interval: 30m + chart: + spec: + chart: coredns + version: 1.29.0 + sourceRef: + kind: HelmRepository + name: coredns + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + valuesFrom: + - kind: ConfigMap + name: coredns-helm-values diff --git a/kubernetes/apps/kube-system/coredns/app/kustomization.yaml b/kubernetes/apps/kube-system/coredns/app/kustomization.yaml new file mode 100644 index 00000000..39444bbd --- /dev/null +++ b/kubernetes/apps/kube-system/coredns/app/kustomization.yaml @@ -0,0 +1,12 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml +configMapGenerator: + - name: coredns-helm-values + files: + - values.yaml=./helm-values.yaml +configurations: + - kustomizeconfig.yaml diff --git a/kubernetes/apps/kube-system/coredns/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/coredns/app/kustomizeconfig.yaml new file mode 100644 index 00000000..58f92ba1 --- /dev/null +++ b/kubernetes/apps/kube-system/coredns/app/kustomizeconfig.yaml @@ -0,0 +1,7 @@ +--- +nameReference: + - kind: ConfigMap + version: v1 + fieldSpecs: + - path: spec/valuesFrom/name + kind: HelmRelease diff --git a/kubernetes/apps/kube-system/coredns/ks.yaml b/kubernetes/apps/kube-system/coredns/ks.yaml new file mode 100644 index 00000000..766a6c07 --- /dev/null +++ b/kubernetes/apps/kube-system/coredns/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app coredns + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/coredns/app + prune: false # never should be deleted + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/kube-system/intel-device-plugin/app/helmrelease.yaml b/kubernetes/apps/kube-system/intel-device-plugin/app/helmrelease.yaml new file mode 100644 index 00000000..359bca29 --- /dev/null +++ b/kubernetes/apps/kube-system/intel-device-plugin/app/helmrelease.yaml @@ -0,0 +1,28 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: intel-device-plugin-operator +spec: + interval: 30m + chart: + spec: + chart: intel-device-plugins-operator + version: 0.30.0 + sourceRef: + kind: HelmRepository + name: intel + namespace: flux-system + install: + crds: CreateReplace + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + crds: CreateReplace + remediation: + retries: 3 + dependsOn: + - name: node-feature-discovery + namespace: kube-system diff --git a/kubernetes/apps/kube-system/intel-device-plugin/app/kustomization.yaml b/kubernetes/apps/kube-system/intel-device-plugin/app/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/kube-system/intel-device-plugin/app/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/kube-system/intel-device-plugin/gpu/helmrelease.yaml b/kubernetes/apps/kube-system/intel-device-plugin/gpu/helmrelease.yaml new file mode 100644 index 00000000..66c53af3 --- /dev/null +++ b/kubernetes/apps/kube-system/intel-device-plugin/gpu/helmrelease.yaml @@ -0,0 +1,30 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: intel-device-plugin-gpu +spec: + interval: 30m + chart: + spec: + chart: intel-device-plugins-gpu + version: 0.30.0 + sourceRef: + kind: HelmRepository + name: intel + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + dependsOn: + - name: intel-device-plugin-operator + namespace: kube-system + values: + name: intel-gpu-plugin + sharedDevNum: 3 + nodeFeatureRule: false diff --git a/kubernetes/apps/kube-system/intel-device-plugin/gpu/kustomization.yaml b/kubernetes/apps/kube-system/intel-device-plugin/gpu/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/kube-system/intel-device-plugin/gpu/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/kube-system/intel-device-plugin/ks.yaml b/kubernetes/apps/kube-system/intel-device-plugin/ks.yaml new file mode 100644 index 00000000..4371f0b5 --- /dev/null +++ b/kubernetes/apps/kube-system/intel-device-plugin/ks.yaml @@ -0,0 +1,42 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app intel-device-plugin + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/intel-device-plugin/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app intel-device-plugin-gpu + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/intel-device-plugin/gpu + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/helm-values.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helm-values.yaml new file mode 100644 index 00000000..00ad772e --- /dev/null +++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helm-values.yaml @@ -0,0 +1,3 @@ +--- +providerRegex: ^k8s-control-\d$ +bypassDnsResolution: true diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml new file mode 100644 index 00000000..2947713b --- /dev/null +++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/helmrelease.yaml @@ -0,0 +1,31 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: kubelet-csr-approver +spec: + interval: 30m + chart: + spec: + chart: kubelet-csr-approver + version: 1.2.1 + sourceRef: + kind: HelmRepository + name: postfinance + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + valuesFrom: + - kind: ConfigMap + name: kubelet-csr-approver-helm-values + values: + metrics: + enable: true + serviceMonitor: + enabled: true diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml new file mode 100644 index 00000000..16074ce8 --- /dev/null +++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomization.yaml @@ -0,0 +1,12 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml +configMapGenerator: + - name: kubelet-csr-approver-helm-values + files: + - values.yaml=./helm-values.yaml +configurations: + - kustomizeconfig.yaml diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomizeconfig.yaml new file mode 100644 index 00000000..58f92ba1 --- /dev/null +++ b/kubernetes/apps/kube-system/kubelet-csr-approver/app/kustomizeconfig.yaml @@ -0,0 +1,7 @@ +--- +nameReference: + - kind: ConfigMap + version: v1 + fieldSpecs: + - path: spec/valuesFrom/name + kind: HelmRelease diff --git a/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml b/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml new file mode 100644 index 00000000..f43156a8 --- /dev/null +++ b/kubernetes/apps/kube-system/kubelet-csr-approver/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app kubelet-csr-approver + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/kubelet-csr-approver/app + prune: false # never should be deleted + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/kube-system/kustomization.yaml b/kubernetes/apps/kube-system/kustomization.yaml new file mode 100644 index 00000000..6c816831 --- /dev/null +++ b/kubernetes/apps/kube-system/kustomization.yaml @@ -0,0 +1,15 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./cilium/ks.yaml + - ./coredns/ks.yaml + - ./metrics-server/ks.yaml + - ./reloader/ks.yaml + - ./spegel/ks.yaml + - ./kubelet-csr-approver/ks.yaml + - ./node-feature-discovery/ks.yaml + - ./intel-device-plugin/ks.yaml diff --git a/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml b/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml new file mode 100644 index 00000000..60b8fdf9 --- /dev/null +++ b/kubernetes/apps/kube-system/metrics-server/app/helmrelease.yaml @@ -0,0 +1,32 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: metrics-server +spec: + interval: 30m + chart: + spec: + chart: metrics-server + version: 3.12.1 + sourceRef: + kind: HelmRepository + name: metrics-server + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + args: + - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname + - --kubelet-use-node-status-port + - --metric-resolution=15s + metrics: + enabled: true + serviceMonitor: + enabled: true diff --git a/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml b/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/kube-system/metrics-server/app/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/kube-system/metrics-server/ks.yaml b/kubernetes/apps/kube-system/metrics-server/ks.yaml new file mode 100644 index 00000000..6a21d99c --- /dev/null +++ b/kubernetes/apps/kube-system/metrics-server/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app metrics-server + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/metrics-server/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/kube-system/namespace.yaml b/kubernetes/apps/kube-system/namespace.yaml new file mode 100644 index 00000000..5eeb2c91 --- /dev/null +++ b/kubernetes/apps/kube-system/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kube-system + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/kube-system/node-feature-discovery/app/helmrelease.yaml b/kubernetes/apps/kube-system/node-feature-discovery/app/helmrelease.yaml new file mode 100644 index 00000000..ff177a02 --- /dev/null +++ b/kubernetes/apps/kube-system/node-feature-discovery/app/helmrelease.yaml @@ -0,0 +1,32 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: node-feature-discovery +spec: + interval: 30m + chart: + spec: + chart: node-feature-discovery + version: 0.15.4 + sourceRef: + kind: HelmRepository + name: node-feature-discovery + namespace: flux-system + install: + crds: CreateReplace + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + crds: CreateReplace + remediation: + retries: 3 + values: + worker: + config: + core: + sources: ["custom", "pci"] + prometheus: + enable: true diff --git a/kubernetes/apps/kube-system/node-feature-discovery/app/kustomization.yaml b/kubernetes/apps/kube-system/node-feature-discovery/app/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/kube-system/node-feature-discovery/app/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/kube-system/node-feature-discovery/ks.yaml b/kubernetes/apps/kube-system/node-feature-discovery/ks.yaml new file mode 100644 index 00000000..c4e859d1 --- /dev/null +++ b/kubernetes/apps/kube-system/node-feature-discovery/ks.yaml @@ -0,0 +1,44 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app node-feature-discovery + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/node-feature-discovery/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app node-feature-discovery-rules + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: node-feature-discovery + path: ./kubernetes/apps/kube-system/node-feature-discovery/rules + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/kube-system/node-feature-discovery/rules/intel-gpu-device.yaml b/kubernetes/apps/kube-system/node-feature-discovery/rules/intel-gpu-device.yaml new file mode 100644 index 00000000..865b9548 --- /dev/null +++ b/kubernetes/apps/kube-system/node-feature-discovery/rules/intel-gpu-device.yaml @@ -0,0 +1,17 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/nfd.k8s-sigs.io/nodefeaturerule_v1alpha1.json +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: intel-gpu-device +spec: + rules: + - # Intel UHD Graphics 630 + name: intel.gpu + labels: + intel.feature.node.kubernetes.io/gpu: "true" + matchFeatures: + - feature: pci.device + matchExpressions: + class: { op: In, value: ["0300"] } + vendor: { op: In, value: ["8086"] } diff --git a/kubernetes/apps/kube-system/node-feature-discovery/rules/kustomization.yaml b/kubernetes/apps/kube-system/node-feature-discovery/rules/kustomization.yaml new file mode 100644 index 00000000..2f8dd39c --- /dev/null +++ b/kubernetes/apps/kube-system/node-feature-discovery/rules/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./intel-gpu-device.yaml diff --git a/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml b/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml new file mode 100644 index 00000000..e04152b0 --- /dev/null +++ b/kubernetes/apps/kube-system/reloader/app/helmrelease.yaml @@ -0,0 +1,30 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: reloader +spec: + interval: 30m + chart: + spec: + chart: reloader + version: 1.0.97 + sourceRef: + kind: HelmRepository + name: stakater + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + fullnameOverride: reloader + reloader: + readOnlyRootFileSystem: true + podMonitor: + enabled: true + namespace: "{{ .Release.Namespace }}" diff --git a/kubernetes/apps/kube-system/reloader/app/kustomization.yaml b/kubernetes/apps/kube-system/reloader/app/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/kube-system/reloader/app/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/kube-system/reloader/ks.yaml b/kubernetes/apps/kube-system/reloader/ks.yaml new file mode 100644 index 00000000..0aae5261 --- /dev/null +++ b/kubernetes/apps/kube-system/reloader/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app reloader + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/reloader/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/kube-system/spegel/app/helm-values.yaml b/kubernetes/apps/kube-system/spegel/app/helm-values.yaml new file mode 100644 index 00000000..a4185ae3 --- /dev/null +++ b/kubernetes/apps/kube-system/spegel/app/helm-values.yaml @@ -0,0 +1,7 @@ +--- +spegel: + containerdSock: /run/containerd/containerd.sock + containerdRegistryConfigPath: /etc/cri/conf.d/hosts +service: + registry: + hostPort: 29999 diff --git a/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml b/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml new file mode 100644 index 00000000..ae67dc7b --- /dev/null +++ b/kubernetes/apps/kube-system/spegel/app/helmrelease.yaml @@ -0,0 +1,30 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: spegel +spec: + interval: 30m + chart: + spec: + chart: spegel + version: v0.0.22 + sourceRef: + kind: HelmRepository + name: spegel + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + valuesFrom: + - kind: ConfigMap + name: spegel-helm-values + values: + serviceMonitor: + enabled: true diff --git a/kubernetes/apps/kube-system/spegel/app/kustomization.yaml b/kubernetes/apps/kube-system/spegel/app/kustomization.yaml new file mode 100644 index 00000000..8c7c0551 --- /dev/null +++ b/kubernetes/apps/kube-system/spegel/app/kustomization.yaml @@ -0,0 +1,12 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml +configMapGenerator: + - name: spegel-helm-values + files: + - values.yaml=./helm-values.yaml +configurations: + - kustomizeconfig.yaml diff --git a/kubernetes/apps/kube-system/spegel/app/kustomizeconfig.yaml b/kubernetes/apps/kube-system/spegel/app/kustomizeconfig.yaml new file mode 100644 index 00000000..58f92ba1 --- /dev/null +++ b/kubernetes/apps/kube-system/spegel/app/kustomizeconfig.yaml @@ -0,0 +1,7 @@ +--- +nameReference: + - kind: ConfigMap + version: v1 + fieldSpecs: + - path: spec/valuesFrom/name + kind: HelmRelease diff --git a/kubernetes/apps/kube-system/spegel/ks.yaml b/kubernetes/apps/kube-system/spegel/ks.yaml new file mode 100644 index 00000000..8f129bd6 --- /dev/null +++ b/kubernetes/apps/kube-system/spegel/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app spegel + namespace: flux-system +spec: + targetNamespace: kube-system + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/kube-system/spegel/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/network/alert.yaml b/kubernetes/apps/network/alert.yaml new file mode 100644 index 00000000..c5edd9d4 --- /dev/null +++ b/kubernetes/apps/network/alert.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: network +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: network +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: HelmRelease + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/network/cloudflared/app/configs/config.yaml b/kubernetes/apps/network/cloudflared/app/configs/config.yaml new file mode 100644 index 00000000..05bcef5c --- /dev/null +++ b/kubernetes/apps/network/cloudflared/app/configs/config.yaml @@ -0,0 +1,10 @@ +--- +originRequest: + originServerName: "external.${SECRET_DOMAIN}" + +ingress: + - hostname: "${SECRET_DOMAIN}" + service: https://ingress-nginx-external-controller.network.svc.cluster.local:443 + - hostname: "*.${SECRET_DOMAIN}" + service: https://ingress-nginx-external-controller.network.svc.cluster.local:443 + - service: http_status:404 diff --git a/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml b/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml new file mode 100644 index 00000000..d4252103 --- /dev/null +++ b/kubernetes/apps/network/cloudflared/app/dnsendpoint.yaml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/externaldns.k8s.io/dnsendpoint_v1alpha1.json +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: cloudflared +spec: + endpoints: + - dnsName: "external.${SECRET_DOMAIN}" + recordType: CNAME + targets: ["${SECRET_CLOUDFLARE_TUNNEL_ID}.cfargotunnel.com"] diff --git a/kubernetes/apps/network/cloudflared/app/externalsecret.yaml b/kubernetes/apps/network/cloudflared/app/externalsecret.yaml new file mode 100644 index 00000000..925ea7d1 --- /dev/null +++ b/kubernetes/apps/network/cloudflared/app/externalsecret.yaml @@ -0,0 +1,26 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/clustersecretstore_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: cloudflared-tunnel +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + refreshInterval: 15m + target: + name: cloudflared-tunnel-secret + template: + engineVersion: v2 + data: + TUNNEL_ID: "{{ .CLUSTER_CLOUDFLARE_TUNNEL_ID }}" + credentials.json: | + { + "AccountTag": "{{ .CLOUDFLARE_ACCOUNT_TAG }}", + "TunnelSecret": "{{ .CLOUDFLARE_TUNNEL_SECRET }}", + "TunnelID": "{{ .CLUSTER_CLOUDFLARE_TUNNEL_ID }}" + } + dataFrom: + - extract: + key: cloudflare diff --git a/kubernetes/apps/network/cloudflared/app/helmrelease.yaml b/kubernetes/apps/network/cloudflared/app/helmrelease.yaml new file mode 100644 index 00000000..9babcecc --- /dev/null +++ b/kubernetes/apps/network/cloudflared/app/helmrelease.yaml @@ -0,0 +1,116 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app cloudflared +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: nginx-external + namespace: network + values: + controllers: + cloudflared: + strategy: RollingUpdate + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: docker.io/cloudflare/cloudflared + tag: 2024.5.0@sha256:5d5f70a59d5e124d4a1a747769e0d27431861877860ca31deaad41b09726ca71 + env: + NO_AUTOUPDATE: true + TUNNEL_CRED_FILE: /etc/cloudflared/creds/credentials.json + TUNNEL_METRICS: 0.0.0.0:8080 + TUNNEL_ORIGIN_ENABLE_HTTP2: true + TUNNEL_TRANSPORT_PROTOCOL: quic + TUNNEL_POST_QUANTUM: true + args: + - tunnel + - --config + - /etc/cloudflared/config/config.yaml + - run + - "$(TUNNEL_ID)" + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /ready + port: &port 8080 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + resources: + requests: + cpu: 10m + limits: + memory: 256Mi + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + seccompProfile: { type: RuntimeDefault } + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: *app + service: + app: + controller: cloudflared + ports: + http: + port: *port + serviceMonitor: + app: + serviceName: cloudflared + endpoints: + - port: http + scheme: http + path: /metrics + interval: 1m + scrapeTimeout: 10s + persistence: + config: + type: configMap + name: cloudflared-configmap + globalMounts: + - path: /etc/cloudflared/config/config.yaml + subPath: config.yaml + readOnly: true + creds: + type: secret + name: cloudflared-tunnel-secret + globalMounts: + - path: /etc/cloudflared/creds/credentials.json + subPath: credentials.json + readOnly: true diff --git a/kubernetes/apps/network/cloudflared/app/kustomization.yaml b/kubernetes/apps/network/cloudflared/app/kustomization.yaml new file mode 100644 index 00000000..dae6e9ed --- /dev/null +++ b/kubernetes/apps/network/cloudflared/app/kustomization.yaml @@ -0,0 +1,14 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./dnsendpoint.yaml + - ./externalsecret.yaml + - ./helmrelease.yaml +configMapGenerator: + - name: cloudflared-configmap + files: + - ./configs/config.yaml +generatorOptions: + disableNameSuffixHash: true diff --git a/kubernetes/apps/network/cloudflared/ks.yaml b/kubernetes/apps/network/cloudflared/ks.yaml new file mode 100644 index 00000000..e3d26dc3 --- /dev/null +++ b/kubernetes/apps/network/cloudflared/ks.yaml @@ -0,0 +1,24 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app cloudflared + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-dns-cloudflare + - name: external-secrets-stores + path: ./kubernetes/apps/network/cloudflared/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/network/echo-server/app/helmrelease.yaml b/kubernetes/apps/network/echo-server/app/helmrelease.yaml new file mode 100644 index 00000000..bfa70629 --- /dev/null +++ b/kubernetes/apps/network/echo-server/app/helmrelease.yaml @@ -0,0 +1,92 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: echo-server +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + controllers: + echo-server: + strategy: RollingUpdate + containers: + app: + image: + repository: ghcr.io/mendhak/http-https-echo + tag: 33 + env: + HTTP_PORT: &port 8080 + LOG_WITHOUT_NEWLINE: true + LOG_IGNORE_PATH: /healthz + PROMETHEUS_ENABLED: true + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /healthz + port: *port + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + resources: + requests: + cpu: 10m + limits: + memory: 64Mi + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + seccompProfile: { type: RuntimeDefault } + service: + app: + controller: echo-server + ports: + http: + port: *port + serviceMonitor: + app: + serviceName: echo-server + endpoints: + - port: http + scheme: http + path: /metrics + interval: 1m + scrapeTimeout: 10s + ingress: + app: + className: external + annotations: + external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}" + hosts: + - host: "{{ .Release.Name }}.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http diff --git a/kubernetes/apps/network/echo-server/app/kustomization.yaml b/kubernetes/apps/network/echo-server/app/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/network/echo-server/app/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/network/echo-server/ks.yaml b/kubernetes/apps/network/echo-server/ks.yaml new file mode 100644 index 00000000..0cfc7559 --- /dev/null +++ b/kubernetes/apps/network/echo-server/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app echo-server + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/network/echo-server/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/network/external-dns/app/externalsecret.yaml b/kubernetes/apps/network/external-dns/app/externalsecret.yaml new file mode 100644 index 00000000..7dd4493d --- /dev/null +++ b/kubernetes/apps/network/external-dns/app/externalsecret.yaml @@ -0,0 +1,19 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: external-dns-cloudflare +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: external-dns-cloudflare-secret + template: + engineVersion: v2 + data: + CF_API_TOKEN: "{{ .CF_API_TOKEN }}" + dataFrom: + - extract: + key: cloudflare diff --git a/kubernetes/apps/network/external-dns/app/helmrelease.yaml b/kubernetes/apps/network/external-dns/app/helmrelease.yaml new file mode 100644 index 00000000..65919d35 --- /dev/null +++ b/kubernetes/apps/network/external-dns/app/helmrelease.yaml @@ -0,0 +1,50 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app external-dns +spec: + interval: 30m + chart: + spec: + chart: external-dns + version: 1.14.4 + sourceRef: + kind: HelmRepository + name: external-dns + namespace: flux-system + install: + crds: CreateReplace + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + crds: CreateReplace + remediation: + strategy: rollback + retries: 3 + values: + fullnameOverride: *app + provider: cloudflare + env: + - name: &name CF_API_TOKEN + valueFrom: + secretKeyRef: + name: &secret external-dns-cloudflare-secret + key: *name + extraArgs: + - --ingress-class=external + - --cloudflare-proxied + - --crd-source-apiversion=externaldns.k8s.io/v1alpha1 + - --crd-source-kind=DNSEndpoint + - --dry-run + policy: sync + sources: ["crd", "ingress"] + txtPrefix: k8s. + txtOwnerId: default + domainFilters: ["${SECRET_DOMAIN}"] + serviceMonitor: + enabled: true + podAnnotations: + secret.reloader.stakater.com/reload: *secret diff --git a/kubernetes/apps/network/external-dns/app/kustomization.yaml b/kubernetes/apps/network/external-dns/app/kustomization.yaml new file mode 100644 index 00000000..4eed917b --- /dev/null +++ b/kubernetes/apps/network/external-dns/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml diff --git a/kubernetes/apps/network/external-dns/ks.yaml b/kubernetes/apps/network/external-dns/ks.yaml new file mode 100644 index 00000000..9c6e788b --- /dev/null +++ b/kubernetes/apps/network/external-dns/ks.yaml @@ -0,0 +1,23 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app external-dns-cloudflare + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + path: ./kubernetes/apps/network/external-dns/app + prune: false + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/network/external-services/ks.yaml b/kubernetes/apps/network/external-services/ks.yaml new file mode 100644 index 00000000..fdb0cc58 --- /dev/null +++ b/kubernetes/apps/network/external-services/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app external-services + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/network/external-services/services + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/network/external-services/services/kustomization.yaml b/kubernetes/apps/network/external-services/services/kustomization.yaml new file mode 100644 index 00000000..2834df48 --- /dev/null +++ b/kubernetes/apps/network/external-services/services/kustomization.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./synology.yaml + - ./sprut.yaml + - ./proxmox.yaml + - ./minio.yaml + - ./pihole.yaml diff --git a/kubernetes/apps/network/external-services/services/minio.yaml b/kubernetes/apps/network/external-services/services/minio.yaml new file mode 100644 index 00000000..964b233b --- /dev/null +++ b/kubernetes/apps/network/external-services/services/minio.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: minio-external-service +spec: + type: ExternalName + externalName: "${NAS_URL}" +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: minio-ingress + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: "minio.png" + gethomepage.dev/name: Minio + gethomepage.dev/group: Storage + gethomepage.dev/description: S3 compatible object storage +spec: + rules: + - host: "minio.${SECRET_DOMAIN}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: minio-external-service + port: + number: 9090 diff --git a/kubernetes/apps/network/external-services/services/pihole.yaml b/kubernetes/apps/network/external-services/services/pihole.yaml new file mode 100644 index 00000000..ecd27fb0 --- /dev/null +++ b/kubernetes/apps/network/external-services/services/pihole.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: pihole-external-service +spec: + type: ExternalName + externalName: "${RPI_URL}" +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pihole-ingress + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: pi-hole.png + gethomepage.dev/name: PiHole + gethomepage.dev/group: Network + gethomepage.dev/description: Network-wide Ad Blocking DNS + gethomepage.dev/widget.type: pihole + gethomepage.dev/widget.url: "http://${RPI_URL}" + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_PI_HOLE_TOKEN}}" +spec: + rules: + - host: "pihole.${SECRET_DOMAIN}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: pihole-external-service + port: + number: 80 diff --git a/kubernetes/apps/network/external-services/services/proxmox.yaml b/kubernetes/apps/network/external-services/services/proxmox.yaml new file mode 100644 index 00000000..97c8af34 --- /dev/null +++ b/kubernetes/apps/network/external-services/services/proxmox.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: proxmox-external-service +spec: + type: ExternalName + externalName: 192.168.0.41 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: proxmox-ingress + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: proxmox.png + gethomepage.dev/name: Proxmox + gethomepage.dev/group: hardware + gethomepage.dev/description: Virtual Environment + gethomepage.dev/widget.type: proxmox + gethomepage.dev/widget.url: https://192.168.0.41:8006 + gethomepage.dev/widget.username: "{{HOMEPAGE_VAR_PROXMOX_USERNAME}}" + gethomepage.dev/widget.password: "{{HOMEPAGE_VAR_PROXMOX_PASSWORD}}" + gethomepage.dev/node: proxmox1 + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" +spec: + rules: + - host: "proxmox.${SECRET_DOMAIN}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: proxmox-external-service + port: + number: 8006 diff --git a/kubernetes/apps/network/external-services/services/sprut.yaml b/kubernetes/apps/network/external-services/services/sprut.yaml new file mode 100644 index 00000000..ae22e707 --- /dev/null +++ b/kubernetes/apps/network/external-services/services/sprut.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: sprut-external-service +spec: + type: ExternalName + externalName: "${RPI_URL}" +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: spruthub-ingress + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: "https://sprut.${SECRET_DOMAIN}/favicon.ico" + gethomepage.dev/name: Spruthub + gethomepage.dev/group: Home + gethomepage.dev/description: Zigbee hub and Homekit integration +spec: + rules: + - host: "sprut.${SECRET_DOMAIN}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sprut-external-service + port: + number: 7777 diff --git a/kubernetes/apps/network/external-services/services/synology.yaml b/kubernetes/apps/network/external-services/services/synology.yaml new file mode 100644 index 00000000..5141e2f1 --- /dev/null +++ b/kubernetes/apps/network/external-services/services/synology.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: synology-external-service +spec: + type: ExternalName + externalName: "${NAS_URL}" +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: synology-ingress + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: synology.png + gethomepage.dev/name: Synology + gethomepage.dev/group: Storage + gethomepage.dev/description: Synology nas disk station + gethomepage.dev/widget.type: diskstation + gethomepage.dev/widget.url: "http://${NAS_URL}:5000" + gethomepage.dev/widget.username: "{{HOMEPAGE_VAR_SYNOLOGY_USERNAME}}" + gethomepage.dev/widget.password: "{{HOMEPAGE_VAR_SYNOLOGY_PASSWORD}}" + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" +spec: + rules: + - host: "nas.${SECRET_DOMAIN}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: synology-external-service + port: + number: 5001 diff --git a/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml b/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml new file mode 100644 index 00000000..1323aabb --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/certificates/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./staging.yaml + - ./production.yaml diff --git a/kubernetes/apps/network/ingress-nginx/certificates/production.yaml b/kubernetes/apps/network/ingress-nginx/certificates/production.yaml new file mode 100644 index 00000000..b5afdf41 --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/certificates/production.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: "${SECRET_DOMAIN/./-}-production" +spec: + secretName: "${SECRET_DOMAIN/./-}-production-tls" + issuerRef: + name: letsencrypt-production + kind: ClusterIssuer + commonName: "${SECRET_DOMAIN}" + dnsNames: + - "${SECRET_DOMAIN}" + - "*.${SECRET_DOMAIN}" diff --git a/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml b/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml new file mode 100644 index 00000000..9c869425 --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/certificates/staging.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: "${SECRET_DOMAIN/./-}-staging" +spec: + secretName: "${SECRET_DOMAIN/./-}-staging-tls" + issuerRef: + name: letsencrypt-staging + kind: ClusterIssuer + commonName: "${SECRET_DOMAIN}" + dnsNames: + - "${SECRET_DOMAIN}" + - "*.${SECRET_DOMAIN}" diff --git a/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml b/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml new file mode 100644 index 00000000..5f299123 --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/external/helmrelease.yaml @@ -0,0 +1,76 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: ingress-nginx-external +spec: + interval: 30m + chart: + spec: + chart: ingress-nginx + version: 4.10.1 + sourceRef: + kind: HelmRepository + name: ingress-nginx + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + dependsOn: + - name: cloudflared + namespace: network + values: + fullnameOverride: ingress-nginx-external + controller: + service: + annotations: + external-dns.alpha.kubernetes.io/hostname: "external.${SECRET_DOMAIN}" + io.cilium/lb-ipam-ips: "192.168.20.63" + externalTrafficPolicy: Cluster + ingressClassResource: + name: external + default: false + controllerValue: k8s.io/external + admissionWebhooks: + objectSelector: + matchExpressions: + - key: ingress-class + operator: In + values: ["external"] + config: + client-body-buffer-size: 100M + client-body-timeout: 120 + client-header-timeout: 120 + enable-brotli: "true" + enable-real-ip: "true" + hsts-max-age: 31449600 + keep-alive-requests: 10000 + keep-alive: 120 + log-format-escape-json: "true" + log-format-upstream: > + {"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for", + "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, + "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", + "request_length": $request_length, "duration": $request_time, "method": "$request_method", "http_referrer": "$http_referer", + "http_user_agent": "$http_user_agent"} + proxy-body-size: 0 + proxy-buffer-size: 16k + ssl-protocols: TLSv1.3 TLSv1.2 + metrics: + enabled: true + serviceMonitor: + enabled: true + namespaceSelector: + any: true + extraArgs: + default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-production-tls" + resources: + requests: + cpu: 100m + limits: + memory: 500Mi diff --git a/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml b/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/external/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml b/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml new file mode 100644 index 00000000..c98c62de --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/internal/helmrelease.yaml @@ -0,0 +1,73 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: ingress-nginx-internal + namespace: network +spec: + interval: 30m + chart: + spec: + chart: ingress-nginx + version: 4.10.1 + sourceRef: + kind: HelmRepository + name: ingress-nginx + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + fullnameOverride: ingress-nginx-internal + controller: + service: + annotations: + io.cilium/lb-ipam-ips: "192.168.20.61" + externalTrafficPolicy: Cluster + ingressClassResource: + name: internal + default: true + controllerValue: k8s.io/internal + admissionWebhooks: + objectSelector: + matchExpressions: + - key: ingress-class + operator: In + values: ["internal"] + config: + client-body-buffer-size: 100M + client-body-timeout: 120 + client-header-timeout: 120 + enable-brotli: "true" + enable-real-ip: "true" + hsts-max-age: 31449600 + keep-alive-requests: 10000 + keep-alive: 120 + log-format-escape-json: "true" + log-format-upstream: > + {"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for", + "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, + "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", + "request_length": $request_length, "duration": $request_time, "method": "$request_method", "http_referrer": "$http_referer", + "http_user_agent": "$http_user_agent"} + proxy-body-size: 0 + proxy-buffer-size: 16k + ssl-protocols: TLSv1.3 TLSv1.2 + metrics: + enabled: true + serviceMonitor: + enabled: true + namespaceSelector: + any: true + extraArgs: + default-ssl-certificate: "network/${SECRET_DOMAIN/./-}-production-tls" + resources: + requests: + cpu: 100m + limits: + memory: 500Mi diff --git a/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml b/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/internal/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/network/ingress-nginx/ks.yaml b/kubernetes/apps/network/ingress-nginx/ks.yaml new file mode 100644 index 00000000..4121eab5 --- /dev/null +++ b/kubernetes/apps/network/ingress-nginx/ks.yaml @@ -0,0 +1,69 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app ingress-nginx-certificates + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: cert-manager-issuers + path: ./kubernetes/apps/network/ingress-nginx/certificates + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: true + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app ingress-nginx-internal + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: ingress-nginx-certificates + path: ./kubernetes/apps/network/ingress-nginx/internal + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app ingress-nginx-external + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: ingress-nginx-certificates + path: ./kubernetes/apps/network/ingress-nginx/external + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml b/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml new file mode 100644 index 00000000..95f41bf7 --- /dev/null +++ b/kubernetes/apps/network/k8s-gateway/app/helmrelease.yaml @@ -0,0 +1,34 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: k8s-gateway +spec: + interval: 30m + chart: + spec: + chart: k8s-gateway + version: 2.4.0 + sourceRef: + kind: HelmRepository + name: k8s-gateway + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + fullnameOverride: k8s-gateway + domain: "${SECRET_DOMAIN}" + ttl: 1 + service: + type: LoadBalancer + port: 53 + annotations: + io.cilium/lb-ipam-ips: "192.168.20.62" + externalTrafficPolicy: Cluster + watchedResources: ["Ingress", "Service"] diff --git a/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml b/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/network/k8s-gateway/app/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/network/k8s-gateway/ks.yaml b/kubernetes/apps/network/k8s-gateway/ks.yaml new file mode 100644 index 00000000..6709e768 --- /dev/null +++ b/kubernetes/apps/network/k8s-gateway/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app k8s-gateway + namespace: flux-system +spec: + targetNamespace: network + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/network/k8s-gateway/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/network/kustomization.yaml b/kubernetes/apps/network/kustomization.yaml new file mode 100644 index 00000000..d527fd1a --- /dev/null +++ b/kubernetes/apps/network/kustomization.yaml @@ -0,0 +1,13 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./cloudflared/ks.yaml + - ./echo-server/ks.yaml + - ./external-dns/ks.yaml + - ./ingress-nginx/ks.yaml + - ./k8s-gateway/ks.yaml + - ./external-services/ks.yaml diff --git a/kubernetes/apps/network/namespace.yaml b/kubernetes/apps/network/namespace.yaml new file mode 100644 index 00000000..4d78d7b1 --- /dev/null +++ b/kubernetes/apps/network/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: network + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/observability/alert.yaml b/kubernetes/apps/observability/alert.yaml new file mode 100644 index 00000000..df840dff --- /dev/null +++ b/kubernetes/apps/observability/alert.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: observability +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: observability +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: HelmRelease + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/observability/gatus/app/externalsecret.yaml b/kubernetes/apps/observability/gatus/app/externalsecret.yaml new file mode 100644 index 00000000..f9eca61d --- /dev/null +++ b/kubernetes/apps/observability/gatus/app/externalsecret.yaml @@ -0,0 +1,29 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: gatus +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: gatus-secret + template: + engineVersion: v2 + data: + INIT_POSTGRES_DBNAME: gatus + INIT_POSTGRES_HOST: postgres-cluster-rw.database.svc.cluster.local + INIT_POSTGRES_USER: "{{ .GATUS_POSTGRES_USER }}" + INIT_POSTGRES_PASS: "{{ .GATUS_POSTGRES_PASS }}" + INIT_POSTGRES_SUPER_USER: "{{ .POSTGRES_SUPER_USER }}" + INIT_POSTGRES_SUPER_PASS: "{{ .POSTGRES_SUPER_PASS }}" + DISCORD_WEBHOOK: "{{ .GATUS_DISCORD_WEBHOOK }}" + dataFrom: + - extract: + key: cloudnative-pg + - extract: + key: gatus + - extract: + key: discord diff --git a/kubernetes/apps/observability/gatus/app/helmrelease.yaml b/kubernetes/apps/observability/gatus/app/helmrelease.yaml new file mode 100644 index 00000000..c11f00cc --- /dev/null +++ b/kubernetes/apps/observability/gatus/app/helmrelease.yaml @@ -0,0 +1,143 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: gatus +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + values: + controllers: + gatus: + annotations: + reloader.stakater.com/auto: "true" + initContainers: + init-db: + image: + repository: ghcr.io/onedr0p/postgres-init + tag: 16 + envFrom: &envFrom + - secretRef: + name: gatus-secret + init-config: + dependsOn: init-db + image: + repository: ghcr.io/kiwigrid/k8s-sidecar + tag: 1.27.1@sha256:df71eab1466c67b84e46fa9cd2d84738372377971d44dbb8699ab4483278c839 + env: + FOLDER: /config + LABEL: gatus.io/enabled + NAMESPACE: ALL + RESOURCE: both + UNIQUE_FILENAMES: true + METHOD: WATCH + restartPolicy: Always + resources: &resources + requests: + cpu: 10m + limits: + memory: 256Mi + containers: + app: + image: + repository: ghcr.io/twin/gatus + tag: v5.10.0@sha256:658a9cb993ff0b16832947dab8de885b2e2a66037330b839310fa3f39d5c00f4 + env: + TZ: "${TIMEZONE}" + GATUS_CONFIG_PATH: /config + GATUS_DELAY_START_SECONDS: 5 + SECRET_DOMAIN: "${SECRET_DOMAIN}" + CUSTOM_WEB_PORT: &port 80 + envFrom: *envFrom + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /health + port: *port + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ "ALL" ] } + resources: *resources + defaultPodOptions: + dnsConfig: + options: + - { name: ndots, value: "1" } + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + fsGroup: 65534 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: { type: RuntimeDefault } + service: + app: + controller: gatus + ports: + http: + port: *port + serviceMonitor: + app: + serviceName: gatus + endpoints: + - port: http + scheme: http + path: /metrics + interval: 1m + scrapeTimeout: 10s + ingress: + app: + className: external + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/group: Observability + gethomepage.dev/name: Gatus + gethomepage.dev/icon: gatus.png + gethomepage.dev/description: Status page + gethomepage.dev/widget.type: gatus + gethomepage.dev/widget.url: http://gatus.observability.svc.cluster.local + external-dns.alpha.kubernetes.io/target: "external.${SECRET_DOMAIN}" + hosts: + - host: "status.${SECRET_DOMAIN}" + paths: + - path: / + service: + identifier: app + port: http + serviceAccount: + create: true + name: gatus + persistence: + config: + type: emptyDir + config-file: + type: configMap + name: gatus-configmap + globalMounts: + - path: /config/config.yaml + subPath: config.yaml + readOnly: true diff --git a/kubernetes/apps/observability/gatus/app/kustomization.yaml b/kubernetes/apps/observability/gatus/app/kustomization.yaml new file mode 100644 index 00000000..30bf43b9 --- /dev/null +++ b/kubernetes/apps/observability/gatus/app/kustomization.yaml @@ -0,0 +1,14 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./rbac.yaml + - ./helmrelease.yaml +configMapGenerator: + - name: gatus-configmap + files: + - config.yaml=./resources/config.yaml +generatorOptions: + disableNameSuffixHash: true diff --git a/kubernetes/apps/observability/gatus/app/rbac.yaml b/kubernetes/apps/observability/gatus/app/rbac.yaml new file mode 100644 index 00000000..0f12c439 --- /dev/null +++ b/kubernetes/apps/observability/gatus/app/rbac.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: gatus +rules: + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "watch", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: gatus +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: gatus +subjects: + - kind: ServiceAccount + name: gatus + namespace: observability diff --git a/kubernetes/apps/observability/gatus/app/resources/config.yaml b/kubernetes/apps/observability/gatus/app/resources/config.yaml new file mode 100644 index 00000000..7e950b0a --- /dev/null +++ b/kubernetes/apps/observability/gatus/app/resources/config.yaml @@ -0,0 +1,46 @@ +--- +# Note: Gatus vars should be escaped with $${VAR_NAME} to avoid interpolation by Flux +web: + port: $${CUSTOM_WEB_PORT} +storage: + type: postgres + path: postgres://$${INIT_POSTGRES_USER}:$${INIT_POSTGRES_PASS}@$${INIT_POSTGRES_HOST}:5432/$${INIT_POSTGRES_DBNAME}?sslmode=disable + caching: true +metrics: true +debug: false +ui: + title: Status | Gatus + header: Status +alerting: + discord: + webhook-url: $${DISCORD_WEBHOOK} + default-alert: + description: health-check failed + send-on-resolved: true + failure-threshold: 5 + success-threshold: 2 +connectivity: + checker: + target: 1.1.1.1:53 + interval: 1m +endpoints: + - name: status + group: external + url: https://status.$${SECRET_DOMAIN} + interval: 1m + client: + dns-resolver: tcp://1.1.1.1:53 + conditions: + - "[STATUS] == 200" + alerts: + - type: discord + - name: flux-webhook + group: external + url: https://flux-webhook.$${SECRET_DOMAIN} + interval: 1m + client: + dns-resolver: tcp://1.1.1.1:53 + conditions: + - "[STATUS] == 404" + alerts: + - type: discord diff --git a/kubernetes/apps/observability/gatus/ks.yaml b/kubernetes/apps/observability/gatus/ks.yaml new file mode 100644 index 00000000..fc200af3 --- /dev/null +++ b/kubernetes/apps/observability/gatus/ks.yaml @@ -0,0 +1,24 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app gatus + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + - name: cloudnative-pg + path: ./kubernetes/apps/observability/gatus/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/observability/grafana/app/externalsecret.yaml b/kubernetes/apps/observability/grafana/app/externalsecret.yaml new file mode 100644 index 00000000..222548e1 --- /dev/null +++ b/kubernetes/apps/observability/grafana/app/externalsecret.yaml @@ -0,0 +1,34 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: grafana +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: grafana-secret + template: + engineVersion: v2 + data: + GF_DATABASE_NAME: &dbName grafana + GF_DATABASE_HOST: postgres-cluster-rw.database.svc.cluster.local + GF_DATABASE_USER: &dbUser "{{ .GRAFANA_POSTGRES_USER }}" + GF_DATABASE_PASSWORD: &dbPass "{{ .GRAFANA_POSTGRES_PASS }}" + GF_DATABASE_SSL_MODE: disable + GF_DATABASE_TYPE: postgres + INIT_POSTGRES_DBNAME: *dbName + INIT_POSTGRES_HOST: postgres-cluster-rw.database.svc.cluster.local + INIT_POSTGRES_USER: *dbUser + INIT_POSTGRES_PASS: *dbPass + INIT_POSTGRES_SUPER_USER: "{{ .POSTGRES_SUPER_USER }}" + INIT_POSTGRES_SUPER_PASS: "{{ .POSTGRES_SUPER_PASS }}" + ADMIN_USER_PASS: "{{ .password }}" + ADMIN_USER_NAME: "{{ .username }}" + dataFrom: + - extract: + key: grafana + - extract: + key: cloudnative-pg diff --git a/kubernetes/apps/observability/grafana/app/helmrelease.yaml b/kubernetes/apps/observability/grafana/app/helmrelease.yaml new file mode 100644 index 00000000..4f2dcfc8 --- /dev/null +++ b/kubernetes/apps/observability/grafana/app/helmrelease.yaml @@ -0,0 +1,355 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: grafana +spec: + interval: 30m + chart: + spec: + chart: grafana + version: 7.3.11 + sourceRef: + kind: HelmRepository + name: grafana + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: kube-prometheus-stack + namespace: observability + - name: loki + namespace: observability + values: + extraInitContainers: + - name: 01-init-db + image: ghcr.io/onedr0p/postgres-init:16 + envFrom: + - secretRef: + name: &secret grafana-secret + replicas: 3 + env: + GF_DATE_FORMATS_USE_BROWSER_LOCALE: true + GF_EXPLORE_ENABLED: true + GF_FEATURE_TOGGLES_ENABLE: publicDashboards + GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: natel-discrete-panel,pr0ps-trackmap-panel,panodata-map-panel + GF_SECURITY_ANGULAR_SUPPORT_ENABLED: true + GF_SECURITY_COOKIE_SAMESITE: grafana + GF_SERVER_ROOT_URL: "https://grafana.${SECRET_DOMAIN}" + envFromSecrets: + - name: *secret + grafana.ini: + analytics: + check_for_updates: false + check_for_plugin_updates: false + reporting_enabled: false + auth.basic: + enabled: true + auth.anonymous: + enabled: true + org_role: Viewer + news: + news_feed_enabled: false + admin: + existingSecret: grafana-secret + passwordKey: ADMIN_USER_PASS + userKey: ADMIN_USER_NAME + dashboardProviders: + dashboardproviders.yaml: + apiVersion: 1 + providers: + - name: default + orgId: 1 + folder: "" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/default-folder + - name: flux + orgId: 1 + folder: Flux + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/flux-folder + - name: kubernetes + orgId: 1 + folder: Kubernetes + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/kubernetes-folder + - name: nginx + orgId: 1 + folder: Nginx + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/nginx-folder + - name: prometheus + orgId: 1 + folder: Prometheus + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/prometheus-folder + - name: thanos + orgId: 1 + folder: Thanos + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/thanos-folder + - name: unifi + orgId: 1 + folder: Unifi + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/unifi-folder + datasources: + datasources.yaml: + apiVersion: 1 + deleteDatasources: + - { name: Alertmanager, orgId: 1 } + - { name: Loki, orgId: 1 } + - { name: Prometheus, orgId: 1 } + datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + url: http://thanos-query-frontend.observability.svc.cluster.local:10902 + jsonData: + prometheusType: Thanos + timeInterval: 1m + - name: Loki + type: loki + uid: loki + access: proxy + url: http://loki-gateway.observability.svc.cluster.local + jsonData: + maxLines: 250 + - name: Alertmanager + type: alertmanager + uid: alertmanager + access: proxy + url: http://alertmanager-operated.observability.svc.cluster.local:9093 + jsonData: + implementation: prometheus + dashboards: + default: + cloudflared: + # renovate: depName="Cloudflare Tunnels (cloudflared)" + gnetId: 17457 + revision: 6 + datasource: + - { name: DS_PROMETHEUS, value: Prometheus } + external-dns: + # renovate: depName="External-dns" + gnetId: 15038 + revision: 3 + datasource: Prometheus + minio: + # renovate: depName="MinIO Dashboard" + gnetId: 13502 + revision: 26 + datasource: + - { name: DS_PROMETHEUS, value: Prometheus } + node-exporter-full: + # renovate: depName="Node Exporter Full" + gnetId: 1860 + revision: 36 + datasource: Prometheus + spegel: + # renovate: depName="Spegel" + gnetId: 18089 + revision: 1 + datasource: + - { name: DS_PROMETHEUS, value: Prometheus } + unpackerr: + # renovate: depName="Unpackerr" + gnetId: 18817 + revision: 1 + datasource: + - { name: DS_PROMETHEUS, value: Prometheus } + zfs: + # renovate: depName="ZFS" + gnetId: 7845 + revision: 4 + datasource: Prometheus + cert-manager: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/cert-manager/dashboards/cert-manager.json + datasource: Prometheus + dragonfly: + url: https://raw.githubusercontent.com/dragonflydb/dragonfly/main/tools/local/monitoring/grafana/provisioning/dashboards/dashboard.json + datasource: Prometheus + node-feature-discovery: + url: https://raw.githubusercontent.com/kubernetes-sigs/node-feature-discovery/master/examples/grafana-dashboard.json + datasource: Prometheus + flux: + flux-cluster: + url: https://raw.githubusercontent.com/fluxcd/flux2-monitoring-example/main/monitoring/configs/dashboards/cluster.json + datasource: Prometheus + flux-control-plane: + url: https://raw.githubusercontent.com/fluxcd/flux2-monitoring-example/main/monitoring/configs/dashboards/control-plane.json + datasource: Prometheus + kubernetes: + kubernetes-api-server: + # renovate: depName="Kubernetes / System / API Server" + gnetId: 15761 + revision: 16 + datasource: Prometheus + kubernetes-coredns: + # renovate: depName="Kubernetes / System / CoreDNS" + gnetId: 15762 + revision: 17 + datasource: Prometheus + kubernetes-global: + # renovate: depName="Kubernetes / Views / Global" + gnetId: 15757 + revision: 37 + datasource: Prometheus + kubernetes-namespaces: + # renovate: depName="Kubernetes / Views / Namespaces" + gnetId: 15758 + revision: 34 + datasource: Prometheus + kubernetes-nodes: + # renovate: depName="Kubernetes / Views / Nodes" + gnetId: 15759 + revision: 29 + datasource: Prometheus + kubernetes-pods: + # renovate: depName="Kubernetes / Views / Pods" + gNetId: 15760 + revision: 21 + datasource: Prometheus + kubernetes-volumes: + # renovate: depName="K8s / Storage / Volumes / Cluster" + gnetId: 11454 + revision: 14 + datasource: Prometheus + nginx: + nginx: + url: https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/grafana/dashboards/nginx.json + datasource: Prometheus + nginx-request-handling-performance: + url: https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/grafana/dashboards/request-handling-performance.json + datasource: Prometheus + prometheus: + prometheus: + # renovate: depName="Prometheus" + gnetId: 19105 + revision: 3 + datasource: Prometheus + thanos: + thanos-bucket-replicate: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/bucket-replicate.json + datasource: Prometheus + thanos-compact: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/compact.json + datasource: Prometheus + thanos-overview: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/overview.json + datasource: Prometheus + thanos-query: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/query.json + datasource: Prometheus + thanos-query-frontend: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/query-frontend.json + datasource: Prometheus + thanos-receieve: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/receive.json + datasource: Prometheus + thanos-rule: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/rule.json + datasource: Prometheus + thanos-sidecar: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/sidecar.json + datasource: Prometheus + thanos-store: + url: https://raw.githubusercontent.com/monitoring-mixins/website/master/assets/thanos/dashboards/store.json + datasource: Prometheus + unifi: + unifi-insights: + # renovate: depName="UniFi-Poller: Client Insights - Prometheus" + gnetId: 11315 + revision: 9 + datasource: Prometheus + unifi-network-sites: + # renovate: depName="UniFi-Poller: Network Sites - Prometheus" + gnetId: 11311 + revision: 5 + datasource: Prometheus + unifi-uap: + # renovate: depName="UniFi-Poller: UAP Insights - Prometheus" + gnetId: 11314 + revision: 10 + datasource: Prometheus + unifi-usw: + # renovate: depName="UniFi-Poller: USW Insights - Prometheus" + gnetId: 11312 + revision: 9 + datasource: Prometheus + sidecar: + dashboards: + enabled: true + searchNamespace: ALL + label: grafana_dashboard + folderAnnotation: grafana_folder + provider: + disableDelete: true + foldersFromFilesStructure: true + datasources: + enabled: true + searchNamespace: ALL + labelValue: "" + plugins: + - grafana-clock-panel + - grafana-piechart-panel + - grafana-worldmap-panel + - natel-discrete-panel + - pr0ps-trackmap-panel + - vonage-status-panel + serviceMonitor: + enabled: true + ingress: + enabled: true + ingressClassName: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: grafana.png + gethomepage.dev/name: Grafana + gethomepage.dev/group: Observability + gethomepage.dev/description: Visual analytics & monitoring platform + gethomepage.dev/widget.type: grafana + gethomepage.dev/widget.url: http://grafana.observability.svc.cluster.local + gethomepage.dev/widget.username: "{{`{{HOMEPAGE_VAR_GRAFANA_USER}}`}}" + gethomepage.dev/widget.password: "{{`{{HOMEPAGE_VAR_GRAFANA_PASSWORD}}`}}" + hosts: ["grafana.${SECRET_DOMAIN}"] + persistence: + enabled: false + testFramework: + enabled: false + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: grafana diff --git a/kubernetes/apps/observability/grafana/app/kustomization.yaml b/kubernetes/apps/observability/grafana/app/kustomization.yaml new file mode 100644 index 00000000..4eed917b --- /dev/null +++ b/kubernetes/apps/observability/grafana/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml diff --git a/kubernetes/apps/observability/grafana/ks.yaml b/kubernetes/apps/observability/grafana/ks.yaml new file mode 100644 index 00000000..5e74d286 --- /dev/null +++ b/kubernetes/apps/observability/grafana/ks.yaml @@ -0,0 +1,24 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app grafana + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + - name: cloudnative-pg-cluster + path: ./kubernetes/apps/observability/grafana/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/observability/kube-prometheus-stack/app/externalsecret.yaml b/kubernetes/apps/observability/kube-prometheus-stack/app/externalsecret.yaml new file mode 100644 index 00000000..079f6dce --- /dev/null +++ b/kubernetes/apps/observability/kube-prometheus-stack/app/externalsecret.yaml @@ -0,0 +1,48 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: alertmanager +spec: + refreshInterval: 5m + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: alertmanager-secret + template: + engineVersion: v2 + data: + alertmanager.yaml: | + global: + resolve_timeout: 5m + route: + group_by: ["alertname", "job"] + group_interval: 10m + group_wait: 1m + receiver: discord + repeat_interval: 12h + routes: + - receiver: "null" + matchers: + - alertname =~ "InfoInhibitor" + - receiver: discord + continue: true + matchers: + - severity = "critical" + inhibit_rules: + - equal: ["alertname", "namespace"] + source_matchers: + - severity = "critical" + target_matchers: + - severity = "warning" + receivers: + - name: "null" + - name: discord + discord_configs: + - webhook_url: "{{ .ALERTMANAGER_DISCORD_WEBHOOK }}" + send_resolved: true + dataFrom: + - extract: + key: discord diff --git a/kubernetes/apps/observability/kube-prometheus-stack/app/helmrelease.yaml b/kubernetes/apps/observability/kube-prometheus-stack/app/helmrelease.yaml new file mode 100644 index 00000000..5793e378 --- /dev/null +++ b/kubernetes/apps/observability/kube-prometheus-stack/app/helmrelease.yaml @@ -0,0 +1,188 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: kube-prometheus-stack +spec: + interval: 30m + timeout: 15m + chart: + spec: + chart: kube-prometheus-stack + version: 58.6.0 + sourceRef: + kind: HelmRepository + name: prometheus-community + namespace: flux-system + install: + crds: CreateReplace + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + crds: CreateReplace + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: local-path-provisioner + namespace: storage + - name: thanos + namespace: observability + values: + crds: + enabled: true + cleanPrometheusOperatorObjectNames: true + alertmanager: + ingress: + enabled: true + pathType: Prefix + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: alertmanager.svg + gethomepage.dev/name: Alertmanager + gethomepage.dev/group: Observability + gethomepage.dev/description: Manage prometheus alerts + ingressClassName: internal + hosts: [ "alertmanager.${SECRET_DOMAIN}" ] + alertmanagerSpec: + replicas: 2 + useExistingSecret: true + configSecret: alertmanager-secret + storage: + volumeClaimTemplate: + spec: + storageClassName: local-hostpath + resources: + requests: + storage: 1Gi + kubelet: + enabled: true + serviceMonitor: + metricRelabelings: + # Drop high cardinality labels + - action: labeldrop + regex: (uid) + - action: labeldrop + regex: (id|name) + - action: drop + sourceLabels: [ "__name__" ] + regex: (rest_client_request_duration_seconds_bucket|rest_client_request_duration_seconds_sum|rest_client_request_duration_seconds_count) + kubeApiServer: + enabled: true + serviceMonitor: + metricRelabelings: + # Drop high cardinality labels + - action: drop + sourceLabels: [ "__name__" ] + regex: (apiserver|etcd|rest_client)_request(|_sli|_slo)_duration_seconds_bucket + - action: drop + sourceLabels: [ "__name__" ] + regex: (apiserver_response_sizes_bucket|apiserver_watch_events_sizes_bucket) + kubeControllerManager: + enabled: true + endpoints: &cp + - 192.168.20.51 + - 192.168.20.52 + - 192.168.20.53 + kubeEtcd: + enabled: true + endpoints: *cp + kubeScheduler: + enabled: true + endpoints: *cp + kubeProxy: + enabled: false + prometheus: + ingress: + enabled: true + ingressClassName: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: prometheus.svg + gethomepage.dev/name: Prometheus + gethomepage.dev/group: Observability + gethomepage.dev/description: Monitoring system + pathType: Prefix + hosts: [ "prometheus.${SECRET_DOMAIN}" ] + thanosService: + enabled: true + thanosServiceMonitor: + enabled: true + prometheusSpec: + podMetadata: + annotations: + secret.reloader.stakater.com/reload: &secret thanos-objstore-config + replicas: 2 + replicaExternalLabelName: __replica__ + scrapeInterval: 1m # Must match interval in Grafana Helm chart + ruleSelectorNilUsesHelmValues: false + serviceMonitorSelectorNilUsesHelmValues: false + podMonitorSelectorNilUsesHelmValues: false + probeSelectorNilUsesHelmValues: false + scrapeConfigSelectorNilUsesHelmValues: false + enableAdminAPI: true + walCompression: true + enableFeatures: # https://prometheus.io/docs/prometheus/latest/feature_flags/ + - auto-gomemlimit + - memory-snapshot-on-shutdown + - new-service-discovery-manager + thanos: + image: quay.io/thanos/thanos:${THANOS_VERSION} + version: "${THANOS_VERSION#v}" + objectStorageConfig: + existingSecret: + name: *secret + key: config + retention: 2d + retentionSize: 10GB + resources: + requests: + cpu: 100m + limits: + memory: 2500Mi + storageSpec: + volumeClaimTemplate: + spec: + storageClassName: local-hostpath + resources: + requests: + storage: 15Gi + nodeExporter: + enabled: true + prometheus-node-exporter: + fullnameOverride: node-exporter + prometheus: + monitor: + enabled: true + relabelings: + - action: replace + regex: (.*) + replacement: $1 + sourceLabels: [ "__meta_kubernetes_pod_node_name" ] + targetLabel: kubernetes_node + kubeStateMetrics: + enabled: true + kube-state-metrics: + fullnameOverride: kube-state-metrics + metricLabelsAllowlist: + - pods=[*] + - deployments=[*] + - persistentvolumeclaims=[*] + prometheus: + monitor: + enabled: true + relabelings: + - action: replace + regex: (.*) + replacement: $1 + sourceLabels: [ "__meta_kubernetes_pod_node_name" ] + targetLabel: kubernetes_node + grafana: + enabled: false + forceDeployDashboards: true + sidecar: + dashboards: + annotations: + grafana_folder: Kubernetes diff --git a/kubernetes/apps/observability/kube-prometheus-stack/app/kustomization.yaml b/kubernetes/apps/observability/kube-prometheus-stack/app/kustomization.yaml new file mode 100644 index 00000000..9cffb524 --- /dev/null +++ b/kubernetes/apps/observability/kube-prometheus-stack/app/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml + - ./prometheusrule.yaml diff --git a/kubernetes/apps/observability/kube-prometheus-stack/app/prometheusrule.yaml b/kubernetes/apps/observability/kube-prometheus-stack/app/prometheusrule.yaml new file mode 100644 index 00000000..4d880fa2 --- /dev/null +++ b/kubernetes/apps/observability/kube-prometheus-stack/app/prometheusrule.yaml @@ -0,0 +1,25 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/prometheusrule_v1.json +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: miscellaneous-rules +spec: + groups: + - name: dockerhub + rules: + - alert: BootstrapRateLimitRisk + annotations: + summary: Kubernetes cluster at risk of being rate limited by dockerhub on bootstrap + expr: count(time() - container_last_seen{image=~"(docker.io).*",container!=""} < 30) > 100 + for: 15m + labels: + severity: critical + - name: oom + rules: + - alert: OOMKilled + annotations: + summary: Container {{ $labels.container }} in pod {{ $labels.namespace }}/{{ $labels.pod }} has been OOMKilled {{ $value }} times in the last 10 minutes. + expr: (kube_pod_container_status_restarts_total - kube_pod_container_status_restarts_total offset 10m >= 1) and ignoring (reason) min_over_time(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}[10m]) == 1 + labels: + severity: critical diff --git a/kubernetes/apps/observability/kube-prometheus-stack/ks.yaml b/kubernetes/apps/observability/kube-prometheus-stack/ks.yaml new file mode 100644 index 00000000..1dca6026 --- /dev/null +++ b/kubernetes/apps/observability/kube-prometheus-stack/ks.yaml @@ -0,0 +1,27 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app kube-prometheus-stack + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/observability/kube-prometheus-stack/app + dependsOn: + - name: external-secrets-stores + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + # renovate: datasource=docker depName=quay.io/thanos/thanos + THANOS_VERSION: v0.35.0 diff --git a/kubernetes/apps/observability/kustomization.yaml b/kubernetes/apps/observability/kustomization.yaml new file mode 100644 index 00000000..3fb6fdbc --- /dev/null +++ b/kubernetes/apps/observability/kustomization.yaml @@ -0,0 +1,15 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./portainer/ks.yaml + - ./gatus/ks.yaml + - ./thanos/ks.yaml + - ./kube-prometheus-stack/ks.yaml + - ./grafana/ks.yaml + - ./vector/ks.yaml + - ./loki/ks.yaml + - ./unpoller/ks.yaml diff --git a/kubernetes/apps/observability/loki/app/externalsecret.yaml b/kubernetes/apps/observability/loki/app/externalsecret.yaml new file mode 100644 index 00000000..e13a2dbf --- /dev/null +++ b/kubernetes/apps/observability/loki/app/externalsecret.yaml @@ -0,0 +1,23 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: loki +spec: + refreshInterval: 5m + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: loki-secret + template: + engineVersion: v2 + data: + S3_BUCKET: "{{ .MINIO_LOKI_BUCKET }}" + S3_ACCESS_KEY: "{{ .MINIO_LOKI_ACCESS_KEY }}" + S3_SECRET_KEY: "{{ .MINIO_LOKI_SECRET_KEY }}" + S3_REGION: us-east-1 + dataFrom: + - extract: + key: minio diff --git a/kubernetes/apps/observability/loki/app/helmrelease.yaml b/kubernetes/apps/observability/loki/app/helmrelease.yaml new file mode 100644 index 00000000..4f1d3af9 --- /dev/null +++ b/kubernetes/apps/observability/loki/app/helmrelease.yaml @@ -0,0 +1,148 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: loki +spec: + interval: 30m + timeout: 15m + chart: + spec: + chart: loki + version: 6.5.2 + sourceRef: + kind: HelmRepository + name: grafana + namespace: flux-system + install: + crds: Skip + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + crds: Skip + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: local-path-provisioner + namespace: storage + - name: vector-agent + namespace: observability + - name: vector-aggregator + namespace: observability + valuesFrom: + - targetPath: loki.storage.bucketNames.chunks + kind: Secret + name: &lokiSecret loki-secret + valuesKey: S3_BUCKET + - targetPath: loki.storage.s3.region + kind: Secret + name: *lokiSecret + valuesKey: S3_REGION + - targetPath: loki.storage.s3.accessKeyId + kind: Secret + name: *lokiSecret + valuesKey: S3_ACCESS_KEY + - targetPath: loki.storage.s3.secretAccessKey + kind: Secret + name: *lokiSecret + valuesKey: S3_SECRET_KEY + values: + deploymentMode: SimpleScalable + loki: + podAnnotations: + configmap.reloader.stakater.com/reload: *lokiSecret + secret.reloader.stakater.com/reload: *lokiSecret + ingester: + chunk_encoding: snappy + storage: + type: s3 + s3: + endpoint: ${NAS_URL}:9000 + s3ForcePathStyle: true + insecure: true + schemaConfig: + configs: + - from: "2024-04-01" + store: tsdb + object_store: s3 + schema: v13 + index: + prefix: loki_index_ + period: 24h + structuredConfig: + auth_enabled: false + server: + log_level: info + http_listen_port: 3100 + grpc_listen_port: 9095 + grpc_server_max_recv_msg_size: 8388608 + grpc_server_max_send_msg_size: 8388608 + limits_config: + ingestion_burst_size_mb: 128 + ingestion_rate_mb: 64 + max_query_parallelism: 100 + per_stream_rate_limit: 64M + per_stream_rate_limit_burst: 128M + reject_old_samples: true + reject_old_samples_max_age: 168h + retention_period: 30d + shard_streams: + enabled: true + split_queries_by_interval: 1h + query_scheduler: + max_outstanding_requests_per_tenant: 4096 + frontend: + max_outstanding_per_tenant: 4096 + ruler: + enable_api: true + enable_alertmanager_v2: true + alertmanager_url: http://alertmanager-operated.observability.svc.cluster.local:9093 + storage: + type: local + local: + directory: /rules + rule_path: /rules/fake + analytics: + reporting_enabled: false + backend: + replicas: 3 + persistence: + size: 7Gi + storageClass: local-hostpath + gateway: + replicas: 3 + image: + registry: ghcr.io + deploymentStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 1 + ingress: + enabled: true + ingressClassName: internal + hosts: + - host: "loki.${SECRET_DOMAIN}" + paths: + - path: / + pathType: Prefix + read: + replicas: 3 + write: + replicas: 3 + persistence: + size: 7Gi + storageClass: local-hostpath + sidecar: + image: + repository: ghcr.io/kiwigrid/k8s-sidecar + rules: + searchNamespace: ALL + folder: /rules/fake + lokiCanary: + enabled: false + test: + enabled: false diff --git a/kubernetes/apps/observability/loki/app/kustomization.yaml b/kubernetes/apps/observability/loki/app/kustomization.yaml new file mode 100644 index 00000000..4eed917b --- /dev/null +++ b/kubernetes/apps/observability/loki/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml diff --git a/kubernetes/apps/observability/loki/ks.yaml b/kubernetes/apps/observability/loki/ks.yaml new file mode 100644 index 00000000..e21695b9 --- /dev/null +++ b/kubernetes/apps/observability/loki/ks.yaml @@ -0,0 +1,24 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app loki + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + - name: dragonfly-cluster + path: ./kubernetes/apps/observability/loki/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/observability/namespace.yaml b/kubernetes/apps/observability/namespace.yaml new file mode 100644 index 00000000..ce3a5bd2 --- /dev/null +++ b/kubernetes/apps/observability/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: observability + labels: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/observability/portainer/app/helmrelease.yaml b/kubernetes/apps/observability/portainer/app/helmrelease.yaml new file mode 100644 index 00000000..08f6b97a --- /dev/null +++ b/kubernetes/apps/observability/portainer/app/helmrelease.yaml @@ -0,0 +1,50 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app portainer + namespace: networking +spec: + interval: 5m + dependsOn: + - name: longhorn + namespace: storage + - name: volsync + namespace: storage + chart: + spec: + chart: portainer + version: 1.0.51 + sourceRef: + kind: HelmRepository + name: portainer-charts + namespace: flux-system + interval: 5m + values: + image: + repository: portainer/portainer-ce + tag: 2.20.2 + service: + type: ClusterIP + httpPort: 9000 + persistence: + existingClaim: *app + ingress: + enabled: true + ingressClassName: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: portainer.png + gethomepage.dev/name: Portainer + gethomepage.dev/group: Observability + gethomepage.dev/description: Container management UI + gethomepage.dev/widget.type: portainer + gethomepage.dev/widget.url: http://portainer.observability.svc.cluster.local:9000 + gethomepage.dev/widget.env: "2" + gethomepage.dev/widget.key: "{{HOMEPAGE_VAR_PORTAINER_TOKEN}}" + hosts: + - host: "portainer.${SECRET_DOMAIN}" + paths: + - path: / + pathType: Prefix diff --git a/kubernetes/apps/observability/portainer/app/kustomization.yaml b/kubernetes/apps/observability/portainer/app/kustomization.yaml new file mode 100644 index 00000000..a928a563 --- /dev/null +++ b/kubernetes/apps/observability/portainer/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ../../../../templates/volsync diff --git a/kubernetes/apps/observability/portainer/ks.yaml b/kubernetes/apps/observability/portainer/ks.yaml new file mode 100644 index 00000000..90c5ec1c --- /dev/null +++ b/kubernetes/apps/observability/portainer/ks.yaml @@ -0,0 +1,25 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app portainer + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/observability/portainer/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 1Gi diff --git a/kubernetes/apps/observability/thanos/app/externalsecret.yaml b/kubernetes/apps/observability/thanos/app/externalsecret.yaml new file mode 100644 index 00000000..21ce82c3 --- /dev/null +++ b/kubernetes/apps/observability/thanos/app/externalsecret.yaml @@ -0,0 +1,23 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: thanos +spec: + refreshInterval: 5m + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: thanos-secret + template: + engineVersion: v2 + data: + S3_BUCKET: "{{ .MINIO_THANOS_BUCKET }}" + S3_ACCESS_KEY: "{{ .MINIO_THANOS_ACCESS_KEY }}" + S3_SECRET_KEY: "{{ .MINIO_THANOS_SECRET_KEY }}" + S3_REGION: us-east-1 + dataFrom: + - extract: + key: minio diff --git a/kubernetes/apps/observability/thanos/app/helmrelease.yaml b/kubernetes/apps/observability/thanos/app/helmrelease.yaml new file mode 100644 index 00000000..660f7690 --- /dev/null +++ b/kubernetes/apps/observability/thanos/app/helmrelease.yaml @@ -0,0 +1,124 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: thanos +spec: + interval: 30m + timeout: 15m + chart: + spec: + chart: thanos + version: 1.17.1 + sourceRef: + kind: HelmRepository + name: stevehipwell + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: local-path-provisioner + namespace: storage + valuesFrom: + - targetPath: objstoreConfig.value.config.bucket + kind: Secret + name: thanos-secret + valuesKey: S3_BUCKET + - targetPath: objstoreConfig.value.config.region + kind: Secret + name: thanos-secret + valuesKey: S3_REGION + - targetPath: objstoreConfig.value.config.access_key + kind: Secret + name: thanos-secret + valuesKey: S3_ACCESS_KEY + - targetPath: objstoreConfig.value.config.secret_key + kind: Secret + name: thanos-secret + valuesKey: S3_SECRET_KEY + values: + objstoreConfig: + value: + type: s3 + config: + insecure: true + endpoint: ${NAS_URL}:9000 + additionalEndpoints: + - dnssrv+_grpc._tcp.kube-prometheus-stack-thanos-discovery.observability.svc.cluster.local + additionalReplicaLabels: ["__replica__"] + serviceMonitor: + enabled: true + compact: + enabled: true + extraArgs: + - --compact.concurrency=4 + - --delete-delay=30m + - --retention.resolution-raw=14d + - --retention.resolution-5m=30d + - --retention.resolution-1h=60d + persistence: &persistence + enabled: true + storageClass: local-hostpath + size: 5Gi + query: + replicas: 3 + extraArgs: ["--alert.query-url=https://thanos.${SECRET_DOMAIN}"] + queryFrontend: + enabled: true + replicas: 3 + extraEnv: &extraEnv + - name: THANOS_CACHE_CONFIG + valueFrom: + configMapKeyRef: + name: &configMap thanos-cache-configmap + key: cache.yaml + extraArgs: ["--query-range.response-cache-config=$(THANOS_CACHE_CONFIG)"] + ingress: + enabled: true + ingressClassName: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: thanos.svg + gethomepage.dev/name: Thanos + gethomepage.dev/group: Observability + gethomepage.dev/description: Highly available Prometheus setup + hosts: + - thanos.${SECRET_DOMAIN} + podAnnotations: &podAnnotations + configmap.reloader.stakater.com/reload: *configMap + rule: + enabled: true + replicas: 3 + extraArgs: ["--web.prefix-header=X-Forwarded-Prefix"] + alertmanagersConfig: + value: |- + alertmanagers: + - api_version: v2 + static_configs: + - dnssrv+_http-web._tcp.alertmanager-operated.observability.svc.cluster.local + rules: + value: |- + groups: + - name: PrometheusWatcher + rules: + - alert: PrometheusDown + annotations: + summary: A Prometheus has disappeared from Prometheus target discovery + expr: absent(up{job="kube-prometheus-stack-prometheus"}) + for: 5m + labels: + severity: critical + persistence: *persistence + storeGateway: + replicas: 3 + extraEnv: *extraEnv + extraArgs: ["--index-cache.config=$(THANOS_CACHE_CONFIG)"] + persistence: *persistence + podAnnotations: *podAnnotations diff --git a/kubernetes/apps/observability/thanos/app/kustomization.yaml b/kubernetes/apps/observability/thanos/app/kustomization.yaml new file mode 100644 index 00000000..3690a04f --- /dev/null +++ b/kubernetes/apps/observability/thanos/app/kustomization.yaml @@ -0,0 +1,13 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml +configMapGenerator: + - name: thanos-cache-configmap + files: + - cache.yaml=./resources/cache.yaml +generatorOptions: + disableNameSuffixHash: true diff --git a/kubernetes/apps/observability/thanos/app/resources/cache.yaml b/kubernetes/apps/observability/thanos/app/resources/cache.yaml new file mode 100644 index 00000000..df31f345 --- /dev/null +++ b/kubernetes/apps/observability/thanos/app/resources/cache.yaml @@ -0,0 +1,5 @@ +--- +type: REDIS +config: + addr: dragonfly.database.svc.cluster.local:6379 + db: 1 diff --git a/kubernetes/apps/observability/thanos/ks.yaml b/kubernetes/apps/observability/thanos/ks.yaml new file mode 100644 index 00000000..c981bcf5 --- /dev/null +++ b/kubernetes/apps/observability/thanos/ks.yaml @@ -0,0 +1,24 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app thanos + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: dragonfly-cluster + - name: external-secrets-stores + path: ./kubernetes/apps/observability/thanos/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/observability/unpoller/app/externalsecret.yaml b/kubernetes/apps/observability/unpoller/app/externalsecret.yaml new file mode 100644 index 00000000..9d5e5d0d --- /dev/null +++ b/kubernetes/apps/observability/unpoller/app/externalsecret.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: unpoller +spec: + refreshInterval: 5m + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: unpoller-secret + template: + engineVersion: v2 + data: + UP_UNIFI_DEFAULT_USER: "{{ .username }}" + UP_UNIFI_DEFAULT_PASS: "{{ .password }}" + dataFrom: + - extract: + key: unifipoller diff --git a/kubernetes/apps/observability/unpoller/app/helmrelease.yaml b/kubernetes/apps/observability/unpoller/app/helmrelease.yaml new file mode 100644 index 00000000..995a7ce7 --- /dev/null +++ b/kubernetes/apps/observability/unpoller/app/helmrelease.yaml @@ -0,0 +1,79 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: unpoller +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: kube-prometheus-stack + namespace: observability + values: + controllers: + unpoller: + containers: + app: + image: + repository: ghcr.io/unpoller/unpoller + tag: v2.11.2@sha256:73b39c0b3b8fa92aa82a7613d3486253ffbd8c057833b4621402a268159bf2a2 + env: + TZ: "${TIMEZONE}" + UP_UNIFI_DEFAULT_ROLE: home-ops + UP_UNIFI_DEFAULT_URL: https://192.168.20.1 + UP_UNIFI_DEFAULT_VERIFY_SSL: false + UP_INFLUXDB_DISABLE: true + envFrom: + - secretRef: + name: unpoller-secret + probes: + liveness: + enabled: true + readiness: + enabled: true + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + resources: + requests: + cpu: 10m + limits: + memory: 128Mi + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + seccompProfile: { type: RuntimeDefault } + service: + app: + controller: unpoller + ports: + http: + port: 9130 + serviceMonitor: + app: + serviceName: unpoller + endpoints: + - port: http + scheme: http + path: /metrics + interval: 2m # Unifi API only polls at 2m intervals + scrapeTimeout: 10s diff --git a/kubernetes/apps/observability/unpoller/app/kustomization.yaml b/kubernetes/apps/observability/unpoller/app/kustomization.yaml new file mode 100644 index 00000000..4eed917b --- /dev/null +++ b/kubernetes/apps/observability/unpoller/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml diff --git a/kubernetes/apps/observability/unpoller/ks.yaml b/kubernetes/apps/observability/unpoller/ks.yaml new file mode 100644 index 00000000..498091f7 --- /dev/null +++ b/kubernetes/apps/observability/unpoller/ks.yaml @@ -0,0 +1,23 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app unpoller + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + dependsOn: + - name: external-secrets-stores + path: ./kubernetes/apps/observability/unpoller/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/observability/vector/app/agent/helmrelease.yaml b/kubernetes/apps/observability/vector/app/agent/helmrelease.yaml new file mode 100644 index 00000000..129b5ff9 --- /dev/null +++ b/kubernetes/apps/observability/vector/app/agent/helmrelease.yaml @@ -0,0 +1,103 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: vector-agent +spec: + interval: 30m + timeout: 15m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + dependsOn: + - name: vector-aggregator + namespace: observability + values: + controllers: + vector-agent: + type: daemonset + strategy: RollingUpdate + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: docker.io/timberio/vector + tag: 0.38.0-alpine@sha256:0f40a5bc6df18de0f4855ee6a801449f1b78fa60b3c5001c530896abc64b18b2 + env: + PROCFS_ROOT: /host/proc + SYSFS_ROOT: /host/sys + VECTOR_SELF_NODE_NAME: + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + VECTOR_SELF_POD_NAME: + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + VECTOR_SELF_POD_NAMESPACE: + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + args: ["--config", "/etc/vector/vector.yaml"] + securityContext: + privileged: true + serviceAccount: + create: true + name: vector-agent + persistence: + config: + enabled: true + type: configMap + name: vector-agent-configmap + globalMounts: + - path: /etc/vector/vector.yaml + subPath: vector.yaml + readOnly: true + data: + type: emptyDir + globalMounts: + - path: /vector-data-dir + procfs: + type: hostPath + hostPath: /proc + hostPathType: Directory + globalMounts: + - path: /host/proc + readOnly: true + sysfs: + type: hostPath + hostPath: /sys + hostPathType: Directory + globalMounts: + - path: /host/sys + readOnly: true + var-lib: + type: hostPath + hostPath: /var/lib + hostPathType: Directory + globalMounts: + - readOnly: true + var-log: + type: hostPath + hostPath: /var/log + hostPathType: Directory + globalMounts: + - readOnly: true diff --git a/kubernetes/apps/observability/vector/app/agent/kustomization.yaml b/kubernetes/apps/observability/vector/app/agent/kustomization.yaml new file mode 100644 index 00000000..cad3d529 --- /dev/null +++ b/kubernetes/apps/observability/vector/app/agent/kustomization.yaml @@ -0,0 +1,13 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./rbac.yaml +configMapGenerator: + - name: vector-agent-configmap + files: + - vector.yaml=./resources/vector.yaml +generatorOptions: + disableNameSuffixHash: true diff --git a/kubernetes/apps/observability/vector/app/agent/rbac.yaml b/kubernetes/apps/observability/vector/app/agent/rbac.yaml new file mode 100644 index 00000000..a088f8d1 --- /dev/null +++ b/kubernetes/apps/observability/vector/app/agent/rbac.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: vector-agent +rules: + - apiGroups: [""] + resources: ["namespaces", "nodes", "pods"] + verbs: ["list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: vector-agent +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: vector-agent +subjects: + - kind: ServiceAccount + name: vector-agent + namespace: observability diff --git a/kubernetes/apps/observability/vector/app/agent/resources/vector.yaml b/kubernetes/apps/observability/vector/app/agent/resources/vector.yaml new file mode 100644 index 00000000..f3a7565c --- /dev/null +++ b/kubernetes/apps/observability/vector/app/agent/resources/vector.yaml @@ -0,0 +1,25 @@ +--- +data_dir: /vector-data-dir + +sources: + kubernetes_source: + type: kubernetes_logs + use_apiserver_cache: true + pod_annotation_fields: + container_image: container_image + container_name: container_name + pod_labels: pod_labels + pod_name: pod_name + pod_annotations: "" + namespace_annotation_fields: + namespace_labels: "" + node_annotation_fields: + node_labels: "" + +sinks: + kubernetes: + type: vector + compression: true + version: "2" + address: vector-aggregator.observability.svc.cluster.local:6010 + inputs: ["kubernetes_source"] diff --git a/kubernetes/apps/observability/vector/app/aggregator/helmrelease.yaml b/kubernetes/apps/observability/vector/app/aggregator/helmrelease.yaml new file mode 100644 index 00000000..30dd1955 --- /dev/null +++ b/kubernetes/apps/observability/vector/app/aggregator/helmrelease.yaml @@ -0,0 +1,78 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app vector-aggregator +spec: + interval: 30m + timeout: 15m + chart: + spec: + chart: app-template + version: 3.1.0 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + values: + controllers: + vector-aggregator: + replicas: 3 + strategy: RollingUpdate + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: docker.io/timberio/vector + tag: 0.38.0-alpine@sha256:0f40a5bc6df18de0f4855ee6a801449f1b78fa60b3c5001c530896abc64b18b2 + args: ["--config", "/etc/vector/vector.yaml"] + probes: + liveness: + enabled: true + readiness: + enabled: true + defaultPodOptions: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: *app + service: + app: + controller: vector-aggregator + type: LoadBalancer + annotations: + external-dns.alpha.kubernetes.io/hostname: vector.devbu.io + io.cilium/lb-ipam-ips: 192.168.20.66 + ports: + http: + primary: true + port: 8686 + journald: + port: 6000 + kubernetes: + port: 6010 + persistence: + config: + type: configMap + name: vector-aggregator-configmap + globalMounts: + - path: /etc/vector/vector.yaml + subPath: vector.yaml + readOnly: true + data: + type: emptyDir + globalMounts: + - path: /vector-data-dir diff --git a/kubernetes/apps/observability/vector/app/aggregator/kustomization.yaml b/kubernetes/apps/observability/vector/app/aggregator/kustomization.yaml new file mode 100644 index 00000000..a1322387 --- /dev/null +++ b/kubernetes/apps/observability/vector/app/aggregator/kustomization.yaml @@ -0,0 +1,12 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml +configMapGenerator: + - name: vector-aggregator-configmap + files: + - vector.yaml=./resources/vector.yaml +generatorOptions: + disableNameSuffixHash: true diff --git a/kubernetes/apps/observability/vector/app/aggregator/resources/vector.yaml b/kubernetes/apps/observability/vector/app/aggregator/resources/vector.yaml new file mode 100644 index 00000000..7d0c4b41 --- /dev/null +++ b/kubernetes/apps/observability/vector/app/aggregator/resources/vector.yaml @@ -0,0 +1,63 @@ +--- +data_dir: /vector-data-dir +api: + enabled: true + address: 0.0.0.0:8686 + +# +# Sources +# + +sources: + journald_source: + type: vector + version: "2" + address: 0.0.0.0:6000 + + kubernetes_source: + type: vector + version: "2" + address: 0.0.0.0:6010 + +# +# Transforms +# + +transforms: + kubernetes_remap: + type: remap + inputs: ["kubernetes_source"] + source: | + # Standardize 'app' index + .custom_app_name = .pod_labels."app.kubernetes.io/name" || .pod_labels.app || .pod_labels."k8s-app" || "unknown" + # Drop pod_labels + del(.pod_labels) + +# +# Sinks +# + +sinks: + journald: + inputs: ["journald_source"] + type: loki + endpoint: http://loki-gateway.observability.svc.cluster.local + encoding: { codec: json } + out_of_order_action: accept + remove_label_fields: true + remove_timestamp: true + labels: + hostname: '{{ host }}' + + kubernetes: + inputs: ["kubernetes_remap"] + type: loki + endpoint: http://loki-gateway.observability.svc.cluster.local + encoding: { codec: json } + out_of_order_action: accept + remove_label_fields: true + remove_timestamp: true + labels: + app: '{{ custom_app_name }}' + namespace: '{{ kubernetes.pod_namespace }}' + node: '{{ kubernetes.pod_node_name }}' diff --git a/kubernetes/apps/observability/vector/app/kustomization.yaml b/kubernetes/apps/observability/vector/app/kustomization.yaml new file mode 100644 index 00000000..54568aa0 --- /dev/null +++ b/kubernetes/apps/observability/vector/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./agent + - ./aggregator diff --git a/kubernetes/apps/observability/vector/ks.yaml b/kubernetes/apps/observability/vector/ks.yaml new file mode 100644 index 00000000..fef82ed7 --- /dev/null +++ b/kubernetes/apps/observability/vector/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app vector + namespace: flux-system +spec: + targetNamespace: observability + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/observability/vector/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/storage/alert.yaml b/kubernetes/apps/storage/alert.yaml new file mode 100644 index 00000000..e16426b6 --- /dev/null +++ b/kubernetes/apps/storage/alert.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/provider_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: alert-manager + namespace: storage +spec: + type: alertmanager + address: http://alertmanager-operated.observability.svc.cluster.local:9093/api/v2/alerts/ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/notification.toolkit.fluxcd.io/alert_v1beta3.json +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: alert-manager + namespace: storage +spec: + providerRef: + name: alert-manager + eventSeverity: error + eventSources: + - kind: HelmRelease + name: "*" + exclusionList: + - "error.*lookup github\\.com" + - "error.*lookup raw\\.githubusercontent\\.com" + - "dial.*tcp.*timeout" + - "waiting.*socket" + suspend: false diff --git a/kubernetes/apps/storage/kustomization.yaml b/kubernetes/apps/storage/kustomization.yaml new file mode 100644 index 00000000..f057d62d --- /dev/null +++ b/kubernetes/apps/storage/kustomization.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespace.yaml + - ./alert.yaml + - ./longhorn/ks.yaml + - ./volsync/ks.yaml + - ./local-path-provisioner/ks.yaml diff --git a/kubernetes/apps/storage/local-path-provisioner/app/helmrelease.yaml b/kubernetes/apps/storage/local-path-provisioner/app/helmrelease.yaml new file mode 100644 index 00000000..b4ef585a --- /dev/null +++ b/kubernetes/apps/storage/local-path-provisioner/app/helmrelease.yaml @@ -0,0 +1,84 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: local-path-provisioner +spec: + interval: 30m + chart: + spec: + chart: democratic-csi + version: 0.14.6 + sourceRef: + name: democratic-csi + kind: HelmRepository + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + + values: + fullnameOverride: local-path-provisioner + controller: + strategy: node + externalProvisioner: + image: registry.k8s.io/sig-storage/csi-provisioner:v4.0.1 + extraArgs: + - --leader-election=false + - --node-deployment=true + - --node-deployment-immediate-binding=false + - --feature-gates=Topology=true + - --strict-topology=true + - --enable-capacity=true + - --capacity-ownerref-level=1 + externalResizer: + enabled: false + externalAttacher: + enabled: false + externalSnapshotter: + enabled: false + csiDriver: + name: local-hostpath.cluster.local + storageCapacity: true + attachRequired: false + fsGroupPolicy: File + storageClasses: + - name: local-hostpath + defaultClass: false + reclaimPolicy: Delete + volumeBindingMode: WaitForFirstConsumer + allowVolumeExpansion: true + driver: + config: + driver: local-hostpath + local-hostpath: + shareBasePath: &storagePath /var/democratic-csi/local + controllerBasePath: *storagePath + dirPermissionsMode: "0770" + dirPermissionsUser: 0 + dirPermissionsGroup: 0 + node: + hostPID: true + driver: + image: ghcr.io/democratic-csi/democratic-csi:v1.9.1 + extraVolumeMounts: + - name: local-hostpath + mountPath: *storagePath + mountPropagation: Bidirectional + extraEnv: + - name: ISCSIADM_HOST_STRATEGY + value: nsenter + - name: ISCSIADM_HOST_PATH + value: /usr/local/sbin/iscsiadm + iscsiDirHostPath: /usr/local/etc/iscsi + iscsiDirHostPathType: "" + extraVolumes: + - name: local-hostpath + hostPath: + path: *storagePath + type: DirectoryOrCreate diff --git a/kubernetes/apps/storage/local-path-provisioner/app/kustomization.yaml b/kubernetes/apps/storage/local-path-provisioner/app/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/storage/local-path-provisioner/app/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/apps/storage/local-path-provisioner/ks.yaml b/kubernetes/apps/storage/local-path-provisioner/ks.yaml new file mode 100644 index 00000000..31625a89 --- /dev/null +++ b/kubernetes/apps/storage/local-path-provisioner/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app local-path-provisioner + namespace: flux-system +spec: + targetNamespace: storage + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/storage/local-path-provisioner/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/storage/longhorn/app/helmrelease.yaml b/kubernetes/apps/storage/longhorn/app/helmrelease.yaml new file mode 100644 index 00000000..9b44c4ba --- /dev/null +++ b/kubernetes/apps/storage/longhorn/app/helmrelease.yaml @@ -0,0 +1,50 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: longhorn +spec: + interval: 30m + chart: + spec: + chart: longhorn + version: 1.6.1 + sourceRef: + kind: HelmRepository + name: longhorn + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + monitoring: + enabled: true + createPrometheusRules: true + defaultSettings: + defaultReplicaCount: 3 + backupstorePollInterval: 0 + createDefaultDiskLabeledNodes: true + restoreVolumeRecurringJobs: true + storageOverProvisioningPercentage: 100 + storageMinimalAvailablePercentage: 1 + guaranteedEngineManagerCPU: 20 + guaranteedReplicaManagerCPU: 20 + orphanAutoDeletion: true + concurrentAutomaticEngineUpgradePerNodeLimit: 3 + defaultLonghornStaticStorageClass: longhorn + nodeDownPodDeletionPolicy: delete-both-statefulset-and-deployment-pod + ingress: + enabled: true + ingressClassName: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/icon: longhorn.png + gethomepage.dev/name: Longhorn + gethomepage.dev/group: Storage + host: longhorn.${SECRET_DOMAIN} + tls: true diff --git a/kubernetes/apps/storage/longhorn/app/kustomization.yaml b/kubernetes/apps/storage/longhorn/app/kustomization.yaml new file mode 100644 index 00000000..4de74b6b --- /dev/null +++ b/kubernetes/apps/storage/longhorn/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./snapshot.yaml diff --git a/kubernetes/apps/storage/longhorn/app/snapshot.yaml b/kubernetes/apps/storage/longhorn/app/snapshot.yaml new file mode 100644 index 00000000..df7c34c7 --- /dev/null +++ b/kubernetes/apps/storage/longhorn/app/snapshot.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +reclaimPolicy: Delete +provisioner: driver.longhorn.io +parameters: + dataLocality: disabled + numberOfReplicas: "1" # Faster restores from the snapshotclass + replicaAutoBalance: best-effort + staleReplicaTimeout: "30" +allowVolumeExpansion: true +volumeBindingMode: Immediate +metadata: + name: longhorn-snapshot diff --git a/kubernetes/apps/storage/longhorn/ks.yaml b/kubernetes/apps/storage/longhorn/ks.yaml new file mode 100644 index 00000000..9e02ae8f --- /dev/null +++ b/kubernetes/apps/storage/longhorn/ks.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app longhorn + namespace: flux-system +spec: + targetNamespace: storage + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/storage/longhorn/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/storage/namespace.yaml b/kubernetes/apps/storage/namespace.yaml new file mode 100644 index 00000000..9ce8ef61 --- /dev/null +++ b/kubernetes/apps/storage/namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: storage + labels: + kustomize.toolkit.fluxcd.io/prune: disabled + pod-security.kubernetes.io/enforce: privileged diff --git a/kubernetes/apps/storage/volsync/app/helmrelease.yaml b/kubernetes/apps/storage/volsync/app/helmrelease.yaml new file mode 100644 index 00000000..6f04de4e --- /dev/null +++ b/kubernetes/apps/storage/volsync/app/helmrelease.yaml @@ -0,0 +1,40 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: volsync +spec: + interval: 30m + chart: + spec: + chart: volsync + version: 0.9.1 + sourceRef: + kind: HelmRepository + name: backube + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + dependsOn: + - name: snapshot-controller + namespace: storage + values: + manageCRDs: true + image: + # https://github.com/backube/volsync/issues/828 + repository: &image ghcr.io/onedr0p/volsync + tag: &tag 0.9.1 + rclone: + repository: *image + tag: *tag + restic: + repository: *image + tag: *tag + metrics: + disableAuth: true diff --git a/kubernetes/apps/storage/volsync/app/kustomization.yaml b/kubernetes/apps/storage/volsync/app/kustomization.yaml new file mode 100644 index 00000000..5e098843 --- /dev/null +++ b/kubernetes/apps/storage/volsync/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml + - ./prometheusrule.yaml diff --git a/kubernetes/apps/storage/volsync/app/prometheusrule.yaml b/kubernetes/apps/storage/volsync/app/prometheusrule.yaml new file mode 100644 index 00000000..880d6738 --- /dev/null +++ b/kubernetes/apps/storage/volsync/app/prometheusrule.yaml @@ -0,0 +1,28 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/monitoring.coreos.com/prometheusrule_v1.json +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: volsync +spec: + groups: + - name: volsync.rules + rules: + - alert: VolSyncComponentAbsent + annotations: + summary: VolSync component has disappeared from Prometheus target discovery. + expr: | + absent(up{job="volsync-metrics"}) + for: 15m + labels: + severity: critical + - alert: VolSyncVolumeOutOfSync + annotations: + summary: >- + {{ $labels.obj_namespace }}/{{ $labels.obj_name }} volume + is out of sync. + expr: | + volsync_volume_out_of_sync == 1 + for: 15m + labels: + severity: critical diff --git a/kubernetes/apps/storage/volsync/ks.yaml b/kubernetes/apps/storage/volsync/ks.yaml new file mode 100644 index 00000000..f909bb5f --- /dev/null +++ b/kubernetes/apps/storage/volsync/ks.yaml @@ -0,0 +1,42 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app snapshot-controller + namespace: flux-system +spec: + targetNamespace: storage + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/storage/volsync/snapshot-controller + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app volsync + namespace: flux-system +spec: + targetNamespace: storage + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/storage/volsync/app + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m diff --git a/kubernetes/apps/storage/volsync/snapshot-controller/helmrelease.yaml b/kubernetes/apps/storage/volsync/snapshot-controller/helmrelease.yaml new file mode 100644 index 00000000..b4e6822c --- /dev/null +++ b/kubernetes/apps/storage/volsync/snapshot-controller/helmrelease.yaml @@ -0,0 +1,43 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: snapshot-controller +spec: + interval: 30m + chart: + spec: + chart: snapshot-controller + version: 2.2.2 + sourceRef: + kind: HelmRepository + name: piraeus + namespace: flux-system + install: + crds: CreateReplace + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + crds: CreateReplace + remediation: + retries: 3 + dependsOn: + - name: longhorn + namespace: storage + values: + controller: + volumeSnapshotClasses: + - name: longhorn-snapclass + annotations: + snapshot.storage.kubernetes.io/is-default-class: "true" + driver: driver.longhorn.io + deletionPolicy: Delete + # Ref: https://github.com/longhorn/longhorn/issues/2534#issuecomment-1010508714 + parameters: + type: snap + serviceMonitor: + create: true + webhook: + enabled: false diff --git a/kubernetes/apps/storage/volsync/snapshot-controller/kustomization.yaml b/kubernetes/apps/storage/volsync/snapshot-controller/kustomization.yaml new file mode 100644 index 00000000..17cbc72b --- /dev/null +++ b/kubernetes/apps/storage/volsync/snapshot-controller/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./helmrelease.yaml diff --git a/kubernetes/bootstrap/flux/kustomization.yaml b/kubernetes/bootstrap/flux/kustomization.yaml new file mode 100644 index 00000000..1d9ad47f --- /dev/null +++ b/kubernetes/bootstrap/flux/kustomization.yaml @@ -0,0 +1,62 @@ +# IMPORTANT: This file is not tracked by flux and should never be. Its +# purpose is to only install the Flux components and CRDs into your cluster. +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - github.com/fluxcd/flux2/manifests/install?ref=v2.3.0 +patches: + # Remove the default network policies + - patch: |- + $patch: delete + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: not-used + target: + group: networking.k8s.io + kind: NetworkPolicy + # Resources renamed to match those installed by oci://ghcr.io/fluxcd/flux-manifests + - target: + kind: ResourceQuota + name: critical-pods + patch: | + - op: replace + path: /metadata/name + value: critical-pods-flux-system + - target: + kind: ClusterRoleBinding + name: cluster-reconciler + patch: | + - op: replace + path: /metadata/name + value: cluster-reconciler-flux-system + - target: + kind: ClusterRoleBinding + name: crd-controller + patch: | + - op: replace + path: /metadata/name + value: crd-controller-flux-system + - target: + kind: ClusterRole + name: crd-controller + patch: | + - op: replace + path: /metadata/name + value: crd-controller-flux-system + - target: + kind: ClusterRole + name: flux-edit + patch: | + - op: replace + path: /metadata/name + value: flux-edit-flux-system + - target: + kind: ClusterRole + name: flux-view + patch: | + - op: replace + path: /metadata/name + value: flux-view-flux-system diff --git a/kubernetes/bootstrap/helmfile.yaml b/kubernetes/bootstrap/helmfile.yaml new file mode 100644 index 00000000..c4c3983e --- /dev/null +++ b/kubernetes/bootstrap/helmfile.yaml @@ -0,0 +1,44 @@ +--- +helmDefaults: + wait: true + waitForJobs: true + timeout: 600 + recreatePods: true + force: true + +repositories: + - name: cilium + url: https://helm.cilium.io + - name: coredns + url: https://coredns.github.io/helm + - name: postfinance + url: https://postfinance.github.io/kubelet-csr-approver + +releases: + - name: cilium + namespace: kube-system + chart: cilium/cilium + version: 1.15.5 + values: + - ../apps/kube-system/cilium/app/helm-values.yaml + - name: coredns + namespace: kube-system + chart: coredns/coredns + version: 1.29.0 + values: + - ../apps/kube-system/coredns/app/helm-values.yaml + needs: ["cilium"] + - name: kubelet-csr-approver + namespace: kube-system + chart: postfinance/kubelet-csr-approver + version: 1.2.1 + values: + - ../apps/kube-system/kubelet-csr-approver/app/helm-values.yaml + needs: ["cilium", "coredns"] + - name: spegel + namespace: kube-system + chart: oci://ghcr.io/spegel-org/helm-charts/spegel + version: v0.0.22 + values: + - ../apps/kube-system/spegel/app/helm-values.yaml + needs: ["cilium", "coredns", "kubelet-csr-approver"] diff --git a/kubernetes/bootstrap/talos/clusterconfig/.gitignore b/kubernetes/bootstrap/talos/clusterconfig/.gitignore new file mode 100644 index 00000000..9fd0998c --- /dev/null +++ b/kubernetes/bootstrap/talos/clusterconfig/.gitignore @@ -0,0 +1,4 @@ +home-kubernetes-k8s-control-1.yaml +home-kubernetes-k8s-control-2.yaml +home-kubernetes-k8s-control-3.yaml +talosconfig diff --git a/kubernetes/bootstrap/talos/talconfig.yaml b/kubernetes/bootstrap/talos/talconfig.yaml new file mode 100644 index 00000000..eaffe44d --- /dev/null +++ b/kubernetes/bootstrap/talos/talconfig.yaml @@ -0,0 +1,202 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/budimanjojo/talhelper/master/pkg/config/schemas/talconfig.json +--- +# renovate: datasource=docker depName=ghcr.io/siderolabs/installer +talosVersion: v1.7.2 +# renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet +kubernetesVersion: v1.30.1 + +clusterName: "home-kubernetes" +endpoint: https://192.168.20.60:6443 +clusterPodNets: + - "10.69.0.0/16" +clusterSvcNets: + - "10.96.0.0/16" +additionalApiServerCertSans: &sans + - "192.168.20.60" + - 127.0.0.1 # KubePrism +additionalMachineCertSans: *sans + +# Disable built-in Flannel to use Cilium +cniConfig: + name: none + +nodes: + - hostname: "k8s-control-1" + ipAddress: "192.168.20.51" + installDisk: /dev/sda + talosImageURL: factory.talos.dev/installer/88d1f7a5c4f1d3aba7df787c448c1d3d008ed29cfb34af53fa0df4336a56040b + controlPlane: true + nodeLabels: + "node.longhorn.io/create-default-disk": "true" + networkInterfaces: + - deviceSelector: + hardwareAddr: "bc:24:11:b5:dd:1f" + dhcp: false + addresses: + - "192.168.20.51/24" + routes: + - network: 0.0.0.0/0 + gateway: "192.168.20.1" + mtu: 1500 + vip: + ip: "192.168.20.60" + - hostname: "k8s-control-2" + ipAddress: "192.168.20.52" + installDisk: /dev/sda + talosImageURL: factory.talos.dev/installer/88d1f7a5c4f1d3aba7df787c448c1d3d008ed29cfb34af53fa0df4336a56040b + controlPlane: true + nodeLabels: + "node.longhorn.io/create-default-disk": "true" + networkInterfaces: + - deviceSelector: + hardwareAddr: "bc:24:11:0c:fd:22" + dhcp: false + addresses: + - "192.168.20.52/24" + routes: + - network: 0.0.0.0/0 + gateway: "192.168.20.1" + mtu: 1500 + vip: + ip: "192.168.20.60" + - hostname: "k8s-control-3" + ipAddress: "192.168.20.53" + installDisk: /dev/sda + talosImageURL: factory.talos.dev/installer/88d1f7a5c4f1d3aba7df787c448c1d3d008ed29cfb34af53fa0df4336a56040b + controlPlane: true + nodeLabels: + "node.longhorn.io/create-default-disk": "true" + networkInterfaces: + - deviceSelector: + hardwareAddr: "bc:24:11:a8:19:33" + dhcp: false + addresses: + - "192.168.20.53/24" + routes: + - network: 0.0.0.0/0 + gateway: "192.168.20.1" + mtu: 1500 + vip: + ip: "192.168.20.60" + +patches: + # Configure containerd + - |- + machine: + files: + - op: create + path: /etc/cri/conf.d/20-customization.part + content: |- + [plugins."io.containerd.grpc.v1.cri"] + enable_unprivileged_ports = true + enable_unprivileged_icmp = true + [plugins."io.containerd.grpc.v1.cri".containerd] + discard_unpacked_layers = false + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] + discard_unpacked_layers = false + + # Disable search domain everywhere + - |- + machine: + network: + disableSearchDomain: true + + # Enable cluster discovery + - |- + cluster: + discovery: + registries: + kubernetes: + disabled: false + service: + disabled: false + + # Configure kubelet + - |- + machine: + kubelet: + extraArgs: + rotate-server-certificates: true + nodeIP: + validSubnets: + - 192.168.20.0/24 + + # Force nameserver + - |- + machine: + network: + nameservers: + - "1.1.1.1" + - "1.0.0.1" + + # Custom sysctl settings + - |- + machine: + sysctls: + fs.inotify.max_queued_events: "65536" + fs.inotify.max_user_watches: "524288" + fs.inotify.max_user_instances: "8192" + net.core.rmem_max: "2500000" + net.core.wmem_max: "2500000" + + # Mount longhorn in kubelet + - |- + machine: + kubelet: + extraMounts: + - destination: /var/lib/longhorn + type: bind + source: /var/lib/longhorn + options: + - bind + - rshared + - rw + - destination: /var/democratic-csi/local + type: bind + source: /var/democratic-csi/local + options: + - bind + - rshared + - rw + +controlPlane: + patches: + # Cluster configuration + - |- + cluster: + allowSchedulingOnControlPlanes: true + controllerManager: + extraArgs: + bind-address: 0.0.0.0 + coreDNS: + disabled: true + proxy: + disabled: true + scheduler: + extraArgs: + bind-address: 0.0.0.0 + + # ETCD configuration + - |- + cluster: + etcd: + extraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + advertisedSubnets: + - 192.168.20.0/24 + + # Disable default API server admission plugins. + - |- + - op: remove + path: /cluster/apiServer/admissionControl + + # Enable K8s Talos API Access + - |- + machine: + features: + kubernetesTalosAPIAccess: + enabled: true + allowedRoles: + - os:admin + allowedKubernetesNamespaces: + - system-upgrade diff --git a/kubernetes/bootstrap/talos/talsecret.sops.yaml b/kubernetes/bootstrap/talos/talsecret.sops.yaml new file mode 100644 index 00000000..40854562 --- /dev/null +++ b/kubernetes/bootstrap/talos/talsecret.sops.yaml @@ -0,0 +1,43 @@ +cluster: + id: ENC[AES256_GCM,data:QbjE9IPiR9tJPQhj1C7blaGnBhxfrhSxRbnQ0EaavO3nMZ5ZmLSAhRNmsO4=,iv:rSDdOW02IsMDn7d1Wp4nj3IuXcXnEgAhTDXcLGqCTes=,tag:rvtrjbXkrg+j0v2xkXyaZg==,type:str] + secret: ENC[AES256_GCM,data:WzkHpllRwM2bDg+luHoJyY9M1sJK1wv2DaOzNAW+CBG8sXOciMsr2D8gbZE=,iv:TEWExl8k1KxJNm49bHQ7WUkYV8VyW8breQiFjxxJ8Cc=,tag:NGgoTWVXxkb1PjPge3Nw4A==,type:str] +secrets: + bootstraptoken: ENC[AES256_GCM,data:9Cv7bdGjaYaPge6bkp8BWi6vc8nax+I=,iv:GVdwUM/3nSAQ6NjTVJYkgEmUQ0CvPrkth+Rbrx1Nv9o=,tag:gHYWBgxe+rlf0+x4gxq5iA==,type:str] + secretboxencryptionsecret: ENC[AES256_GCM,data:vTLkg1GwsF1+lpfNk/is5UTsKj9d+ZtHwCs1c0SMaazT/kA8iCIKiv5Lk+k=,iv:WVFQWe82dPcfytPJqkaPb3BglbS+65GFAgVhiqoMyq0=,tag:4CZWulQHowfRPw8cfE/MVg==,type:str] +trustdinfo: + token: ENC[AES256_GCM,data:tL3MCYF8IAlpJCkOp99uPFIaIAedTvs=,iv:pTHRGFxMj+dSYF4UPWTGWzEQrfHR8MLXqZr+5rlFq+I=,tag:6lvc0E1+uegGM6d6DQpkVg==,type:str] +certs: + etcd: + crt: ENC[AES256_GCM,data:C/RmLESEFlw0dJExGIG3NxthZlr50zsej6srQpcrKkNg0v4AAfwCtyAvpX67w2a8/ivsErX1dFifaMDW5yJse1tbfUKhEZ5ZY0ZmmJkALDJMmS0SfVyIw3Hg8lEJer7JTCY7MYC3cFDPez5Yml2Y73ETiak2RB81OvFcItQseNsmo+2Ut5zgDP7MwW5WaDoaZVQcwMJHYp/OG/JD6UE8VwmLT0H8eGXowzNlc9OkD/UHlQbimRBCUykZE5/xOC07A3lqByZ2zNoDnD9sBbgRJ1cC5gbL8GNuPTm6Yu7vX4snCmc3vq954kYz46nHApj7v0+FpNg0f8RckLYRC8nRqWldnduwatCzua9AXdWmkftUCQgkNoUYF/MnGoINo6exJ5GaW5RNMj/qc9fTzHI7NArRhzAbe5y0kQo0L5/TtNSvTj+nBZ+beGwcTvQuzNKcRkRf3puOaPWwsjYAByiUdiqBEY8qWU3O4ojZkw7/ISd2qh4mgkySK7UXyCg1GFHCi0ywoILMbEqnk9sjCZuIilVUOEyghohJkjhGyviFcIwLvdaxvW8vv4rtIKc9e2FAHQM0RjY7KpXGxSf0vicrFfBR7ZLom0UK6Qa8p+3hR9TKNuySxgiclOM29qAiL6tDkQSEvsyCZpmIkkIWkTB4DjG2jAk3CvnnnWpr0QrdW6ES17Uq7q8X4iyCsZoUVCvPnS7XcEvnOxjMqRA4Ew+AyQgMFwLAko0IADK6TB9XPymDraveDFiRTGG4DhyZ9eVWJnjZu9k8PvHMHodaVq3UMWbYtz97LmIg1uAfl9C/L4sqf5F4WpuwwTZiJRatPt0vx6bKKqXkSN4ZjGr0FZBwSBd1NJD0ywvT68XaHpK9hp0D90oCWkIpX5zvZGRFtL9zcsMx1fnuS09rfNXuQcVGO1mqBwNLotqfuL7mEz6+POkAfgQbZuJGrkMv02jMWKdsg4KL9NCuLAQG6Axn8FklBJ22l2UFDTmyxVKKmAfPpAhC+fmERQNVivRYQNShYLqdICtxeA==,iv:oVeXHANV06t1BsVYO6mM168HH5/yyKY52qW20UVeRQU=,tag:QobVf5D46rKfC0FiZHhGBA==,type:str] + key: ENC[AES256_GCM,data:fVZwnBW6e1+Wf2k7qM9wjfh7vW/70UqTOG3wsfRvQ9OyvjeDgP6eJftK1kQ8d9tsvyE34+zX/XnrLH3fhco/36594zvq+P+3wDtDBOBj+DtjF5/+brO/u2LnWtNc5WoyRzw4FatzKLQIdN+Fi9jsUKdiAyBPot8cXMi6av4kyXl1ZJRzWLWTQ2tLnhCRHWOTte+tccD+7dpwikP1mqM4nOH4bYwUKXWn95Py2IYBFQaMVo5CzW5Mb7GdumLItLz1aIivQxDXSD4uJGXIdITtH5x3XnIawU5B6dvlP37qdtcfEfUM4DNYdm8f0oFPOEqo0/dkk8YwYko+AeJXKcajMD17kWeRYVuWo0FcT//6+ZZMIhlGa01OXUxtryxym6e7K1/TmZFdrLm1evqTooDZOA==,iv:kRkpwMPFviKYzN+6LB6RNQVmLNaUnUpxLYcz2kOdwUk=,tag:ANQ6nkyd1wkE3A32GCBXfQ==,type:str] + k8s: + crt: ENC[AES256_GCM,data:VNIWvJZD3yDPt0NN7sqOFKinYICxH2EY26SoiRcVaRlatltv4FebwpgXe+bl1yCznrGM/S3fvmqPI8zpRXaafRX2R38kDaaNyJwGkrsftNA7QpRl9FBxqDswoL7qOFzuSA4MQ/8iQlpdMsdB3AcniFKyrYDeQawmAyBWT/ailQ5V1DkoB1cbld4e5u+9wpqVQUKHwmFT0Cv0zOk7p4sTsw6wmDnu6ofUfs7sWMPT7MdCVepCgXHlPMirPDDl40KR9XAWL9YwBvhFx+hZWgfK9THOG6JNYbQ9MGT2QwiFABXyht/ldptxYlDlU/sk3pkbkNcz89hn+na3WNbBbXp7RtUG138HEq7bPCtd58kmFIpU0w7Cbz3kvWeLnZCc0DaCzkMH8Gia+0LkK5j52465MMXYc5tNDDX3vjwGrVB4aYx4HHIaleseKwjAgQeqxExZBdnFjS8J7lshOFYFHwROtZJPlDfRaGA/iBd7CnOjXjNHtGYzD7Xyy7Ek1fHF5Q1vWw0OudIYWrfHI/3G+yJeK+pAI+IA7/irLTp94jJ/aGm7Ty8oEi+uue8+cTFQrUC0Q9ASg00YYNX+yeZjCPtYcIkn50OKHGXsIx6zvfHNk97xCZcNXoPEtQ1fph5vlIOgCe2QN56jZcuG9QNWJgQ1Y2f0AmEl1M1rlwb3AbcI6UwOP57+oE9JwAzN+mDg/L8i72jLwbwwKe6lMKWCB4K+afNMzvsICIdzG4m0VWLM5irHdirK1y6CEr5UnmJ+MtNTDjqbyTq59zqydxjDpb4L4ICTOtkV+YJKh3xjetcAUjToyscHXPriXJAfgRkf4q1k9XNQ9HT/MtLHPT/zVDDfAoDhj+FlV8uIJRFpgMLmKu0KnRXSKLMW5wDTlAeRHrhMpc0W5hRw17+jVca8vFOq20k5mSDG7T7Bm9b0qlBSYb4NLMUcD+FBpoLBskI+tFnQR30Gc3qMZCYJFCHVQUUmVGEemNIB3KdF/0S9ARKJOT6C8aNWtZgMrrSUXPRdB46C/p30Cus+gwzqNQidy7gvNlgGmQo=,iv:Z0q8KsRr3d631InGWONKHyV4Wr2iCnK+8KDeX9Mm5TQ=,tag:OpYwMOKv5tsfUKk/fJEFYw==,type:str] + key: ENC[AES256_GCM,data:sGWbI1SjTmikn5MgiMo1D71oT9WssJlUOpUuGKWpTV9h1S8CpPGGug7YHul29wqSWF1pZk40qBpCXZ/Ab2pgkRdehCbis/Bx0MjMQZc3iT1svBlRt0jjuPLqkwXD6R8pjc9aqahn41Sl2lqO4EDLH8TpO+JsqWyQ8BFqPTVTSiBrxHfFKjCb+TH6H0hD48OVG65AOSD2CvX/JjSZe7jYam4JBMhsPQ1VVB8O9KwIUl4OADSlTuJwQIxSIZHQIVDcvhrElLMozOc1JQ8FrrW48zrnOIBEMHxbaEz6QpNFnM95Mfic3W4G46PE7W89VR4TZNQ6HyeJJplxa8Z+YHRCE7VJFMhSfzUTZYy9jDlF/bDqFteINBF+zSZ1IdWT3C4nKieTvsImvF31YG3kWlia9Q==,iv:B1iQcQ3RC62dzBMCcY3B4mNg4lCeEcZljgMG/RW8O/Y=,tag:Iqwz72NVXSmkopzWSu6T2A==,type:str] + k8saggregator: + crt: ENC[AES256_GCM,data:Yayva4S55WTYF+tGBkNfBsIi5+PqVM7Se7DXBvgDy7IrbGj2Sy+rxIjSO4lRS2M7nnsnTc0wIp7bwqFbgTHN/LqLAdVz3ZqPfZkSaeFCuJuQ4ZNM3qY0DmEiCHyu9EOBA1Yfe4F7v+Y9c+xKUK7SVVUMvBBvcK6FIQw2Ho2Y5XoJvS/Zop8dqr03OSxg6bjkKYt49XPU/EItH8Xyas1EWYZ4ciYSa4Mmz1/xu1L7vTicztI/sBlf0eHujPU26PPaQgxLQRr+Bk4oWFvqjR6ZRXUFGDicE2cM+wzKo2O95jG2ku0AW24pbbsQ9WRZX4SZ6G76Q1xEq34GgVvyj1ZvywMJh+wLZ3JGtu+cfRFxjba6vprOeAd4VuX/TOQroJoPB9MyIOb4ZcIfeGWllD6Myv3hUUh4dAof7CHhAQxGkTEjq07YYKQzQ5QlOvdgh69owvh5G44ug4+uAXbqVhX2x1aJRM+94JytKuahwgvxvpfOOWMWjBC0YzXQbAvQy320hjLt9k/lwiHI5MZwIfLsXLegzXhTNMHb7Xm2zrHcPMdpe1vovXDPDgJCbY7O+4bvTvO6xwDKYDS9SVh+tYE/aCaWaWj/4vYIlFpc8CPnKLsyLn9DNbtF5Gwj2k9TY/RWg9hTcuR0cGDhHqET3B4NBv4SDJXLPlg6v0DYfstGWh1NT6PG8DbxHc4q47WQoNmip/Q8A3asC9tJba0k24zBz1ubPrlawe5XxY9SbZUkdup5f1Eiay1CkcFPplEKpwQcj+ph4/0V/tCs0nm8qEQAner7lOO7RHve2H14zu+R6plNiKiYHGs+Nu9eEmbIXmoN/e7Q/HhpOAnYmMn7V00UHZjCm/5jDLdKONbt3FLHtFnDZbQ+epfNZRZGVTvTKoYWMJJXHozbbdDVgfHLKoWfONRa5ShyXeesYQiDwOEiuyzAMt6+CPNLJ9OrTcFOoMA9,iv:d8d65vq2XXeegEfN0eItNt764tXTbuNw06vNsjf5rcA=,tag:l7Ex5rtA5PPXf0DLbQQw0w==,type:str] + key: ENC[AES256_GCM,data:C9SsEWtBg1mnfW/bWjp7417VWmTlU2CggHDjutDLTamJrsecWREGlNfB+ZwyTAZb1YaXUNXuynMwBWW+MIsOFRTFe+FlfpflEROLrqYfP9lH+6gldVqnkixxocP1TISNMnjVAeUgZap9WOHs2rYV6xYuY2X3H9CWZ2ZaY1rYRvmIx+VWxa7VEuz5NPdB4s+ri8JFj/bFE0e26sHQdCgid0nqKobVk8AlvxkwxOYH8u392KNXwZd7d1oonB5t3nEXyXO0SZ1iiBeSVHjZ6xEOL5/26braJVYd6dius6zD6tL0vffBAAbGACFqRpn+sfo7J/ltiEJN4UDiQT7Pko6anHQFqFm86NilS0krMnP/y1S2u8l4WNWddQzJ917yODnkF6iDYycjw/5Ra2r9crkMEA==,iv:MfIk7+p66fyq8weFMQpOPpRkkxTwxRzb3SjbRwhvMdw=,tag:rtpIt2wtXXLp/RvYS2mkNw==,type:str] + k8sserviceaccount: + key: ENC[AES256_GCM,data:j930E4vkazSLg9dGEjw+5V5sWTHCf4QEm7knukRRPLSFRZCfIx9NaF1bebRA0PeM66PUWbf+HBb1X7C//Q1BpNSAylG1fYg5hsnrTp7s3fDKUkcoCd6LgSpHUDkFV6ki3RCCI4+xYPKBbrNza+bUaoR3Iw26JaVpn98D4S07IFhebQYzTTAFJbzqrZbmHgclL7cjgdU2oBIwYKlFyIzO1yRpx27iUDD94XeoNa4mLTIeWrhXr3BoQc+MRHwuYN4q0oPO+XbUejmEMi8Y8pTJYOGqqg9OWNxdon1eAqz/knUzjGR5u2ATlMFUyNH9JQqps7Wv76v5HaVpGzHs2eE4kwCTTd7E60kd73REwDWpu9/kLfUyBbGBlo0acarCl3PRBcVyYgmWEm9JYH9PpJCYe2OHSITkWUXazzKna12Hbr+VXjvmv+Pgs6wKaQjNmIPiDxBmT7AxyWGJpv6bqz8rg67FgdPRaYLJvkKnKz3mn3xcLvTMqadL13YdIc0vxjz17xWxxgzVtFlBYic5e77qy23h0mRg5unmxwWLOKVJoxf2+zRDcvNrN5uvdLVRSVbvN9DLM24Gaa3VvCHVhm4y5I0KQbUJxg5PTNXSNdcDANwFtrwbfUAtkYwzw0p9kc+OGTK5QMNMHJtL2go1Qftuh1/EEkK+qj7mZS9fkNjGisnNxEXAB1WC6VWBMjbEZ1RleSCvSOMerkk93W2U7t69tPeLwKZHHR6wdexyjN4P/HvM/NOpisza65Y2P1Lyb9WG6tm8+mhl+kradcdcD0cSPGlQBAWJXOARW3wSxdpYa+3Slt5r8l+bT5oT1FX6kGZZfEzr++KFUlQSwAYJulkKXUdse+CO6k2Nt7pySGpupadgYRzabCxwTh3AvVWmg9Ap//2povvypx33TLSr/zaeanIb3jYuRkgMcWIQ3irQjCTVroaBYtNqUD0oCgw/O+CHJzb1GX8Ulds8Aqc8RdL0I9jO75LnIq/dDSeWA8Z+6feW8vqS9CMDzRnWQV3XNjBAZq4i/7qFxLUwnkgwRPB8Em0IH7GMs009MlwKXHwItOxDVLlnCkzakPQPYf5Lwkoo8Qzf9CteeY9erhGlvHE5mj5+NLkoh/EylMCNrK40aldfHn0IRRLTrpp7pMoY/BshUQloW9fanDYWUflMnd9Vp9MU2DPiEbjFIAHrsDDk7/UAkHTjKd2l3x4NBBMAlBkfh4znFA5/BR6ahkSeas68Tu31ZgeVXY6IJOfnvt1iCikgw5sGe4vMvZyA/rkkZZCrTEij/WYlzhwvHOVTVnlm+bk6xv6E2Uvn6nWFJCp1XIYnPP14Z3ABG5kCXVjVJHREraKEhFXqMUqkpCT7CE4aH7EQe53pkOymKx+JCPOgum1wzLZKjVsJKc9jl5FMcV8ycHGHFzLSynGApYAYmYkzZbmVKMi2YR73StYEc5hKySNej10al5jxkb5MoYw0LqOa2ojvkPuR7UDPi+0G1qltsIYldfQ5MZfupXKCfbJ4HbHMP9BnFgTv6rTFUqjuk+lFg3mCdUEsQTERqi7PoBuaq70lyd5W2IOxZqrF8SVD6Cza+It1yxlWTiq6Lgo3tMOiyTKGvkuUf/A52y8VFrOOUUZySNpbqF8M/ru84eXiVzG8tDnNiDy1zSdDVChY80wZd0H9J+RSR8ODfF+JZQt4GEgmsU5cBn9zuftddc4RimI0tdhtorxA/xqfRKiZ2hpGyU6ULyEb6TpK3dkK8UEkSf71U2Ttr/lMdkerEteZ2eUlDY5fkiE59gN8oAZohV1KCTCpDG/ZPOSsyDkH+IJAmabPJGnTSsYQI3G69KdTUSu7AS9cIP0H3BNe8cHVujAB/DKQ6RW8bjVI/7f6cotDo4e2pxJ1wq0Tx2uvNBYLi1YUklobeDQkc/SsQiZDlL2BVBBMmMBNIgqVcEKFxF9V6tugrwEoEIYlN1aoJTypoq0bRbyZ8eCaf2ilmYzoFQxXhqM3mkOyohqsKqLLZvAq0hxvSw64pW6SN0Bnw9kJKeZw578XfnIKVzzHVQCyAfhAoy708sp7udVXkI63v+/tc4XY2LAzedFkJlp1Prp+WgRCSH/Cn6oGZsl6fdR6V2aIvoviyQEZnmWOyYVCtIZC/2GMk8lAmsXYw0DbK0X+JA9pRl6e7Bf+fmKHhwFoLVh27+SZi9JRByOk5/O4XKtSlfq6c2AFp4iYjdYpMXMmeBGZmuZEAkXZmX7NscBZ3obxHWcgM8zAV+N4q6Hc24tAaJ7pXkmAQWgxCDV8LJPdbOgn3wG/GKEHKBdpLVYV8NZsMPwaam9vRx93Uo79AhsS7+A12zw8Ko6lCnXlD3GksvhNbkx+mHEANkn7Hgz1PAPAYBLkRluOKDi40q1QvvJJg01evFNkQouPieQrpa20wqzC4rPVcb2f+GqaO+Tu85ycrcUdfZXwWMRGIcCNQEwIVchBeKeMzSDL3fEJc2sPi9KxCEg216PE75DZZJDBGT/tVRHbrQfLQtcRNMevoF2aVrAWmOX7enRhvs5RS1I48j38sNzylS0y4Ae8amUkj8TIzG4HYyRnxZm88cIUVKau4kSEmAL/FCQYfoQeHOyrk2w44JpUOe83NHpeFSXdMbrmc63jKYF0jIBciozBurAgdb1bjF/6KRejt/rhR7XN5iSt5ER83dAZJ061LMQ284wsQRmeFbFDzEUtkUgOKkj/ZYq6Cs3QKQBzcyXHslLQeb7HzT66ZjA3yiPmKEHnntnWatQMleKgRGsdG33x0qCBZBZ6LEYxsnkGnOyuOZUN2HilNXgonqI+DigjCxuZgTCQB4+7gD6VFaRs1AcjAplSHo8it/YJNHCmTa6gXShH2c7gc1ACpoxO4c2IaxD1Us0h2uKNdwkWsMv2l2DO8+I3chCSX8EuGm1RYwlJlzXlnEqIBkSJnbEVYipD+X01NPwSyg3YqX1NGd+8qi+ZZXn6UmItIJT2fXe3VwmB/VXCBOwJbvAaJfrSQUN0lCe34tQ5qF7Rfh9P74TlcNZtt0xMTvUIp91Ohe2BK3CHa6MmoiE5hVvJSkOb/uCv/s3BeldKeDxFvMJpQ77aE0P0fC+CByavhV3PL9JYwAx8dySgRb7giPzxceR187tEtpNWm89dE204MbkedGyBDxMUn4DWD/7UHlprquo62f5JvYNY46nJP6YVN1W6q+80QAvBtSYpdD8MGR6g9KRXRz4WaQaxoAakJ9t/fPyZILJROEzJixco70eSXFjLNyVCtVtqSveIdyIT3dc8zqv+FJCaquFJ2G08D9kytCUJRkCcvFxXJ3pUyXawzS94qdtCsmuyM/KSyEJF5nYRmnYxJvxWhVnJnqZdTIxlwFUnPuhk5X/sNogEHFqlJjTPb0Kb4CdqSpjo0QzZ35HuekRMeAFDymzMpQu9Lxco1913Ql3ev8COCeDY5J9hSWvVLDVZjjahWXiabzbsOr+69HtxIIyyRs9byUdhPeE3+L5v9O/vo4Tj8sHihY92Md5s6G96qRFgJ6g0zNs2jSpr8tP/CTUkdHaw013kTsjkwOx+uc/kciLXdYcg1DjwKK3i9aObHE+YPVtMEA4vLTvAE0d5Q53RRxKG9qWgGCDPau8VX6Oqfzdq1ZWy6wFuxQvqjx50VReAyE4wPzTXJ1eC004f8VUsHet9t+Xds352IYvzPIkn1071Hvnd0fCeXeOtfse9tKOHoc8mEOs9YfFWq/4NG0hce8AjyKAlc9SkEwgAi4whJAJV/s3Vzu9n8JIIblMxjzpcR1BP97qEgODC3Rcyv4pNu5izKVtvxlyILNdBf8HVyOxZGxO8u6Y137bisqApe7crbYvlC5FTyO6EjHJzSPMTl2O4M8s25BrksQCKLG5Tvr6eE1zid8goaPNK3z956gzZIFPJ2z3xRhk2jV03gfF8G7L8jlGrpa9b5HKgMg/9LgUQq815dSJGMK5MhA2kQQoEwWReZtMEdX7E0OJgbJPbB07ATsrEkd3O5zuvWlBmWC1LSsoMLzr87+Jb9zy8wYv5yh2oM8SlmL2wwpFvxDrUNLiltQxlm6kitJJz0Ckcw7hLtJOK2AzvGkryGifDdvEHqphV7J96SaGP8rT7rY7niX3NuGYqaBwg2PlPs4YuPrWcKaZNUEZprH3+uvczvV7v1XD0LtSMfx5EtZcB90QpGW8QCMDxKICWQPjknY1t4ibcY0yph51nz3MA8k33LHirMYHGyHrZnrM9DDScyqqQ0a6jitnd1PdshtwhliCZvFDTVro127KGxeOOAZv9DBmi2By9IMSvuDDuWDf3sHXethcP9SMoeBQ7XCjlrO+eMNzRV6PPwTJEg6+JHCiVgGSkfSXeTcOxSCfQU/liUExjKvFUtX/R3OhP9zbVjT4hXGULyC1JGBlJbGxzuM/MBx6Qoju4j8Xt/uVvU1KMoDz9ik6J48X4AR8iPgTsvNhKkrCuZ9dQXf3+bxDNToj/wqubZNeJ4FDhiZNcMibeyFmwpanqiC5SoryD73JVoF+N51eg83Beksj6oO0fuZjL/dMOTVFeUWI3/t5gXvxMLqYWwo3PYwhDgcy0vbBpf7wd77YQgF1iuCevGOItnFXnpBHEHC3rYVNvCAX7RDKms+CeTHwiCornRiWvLmnQ1zMgYN8RT+mfKqVv3gUMv8XMvEbOY7PSG8T4uMzyewzvXawkGojF3rKau9gLGwwpo7uclUq927TAYkT4C4n0aUe7LFrlKrillO+sTAHxXbmKaY+rwteHKO1Bbwoxvak/1aVfUmE+bovynsHDJnUbs+bnLVk9zUYA14SST/I28c/0cNgayTnbtImoqTZNtaES0kyIxJZZpEGXKvZ/ODpkTxJKSJCSDLKULMaXgUSxvk+bJ8HNyXXRtGORgbaoYeXG4a8z/PpDZiV/WKYB7rfn4uGy5ieJTHMqGpNS5hDz7UJwFPJwgU9R6nS2H9wrBhv7f5Z/E322MFT3BbIKYZ6mHoBjkqF18RfAoFryEkvzY5fuPkQAF1VXqvS9j5UL+SuPsWkKOGRlpwhcsws0kWj80Pl5G8BkwPKvjTX847mrfXyZcspivmcriTtTAYI8jRZ40o1O/XsuEB0Gw7fHTmjWiekToYT5XCWSxhrUJPKP9Rom/H2eCWXp4NIfm/O1t3jBElsPzv8yEnlgGcYIpQn/JYt7bGggPoh5pNLAyfKXKzkW69cRZjnTeUXhUlBHz0Odg9YFYiPUwh8bkxlcLRvNK+PYiRDjcjlr0vmrAH0pB4UorgJL3KAt7ZQsNErAGr9j49pMY9LdrfesJ4OcnSEneHrl4uxhX52M6gHlPDu2eVp+yl4E5McoALP1okwN6qhCGKqeUrBQ2uIp9DOixQzd51RkOh/aGj4/bCv5Zr+1IPG0+nULeE8hFaruCqVrC4lN3PkKK1BZSMHRjA3Pm4nHAHN/5K5lhtx26y38MhNkhBLPNjSkxM7MrZyl6+hZ7draewJm5U8Mx9hVCB9sySvlFInggY/G8KjdHQVyfNTq471djbU0vnrHwWz7YxwxeM3axFKwDr9hgGuSVdoeEEzFREQyTKmxB6pfksfuIZ/ejECU7Sr0hMSF0q6pq/kKkj5HIDw38KlaLjxwojr5pIGUA9pWo1sD2Mm8f5okmD5J3Gd2VL3AG72y1ZyzLVq/7UAicLrc3DTADN5sU+Pw9QFN4VdTHO7hEKtohQMOnooi8T8k+qLVseOP8cReBofqF6Xfeq3qdK4R9wLEshN8pigqOa0BTCgmIHDlZX9hVQ==,iv:kXHxGNjTXMS11sRK2XYVmClxlflUGtFKTSYjeEmLeIU=,tag:mXSvm/LLFQkeYLfh8BNlBQ==,type:str] + os: + crt: ENC[AES256_GCM,data:itxTjHaFxmzH+vfALnZaF08xtYca/tVNc5fbHuSSCK1hLiwmGfoefoiG15yurvrUdizYlK5w3086rmvoOUlUw1n0ecywavvGxTKZu0sOgTuOYKBakpTr+gux/le4RmCWbGnboMyDmnWBKJefbyp32jk6tuQIMonkOCBW6/D5urg8uZA4zZh/fT994jfQAFSoWtIDzFOGIkEG8SipeMGI4V9JDlXAgpGdmj3DBO/uaZLkMA5uRd27KlrFFNpVhZuTyKfE9jYoF1m4pOEL3B02E1agN0puk/r4MCopuQ7CXwe3YX6b40YifMKUvC5QkKnSH6ZWjj8XJnsiwNUSHJoogicqtAklRDj6Y67n51U+ljzfUmKt12Ece8qWj8JpyptbrRR8GCzX9O+9X0RR3CVRJAXiKVWbMRkHUW7HJVFWFx3Z+TjJ21IeOdmy6otibyQauQxWUCVJl7JOmFuEC/XlJkQL0JdLXpkYHbTromgRz1e4NKDNcFtwGuNlhHHIq+cyyRtp6ksBY4rG4433QlDYtiIsrNBr8bzZr8Hm4x3ylYRbtocCPuINqK+FOVfcrTszr+U2J/PbBe0G3nz6KNHfn5ho8m1DbsZm3msnoj5IIFfy6tefz+yL0LUbjOEeAMb1gf+qXSXNOxZDTH1AoSPXr5nRJxep9b32e2lxl9IpOAXDQ4JZe0uA/HW0fnanEnYcOHAVh1QRjPdq9eZl3cUMKhtRiYcy4YGHQgjrI07A+0S44eink4HaknVaDIHrz1r1jHjmiApcRGeDrWzcXoGG4WJ3uRTspIyfbTVQW9M5Z4H7WCPz0fX1K+JKJsrk+ot6oG/zZpRvAtO1dJyL30vP9zPJ5iPCzDMdFJ6I6/0doqHUXGws,iv:iuGQpiqfImaiABselgCSbRYUY6x2PYsJoXlbPndG6FA=,tag:FTXzPeL2tvfECJawCdsfCQ==,type:str] + key: ENC[AES256_GCM,data:pDSqpBWMaAR05pukZllr0UfunEEp1p+zGXTWNLbunrYlWc+1BT/4uLsk65iy5OXM/o3c+MJCyUqxyNBr+Z0fAoiqM8Qy8HPq5WZC/WWXtR4SZcBnmTHd7RwiGvIepw3omjeJzJ3hJ7Y4/8K+EmSftdXPOusGA0/36AAAkeH/Fd/NxPhBFgFc9p/oCfyed/oSiJkXelH5CPO6a8qTNgb0znKk+CDxZUOiffEfpQaYT/AwMiMP,iv:INTrpATj8TeA6BXx0nH/lXUQw21TSQnCI2pbNrfRlcY=,tag:qtR7mo/60iI48lOQfJXEsw==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaS9kQ0Y1Q1l5WktMZllK + WXo0U0I2UytlTFMyVnVrN0hBV2xUeG1raW1zCkZCODNBS0w1SUpqaUtlS3NPMnEw + UnkwOTJQTkt0aUhFanlNWjI4VHFEZWcKLS0tIDVRZ0I2Y0plRU9NRkJwYnJyUjFa + UTFrN1VETHBtV1NjK2xVQzZxMHVaZDQKPoDlgLPOoDAu1bCAbQnBo2i7u8v/fV4O + 6QNDDn4RDxt1kGDNuvCeXkWtnIP1Vcw/0Z8DNlqgOGgjY+oLsKY7fA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-05-24T14:59:54Z" + mac: ENC[AES256_GCM,data:K+gdYNcbMQs3/IZuoziE4XOpuAro7eQdvv25S5/nI595DZyPW3dvH0G9IZObvzJWCwoYI6mVbI1jvidA3lhFbD1n9vyGFmOh15A80ll1QX+jjKEJFqN1230jwZaosaaZ8Eqb1gnjt+0wvTS/PpuwVP4SJL7iTY8p0aGQYK5YAa4=,iv:1f+fgn+Uq2+cM7h4TwZl74+AlmYpBTSyk/1J0gE8lGU=,tag:n+9uKWqvjIG4yCLNOHUCEw==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.8.1 diff --git a/kubernetes/flux/apps.yaml b/kubernetes/flux/apps.yaml new file mode 100644 index 00000000..6d260916 --- /dev/null +++ b/kubernetes/flux/apps.yaml @@ -0,0 +1,57 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cluster-apps + namespace: flux-system +spec: + interval: 30m + path: ./kubernetes/apps + prune: true + sourceRef: + kind: GitRepository + name: home-kubernetes + decryption: + provider: sops + secretRef: + name: sops-age + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-settings + - kind: Secret + name: cluster-secrets + - kind: ConfigMap + name: cluster-settings-user + optional: true + - kind: Secret + name: cluster-secrets-user + optional: true + patches: + - patch: |- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: not-used + spec: + decryption: + provider: sops + secretRef: + name: sops-age + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-settings + - kind: Secret + name: cluster-secrets + - kind: ConfigMap + name: cluster-settings-user + optional: true + - kind: Secret + name: cluster-secrets-user + optional: true + target: + group: kustomize.toolkit.fluxcd.io + kind: Kustomization + labelSelector: substitution.flux.home.arpa/disabled notin (true) diff --git a/kubernetes/flux/config/cluster.yaml b/kubernetes/flux/config/cluster.yaml new file mode 100644 index 00000000..0c6f3785 --- /dev/null +++ b/kubernetes/flux/config/cluster.yaml @@ -0,0 +1,42 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/gitrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: home-kubernetes + namespace: flux-system +spec: + interval: 30m + url: "https://github.com/MaksimShakavin/flux-homelab.git" + ref: + branch: "main" + ignore: | + # exclude all + /* + # include kubernetes directory + !/kubernetes +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cluster + namespace: flux-system +spec: + interval: 30m + path: ./kubernetes/flux + prune: true + wait: false + sourceRef: + kind: GitRepository + name: home-kubernetes + decryption: + provider: sops + secretRef: + name: sops-age + postBuild: + substituteFrom: + - kind: ConfigMap + name: cluster-settings + - kind: Secret + name: cluster-secrets diff --git a/kubernetes/flux/config/flux.yaml b/kubernetes/flux/config/flux.yaml new file mode 100644 index 00000000..f714a906 --- /dev/null +++ b/kubernetes/flux/config/flux.yaml @@ -0,0 +1,88 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/ocirepository_v1beta2.json +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: flux-manifests + namespace: flux-system +spec: + interval: 10m + url: oci://ghcr.io/fluxcd/flux-manifests + ref: + tag: v2.3.0 +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: flux + namespace: flux-system +spec: + interval: 10m + path: ./ + prune: true + wait: true + sourceRef: + kind: OCIRepository + name: flux-manifests + patches: + # Remove the network policies that does not work with k3s + - patch: | + $patch: delete + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: not-used + target: + group: networking.k8s.io + kind: NetworkPolicy + # Increase the number of reconciliations that can be performed in parallel and bump the resources limits + # https://fluxcd.io/flux/cheatsheets/bootstrap/#increase-the-number-of-workers + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --concurrent=8 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --kube-api-qps=500 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --kube-api-burst=1000 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --requeue-dependency=5s + target: + kind: Deployment + name: (kustomize-controller|helm-controller|source-controller) + - patch: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: not-used + spec: + template: + spec: + containers: + - name: manager + resources: + limits: + cpu: 2000m + memory: 2Gi + target: + kind: Deployment + name: (kustomize-controller|helm-controller|source-controller) + # Enable Helm near OOM detection + # https://fluxcd.io/flux/cheatsheets/bootstrap/#enable-helm-near-oom-detection + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --feature-gates=OOMWatch=true + - op: add + path: /spec/template/spec/containers/0/args/- + value: --oom-watch-memory-threshold=95 + - op: add + path: /spec/template/spec/containers/0/args/- + value: --oom-watch-interval=500ms + target: + kind: Deployment + name: helm-controller diff --git a/kubernetes/flux/config/kustomization.yaml b/kubernetes/flux/config/kustomization.yaml new file mode 100644 index 00000000..762bc44e --- /dev/null +++ b/kubernetes/flux/config/kustomization.yaml @@ -0,0 +1,6 @@ +--- + +kind: Kustomization +resources: + - ./flux.yaml + - ./cluster.yaml diff --git a/kubernetes/flux/repositories/git/kustomization.yaml b/kubernetes/flux/repositories/git/kustomization.yaml new file mode 100644 index 00000000..8fb7c142 --- /dev/null +++ b/kubernetes/flux/repositories/git/kustomization.yaml @@ -0,0 +1,5 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: [] diff --git a/kubernetes/flux/repositories/helm/actions-runner-controller.yaml b/kubernetes/flux/repositories/helm/actions-runner-controller.yaml new file mode 100644 index 00000000..54fa67be --- /dev/null +++ b/kubernetes/flux/repositories/helm/actions-runner-controller.yaml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: actions-runner-controller + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/actions/actions-runner-controller-charts diff --git a/kubernetes/flux/repositories/helm/backube.yaml b/kubernetes/flux/repositories/helm/backube.yaml new file mode 100644 index 00000000..acdca6dc --- /dev/null +++ b/kubernetes/flux/repositories/helm/backube.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: backube + namespace: flux-system +spec: + interval: 1h + url: https://backube.github.io/helm-charts diff --git a/kubernetes/flux/repositories/helm/bjw-s.yaml b/kubernetes/flux/repositories/helm/bjw-s.yaml new file mode 100644 index 00000000..c32ccd8d --- /dev/null +++ b/kubernetes/flux/repositories/helm/bjw-s.yaml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: bjw-s + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/bjw-s/helm diff --git a/kubernetes/flux/repositories/helm/cilium.yaml b/kubernetes/flux/repositories/helm/cilium.yaml new file mode 100644 index 00000000..d6736ba4 --- /dev/null +++ b/kubernetes/flux/repositories/helm/cilium.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: cilium + namespace: flux-system +spec: + interval: 1h + url: https://helm.cilium.io diff --git a/kubernetes/flux/repositories/helm/cloudnative-pg.yaml b/kubernetes/flux/repositories/helm/cloudnative-pg.yaml new file mode 100644 index 00000000..4b2f0e61 --- /dev/null +++ b/kubernetes/flux/repositories/helm/cloudnative-pg.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: cloudnative-pg + namespace: flux-system +spec: + interval: 2h + url: https://cloudnative-pg.github.io/charts diff --git a/kubernetes/flux/repositories/helm/coredns.yaml b/kubernetes/flux/repositories/helm/coredns.yaml new file mode 100644 index 00000000..ed0bb65a --- /dev/null +++ b/kubernetes/flux/repositories/helm/coredns.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: coredns + namespace: flux-system +spec: + interval: 2h + url: https://coredns.github.io/helm diff --git a/kubernetes/flux/repositories/helm/democratic-csi.yaml b/kubernetes/flux/repositories/helm/democratic-csi.yaml new file mode 100644 index 00000000..a7fdc024 --- /dev/null +++ b/kubernetes/flux/repositories/helm/democratic-csi.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: democratic-csi + namespace: flux-system +spec: + interval: 2h + url: https://democratic-csi.github.io/charts/ diff --git a/kubernetes/flux/repositories/helm/external-dns.yaml b/kubernetes/flux/repositories/helm/external-dns.yaml new file mode 100644 index 00000000..f38c48ad --- /dev/null +++ b/kubernetes/flux/repositories/helm/external-dns.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: external-dns + namespace: flux-system +spec: + interval: 1h + url: https://kubernetes-sigs.github.io/external-dns diff --git a/kubernetes/flux/repositories/helm/external-secrets.yaml b/kubernetes/flux/repositories/helm/external-secrets.yaml new file mode 100644 index 00000000..2acd768a --- /dev/null +++ b/kubernetes/flux/repositories/helm/external-secrets.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: external-secrets + namespace: flux-system +spec: + interval: 2h + url: https://charts.external-secrets.io diff --git a/kubernetes/flux/repositories/helm/grafana.yaml b/kubernetes/flux/repositories/helm/grafana.yaml new file mode 100644 index 00000000..eb1a6fb0 --- /dev/null +++ b/kubernetes/flux/repositories/helm/grafana.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: grafana + namespace: flux-system +spec: + interval: 2h + url: https://grafana.github.io/helm-charts diff --git a/kubernetes/flux/repositories/helm/ingress-nginx.yaml b/kubernetes/flux/repositories/helm/ingress-nginx.yaml new file mode 100644 index 00000000..492d9cdf --- /dev/null +++ b/kubernetes/flux/repositories/helm/ingress-nginx.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: ingress-nginx + namespace: flux-system +spec: + interval: 1h + url: https://kubernetes.github.io/ingress-nginx diff --git a/kubernetes/flux/repositories/helm/intel.yaml b/kubernetes/flux/repositories/helm/intel.yaml new file mode 100644 index 00000000..fb2c66b0 --- /dev/null +++ b/kubernetes/flux/repositories/helm/intel.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: intel + namespace: flux-system +spec: + interval: 2h + url: https://intel.github.io/helm-charts diff --git a/kubernetes/flux/repositories/helm/jetstack.yaml b/kubernetes/flux/repositories/helm/jetstack.yaml new file mode 100644 index 00000000..b513441b --- /dev/null +++ b/kubernetes/flux/repositories/helm/jetstack.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: jetstack + namespace: flux-system +spec: + interval: 1h + url: https://charts.jetstack.io diff --git a/kubernetes/flux/repositories/helm/k8s-gateway.yaml b/kubernetes/flux/repositories/helm/k8s-gateway.yaml new file mode 100644 index 00000000..428b19f9 --- /dev/null +++ b/kubernetes/flux/repositories/helm/k8s-gateway.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: k8s-gateway + namespace: flux-system +spec: + interval: 1h + url: https://ori-edge.github.io/k8s_gateway diff --git a/kubernetes/flux/repositories/helm/kustomization.yaml b/kubernetes/flux/repositories/helm/kustomization.yaml new file mode 100644 index 00000000..ce911ede --- /dev/null +++ b/kubernetes/flux/repositories/helm/kustomization.yaml @@ -0,0 +1,29 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./bjw-s.yaml + - ./cilium.yaml + - ./external-dns.yaml + - ./ingress-nginx.yaml + - ./k8s-gateway.yaml + - ./prometheus-community.yaml + - ./stevehipwell.yaml + - ./grafana.yaml + - ./portainer-charts.yaml + - ./external-secrets.yaml + - ./jetstack.yaml + - ./metrics-server.yaml + - ./stakater.yaml + - ./cloudnative-pg.yaml + - ./longhorn.yaml + - ./backube.yaml + - ./piraeus.yaml + - ./democratic-csi.yaml + - ./node-feature-discovery.yaml + - ./intel.yaml + - ./actions-runner-controller.yaml + - ./spegel.yaml + - ./coredns.yaml + - ./postfinance.yaml diff --git a/kubernetes/flux/repositories/helm/longhorn.yaml b/kubernetes/flux/repositories/helm/longhorn.yaml new file mode 100644 index 00000000..c0abf7f0 --- /dev/null +++ b/kubernetes/flux/repositories/helm/longhorn.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: longhorn + namespace: flux-system +spec: + interval: 1h + url: https://charts.longhorn.io diff --git a/kubernetes/flux/repositories/helm/metrics-server.yaml b/kubernetes/flux/repositories/helm/metrics-server.yaml new file mode 100644 index 00000000..62a6473b --- /dev/null +++ b/kubernetes/flux/repositories/helm/metrics-server.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: metrics-server + namespace: flux-system +spec: + interval: 1h + url: https://kubernetes-sigs.github.io/metrics-server diff --git a/kubernetes/flux/repositories/helm/node-feature-discovery.yaml b/kubernetes/flux/repositories/helm/node-feature-discovery.yaml new file mode 100644 index 00000000..5e45d5a8 --- /dev/null +++ b/kubernetes/flux/repositories/helm/node-feature-discovery.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: node-feature-discovery + namespace: flux-system +spec: + interval: 2h + url: https://kubernetes-sigs.github.io/node-feature-discovery/charts diff --git a/kubernetes/flux/repositories/helm/piraeus.yaml b/kubernetes/flux/repositories/helm/piraeus.yaml new file mode 100644 index 00000000..ebd0fa59 --- /dev/null +++ b/kubernetes/flux/repositories/helm/piraeus.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: piraeus + namespace: flux-system +spec: + interval: 1h + url: https://piraeus.io/helm-charts diff --git a/kubernetes/flux/repositories/helm/portainer-charts.yaml b/kubernetes/flux/repositories/helm/portainer-charts.yaml new file mode 100644 index 00000000..b7cc33e4 --- /dev/null +++ b/kubernetes/flux/repositories/helm/portainer-charts.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: portainer-charts + namespace: flux-system +spec: + interval: 1h + url: https://portainer.github.io/k8s/ diff --git a/kubernetes/flux/repositories/helm/postfinance.yaml b/kubernetes/flux/repositories/helm/postfinance.yaml new file mode 100644 index 00000000..015568bf --- /dev/null +++ b/kubernetes/flux/repositories/helm/postfinance.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: postfinance + namespace: flux-system +spec: + interval: 2h + url: https://postfinance.github.io/kubelet-csr-approver diff --git a/kubernetes/flux/repositories/helm/prometheus-community.yaml b/kubernetes/flux/repositories/helm/prometheus-community.yaml new file mode 100644 index 00000000..78c4f0c0 --- /dev/null +++ b/kubernetes/flux/repositories/helm/prometheus-community.yaml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: prometheus-community + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/prometheus-community/charts diff --git a/kubernetes/flux/repositories/helm/spegel.yaml b/kubernetes/flux/repositories/helm/spegel.yaml new file mode 100644 index 00000000..0350b3ad --- /dev/null +++ b/kubernetes/flux/repositories/helm/spegel.yaml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: spegel + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/spegel-org/helm-charts diff --git a/kubernetes/flux/repositories/helm/stakater.yaml b/kubernetes/flux/repositories/helm/stakater.yaml new file mode 100644 index 00000000..bcc3304b --- /dev/null +++ b/kubernetes/flux/repositories/helm/stakater.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: stakater + namespace: flux-system +spec: + interval: 1h + url: https://stakater.github.io/stakater-charts diff --git a/kubernetes/flux/repositories/helm/stevehipwell.yaml b/kubernetes/flux/repositories/helm/stevehipwell.yaml new file mode 100644 index 00000000..832684b7 --- /dev/null +++ b/kubernetes/flux/repositories/helm/stevehipwell.yaml @@ -0,0 +1,11 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: stevehipwell + namespace: flux-system +spec: + type: oci + interval: 5m + url: oci://ghcr.io/stevehipwell/helm-charts diff --git a/kubernetes/flux/repositories/kustomization.yaml b/kubernetes/flux/repositories/kustomization.yaml new file mode 100644 index 00000000..ae7e0ad4 --- /dev/null +++ b/kubernetes/flux/repositories/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./git + - ./helm + - ./oci diff --git a/kubernetes/flux/repositories/oci/kustomization.yaml b/kubernetes/flux/repositories/oci/kustomization.yaml new file mode 100644 index 00000000..8fb7c142 --- /dev/null +++ b/kubernetes/flux/repositories/oci/kustomization.yaml @@ -0,0 +1,5 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: [] diff --git a/kubernetes/flux/vars/cluster-secrets.sops.yaml b/kubernetes/flux/vars/cluster-secrets.sops.yaml new file mode 100644 index 00000000..4bf6f7eb --- /dev/null +++ b/kubernetes/flux/vars/cluster-secrets.sops.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Secret +metadata: + name: cluster-secrets + namespace: flux-system +stringData: + SECRET_EXAMPLE: ENC[AES256_GCM,data:9YUm8BOmLfWPmPSnFfv4yqQ8whU4TaZbTQBo3wRspApYvWNqmroZ3EN6pGHcBLxDHGR2JhQ8SrDNps0dZwOJfk/a5Me2GRoDsNalx5zGF8bcHtNoPRrG1dNIw0heuw==,iv:QG3FEJh4TBSCo/fObNQqqGYWUKrXeNlwWcUaGXo0hxU=,tag:lSPMvF/VtrLxgf8mCQEvQw==,type:str] + SECRET_DOMAIN: ENC[AES256_GCM,data:wIYcdMzbnNTbcV2LJA==,iv:0Uv+rP/oyYciSkNb7RQ5mF4iqHPVP1uBtT34ZhLowUc=,tag:+XQceahKYlYPKgeTnDiKSg==,type:str] + SECRET_ACME_EMAIL: ENC[AES256_GCM,data:RfWJZgnzIDbI2purDft7jjrrGJhjDdUxwg==,iv:XVTUbnM8CuXEe0UaZ4LadaPnj/pAC2YB+n34lDYC9mw=,tag:WlO3dsmY5SSAG1VQIqWrow==,type:str] + SECRET_CLOUDFLARE_TUNNEL_ID: ENC[AES256_GCM,data:E/e0M+QKytLvZgD9U6egSGwV4qbpKVUV1ZUKaq2bd9S2FoDi,iv:CBzu//vbiSr3sxNd3Wz72o6/csx3AEU9lSN03Fzf4V8=,tag:ESOgRxclJqa/I+j30HwMhg==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0d05DamV1TWNOc0o1aXoz + Q0ltajRxWmEraDlsRE4veFhZdXpqNFZBM2tBCjFPZnhQNGlNQTUxQSt0U3oreFZJ + T1RaSWVzVTNDUWFuOHBUbGNmbFdBSk0KLS0tIG9UNUlSdFd0emRuN2NGVzZiUjZy + VGx5R1lKZU1aejkvUjZDcytvQkczalkKjKsV4X9HnVtQG80TpVctxHdio//g3vJN + oD7AdZ0iGC3Z0W8VD9N7kLYIpB4BvC1QVP2cEVw6YW/9x+M2Y/to0w== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-02-17T21:45:25Z" + mac: ENC[AES256_GCM,data:RUzvexrVSeRFAmzJvLKQkXdFft3erBN9ZOUPgMAl+RJr0cSu/x5EC2EyogxxD4KY9x25HeVI/u986rgovwWkm95vDVV8BT/xWk+x1QgsuUguY2eAY2tJaebgS4CoE607vUj9M7g0EfytoWpAKrmWuGFn+vRVfQfBVntYCHnoJhs=,iv:sIM7jCiYfyfcVR+HsG/jULWrYIXxIktd/hOx/v/3wcM=,tag:Yc/LoFIvemzf8/jJDj7+ow==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.7.3 diff --git a/kubernetes/flux/vars/cluster-settings.yaml b/kubernetes/flux/vars/cluster-settings.yaml new file mode 100644 index 00000000..cd25252f --- /dev/null +++ b/kubernetes/flux/vars/cluster-settings.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cluster-settings + namespace: flux-system +data: + TIMEZONE: "Europe/Warsaw" + CLUSTER_CIDR: "10.69.0.0/16" + NODE_CIDR: "192.168.20.0/24" + CLUSTER_LB_QBITTORRENT: "192.168.20.64" + CLUSTER_LB_PLEX: "192.168.20.65" + NAS_URL: "192.168.20.5" + RPI_URL: "192.168.20.3" + NAS_PATH: "/volume1/kubernetes" diff --git a/kubernetes/flux/vars/kustomization.yaml b/kubernetes/flux/vars/kustomization.yaml new file mode 100644 index 00000000..9ea91972 --- /dev/null +++ b/kubernetes/flux/vars/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./cluster-settings.yaml + - ./cluster-secrets.sops.yaml diff --git a/kubernetes/talos/clusterconfig/talosconfig b/kubernetes/talos/clusterconfig/talosconfig new file mode 100644 index 00000000..e35e5db5 --- /dev/null +++ b/kubernetes/talos/clusterconfig/talosconfig @@ -0,0 +1,2 @@ +context: "" +contexts: {} diff --git a/kubernetes/templates/gatus/external/configmap.yaml b/kubernetes/templates/gatus/external/configmap.yaml new file mode 100644 index 00000000..34725b2c --- /dev/null +++ b/kubernetes/templates/gatus/external/configmap.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: "${APP}-gatus-ep" + labels: + gatus.io/enabled: "true" +data: + config.yaml: | + endpoints: + - name: "${APP}" + group: external + url: "https://${GATUS_SUBDOMAIN:-${APP}}.${SECRET_DOMAIN}${GATUS_PATH:-/}" + interval: 1m + client: + dns-resolver: tcp://1.1.1.1:53 + conditions: + - "[STATUS] == ${GATUS_STATUS:-200}" + alerts: + - type: discord diff --git a/kubernetes/templates/gatus/external/kustomization.yaml b/kubernetes/templates/gatus/external/kustomization.yaml new file mode 100644 index 00000000..e09060b9 --- /dev/null +++ b/kubernetes/templates/gatus/external/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./configmap.yaml diff --git a/kubernetes/templates/gatus/internal/configmap.yaml b/kubernetes/templates/gatus/internal/configmap.yaml new file mode 100644 index 00000000..039321aa --- /dev/null +++ b/kubernetes/templates/gatus/internal/configmap.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: "${APP}-gatus-ep" + labels: + gatus.io/enabled: "true" +data: + config.yaml: | + endpoints: + - name: "${APP}" + group: internal + url: 1.1.1.1 + interval: 1m + ui: + hide-hostname: true + hide-url: true + dns: + query-name: "${GATUS_SUBDOMAIN:-${APP}}.${SECRET_DOMAIN}" + query-type: A + conditions: + - "len([BODY]) == 0" + alerts: + - type: discord diff --git a/kubernetes/templates/gatus/internal/kustomization.yaml b/kubernetes/templates/gatus/internal/kustomization.yaml new file mode 100644 index 00000000..e09060b9 --- /dev/null +++ b/kubernetes/templates/gatus/internal/kustomization.yaml @@ -0,0 +1,6 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./configmap.yaml diff --git a/kubernetes/templates/volsync/claim.yaml b/kubernetes/templates/volsync/claim.yaml new file mode 100644 index 00000000..d492771d --- /dev/null +++ b/kubernetes/templates/volsync/claim.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: "${VOLSYNC_CLAIM:-${APP}}" +spec: + accessModes: ["${VOLSYNC_ACCESSMODES:-ReadWriteOnce}"] + dataSourceRef: + kind: ReplicationDestination + apiGroup: volsync.backube + name: "${APP}-bootstrap" + resources: + requests: + storage: "${VOLSYNC_CAPACITY}" + storageClassName: "${VOLSYNC_STORAGECLASS:-longhorn-snapshot}" diff --git a/kubernetes/templates/volsync/kustomization.yaml b/kubernetes/templates/volsync/kustomization.yaml new file mode 100644 index 00000000..793a6d4a --- /dev/null +++ b/kubernetes/templates/volsync/kustomization.yaml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./secret.sops.yaml + - ./claim.yaml + - ./minio.yaml diff --git a/kubernetes/templates/volsync/minio.yaml b/kubernetes/templates/volsync/minio.yaml new file mode 100644 index 00000000..51262499 --- /dev/null +++ b/kubernetes/templates/volsync/minio.yaml @@ -0,0 +1,51 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.devbu.io/volsync.backube/replicationsource_v1alpha1.json +apiVersion: volsync.backube/v1alpha1 +kind: ReplicationSource +metadata: + name: "${APP}" +spec: + sourcePVC: "${CLAIM:-${APP}}" + trigger: + schedule: "0 * * * *" #every hour + restic: + copyMethod: "${VOLSYNC_COPYMETHOD:-Snapshot}" + pruneIntervalDays: 7 + repository: "${APP}-volsync-secret" + volumeSnapshotClassName: "${VOLSYNC_SNAPSHOTCLASS:-longhorn-snapclass}" + cacheCapacity: "${VOLSYNC_CACHE_CAPACITY:-8Gi}" + cacheStorageClassName: "${VOLSYNC_CACHE_SNAPSHOTCLASS:-local-hostpath}" + cacheAccessModes: ["${VOLSYNC_CACHE_ACCESSMODES:-ReadWriteOnce}"] + storageClassName: "${VOLSYNC_STORAGECLASS:-longhorn-snapshot}" + accessModes: ["${VOLSYNC_ACCESSMODES:-ReadWriteOnce}"] + moverSecurityContext: + runAsUser: "${VOLSYNC_UID:-568}" + runAsGroup: "${VOLSYNC_GID:-568}" + fsGroup: "${VOLSYNC_GID:-568}" + retain: + hourly: 24 + daily: 7 + weekly: 5 +--- +# yaml-language-server: $schema=https://kubernetes-schemas.devbu.io/volsync.backube/replicationdestination_v1alpha1.json +apiVersion: volsync.backube/v1alpha1 +kind: ReplicationDestination +metadata: + name: "${APP}-bootstrap" +spec: + trigger: + manual: restore-once + restic: + repository: "${APP}-volsync-secret" + copyMethod: Snapshot # must be Snapshot + volumeSnapshotClassName: "${VOLSYNC_SNAPSHOTCLASS:-longhorn-snapclass}" + cacheStorageClassName: "${VOLSYNC_CACHE_SNAPSHOTCLASS:-local-hostpath}" + cacheAccessModes: ["${VOLSYNC_CACHE_ACCESSMODES:-ReadWriteOnce}"] + cacheCapacity: "${VOLSYNC_CACHE_CAPACITY:-8Gi}" + storageClassName: "${VOLSYNC_STORAGECLASS:-longhorn-snapshot}" + accessModes: ["${VOLSYNC_ACCESSMODES:-ReadWriteOnce}"] + capacity: "${VOLSYNC_CAPACITY}" + moverSecurityContext: + runAsUser: "${VOLSYNC_UID:-568}" + runAsGroup: "${VOLSYNC_GID:-568}" + fsGroup: "${VOLSYNC_GID:-568}" diff --git a/kubernetes/templates/volsync/secret.sops.yaml b/kubernetes/templates/volsync/secret.sops.yaml new file mode 100644 index 00000000..4ce6c6eb --- /dev/null +++ b/kubernetes/templates/volsync/secret.sops.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ${APP}-volsync-secret +type: Opaque +stringData: + #ENC[AES256_GCM,data:4r+r5roqSAxfBgtvUL+4PC0J7g==,iv:BEHtl0dVZH12As/cfFApfvXizNIYVQRUHbqXbZSVxwU=,tag:ooSC5NGx0b5kyLu0SeWgRA==,type:comment] + RESTIC_REPOSITORY: ENC[AES256_GCM,data:3QZlzc+3cUZnZyfBU7X2ATXQsSR/FPUOsETtDwgKceI=,iv:Nkzrllwf2TWjUAK7m0G/u9opYaTmGagDyBxo6GMFM3w=,tag:wHtjyqZD6e6sCDHEh+h2HA==,type:str] + #ENC[AES256_GCM,data:dhkzp+nuSvs1YpWlZJzRHrZW06tH1uGaJ87P7zwZ,iv:4Ix8Uuy1+yzCkK6rPIH0VXMiPmJRTOuSaIrUphzhEwQ=,tag:4b/WROlAdcQR5utwXJmKKQ==,type:comment] + RESTIC_PASSWORD: ENC[AES256_GCM,data:v9Sx4UJrTsgzCZ2DXJab7A==,iv:84oVrjzPYDunXYWZdfooCA2/R6YhD6joAHGunrqBxC8=,tag:wHBdRvO+3QMcwkwEl2gNsw==,type:str] + #ENC[AES256_GCM,data:1HuwOiAG75sZBJm/F/UsyKZgU1I/DbGTdrhy+pdLg2MjMlMm7YA1NlU=,iv:S/csA+nGkf2oaYNnxKcD4aNjYlgyBj/zl+p7ty9O7Hs=,tag:txcjP4OFGF70BKMeYJyibQ==,type:comment] + #ENC[AES256_GCM,data:hD66uez8UQJxgVKRJ6XEjdtT4pus4KIGbwWrKbWnQpTouBwi6OXlY/yYmMiSWZocJ3lTuOQkH3g9vwLM+QfjLs6gp/E3uQ==,iv:wzLyjng0HKU1PzHD43BAR/EtgaOu7T7tZnF3Y80YkV8=,tag:MN/nQoO74U8kXAkQEaoTiQ==,type:comment] + AWS_ACCESS_KEY_ID: ENC[AES256_GCM,data:SFhY+1NbjfGugBQhASXWTSOSeLk=,iv:PHKsy4xU8Ban68X+n/5Fh9F0vaTIBUXjalpIxaE5tJs=,tag:xe1ma8LZu+K5aDiWEqXd2A==,type:str] + AWS_SECRET_ACCESS_KEY: ENC[AES256_GCM,data:+pbnoHZWLM1SRIxCyt3O7EiB6+RF21NgBymf9mR2NVA6IVeO1XEk7A==,iv:VaN/jhN+zr4/gDkG1SNwBmmHKlP3ZM+CVRYda50LRww=,tag:/yu6fA2748pLN8GK+9iOZg==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1k5xl02aujw4rsgghnnd0sdymmwd095w5nqgjvf76warwrdc0uqpqsm2x8m + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrQVNNUXo1bnVWQW9aWHds + NXNNMUF1QVNxMFlBdUV0ZGtHdWZGMnNrYzNZCktwZmI2OTZrclJlVU5LNlg5bGpl + amc5MEdTb01xNG1UTGVNREE1WmUvRUkKLS0tIHg1eHBaT2RjSDEvaUhTYXB3Rzd3 + YUVOVFdFRFhLYm1MaW5JQXFmYldMYk0KA7BFGNUu7bLkJMJR9BtOPEKuTcVksOOP + sOyKkPQ1feSqEOmr+9iIOQRsPkbOHnUBmodrCt3exgWKGK/et2cpcA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-02-24T16:16:22Z" + mac: ENC[AES256_GCM,data:AZKPKgTJuYTfjeOi0csjKLUwkMbT9KDFosMJ/hRnPSww4xFFpjmZqEbhtMmipbLM8KmlAjIoGsE9vmKAQjCH1wooLpY/4U6KMqIzF3NY0b6wjtP/+j6Alqxdt6wvbrIXUZ9AuLUYvrJ0ltaci3ZC/sgo8Fj64yi06NuLWHtbS+w=,iv:CTAWc54lslkF61iBxu/mx7dXrIok2aKtZ8BxI3/Ojfs=,tag:DUuCZFY2PtG9szztUYf6uw==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.7.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..bdc5b50e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +bcrypt==4.1.3 +cloudflare==2.20.0 +email-validator==2.1.1 +makejinja==2.6.0 +netaddr==1.2.1 +passlib==1.7.4 diff --git a/scripts/kubeconform.sh b/scripts/kubeconform.sh new file mode 100755 index 00000000..a69308b1 --- /dev/null +++ b/scripts/kubeconform.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -o errexit +set -o pipefail + +KUBERNETES_DIR=$1 + +[[ -z "${KUBERNETES_DIR}" ]] && echo "Kubernetes location not specified" && exit 1 + +kustomize_args=("--load-restrictor=LoadRestrictionsNone") +kustomize_config="kustomization.yaml" +kubeconform_args=( + "-strict" + "-ignore-missing-schemas" + "-skip" + "Secret" + "-schema-location" + "default" + "-schema-location" + "https://kubernetes-schemas.pages.dev/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json" + "-verbose" +) + +echo "=== Validating standalone manifests in ${KUBERNETES_DIR}/flux ===" +find "${KUBERNETES_DIR}/flux" -maxdepth 1 -type f -name '*.yaml' -print0 | while IFS= read -r -d $'\0' file; + do + kubeconform "${kubeconform_args[@]}" "${file}" + if [[ ${PIPESTATUS[0]} != 0 ]]; then + exit 1 + fi +done + +echo "=== Validating kustomizations in ${KUBERNETES_DIR}/flux ===" +find "${KUBERNETES_DIR}/flux" -type f -name $kustomize_config -print0 | while IFS= read -r -d $'\0' file; + do + echo "=== Validating kustomizations in ${file/%$kustomize_config} ===" + kustomize build "${file/%$kustomize_config}" "${kustomize_args[@]}" | \ + kubeconform "${kubeconform_args[@]}" + if [[ ${PIPESTATUS[0]} != 0 ]]; then + exit 1 + fi +done + +echo "=== Validating kustomizations in ${KUBERNETES_DIR}/apps ===" +find "${KUBERNETES_DIR}/apps" -type f -name $kustomize_config -print0 | while IFS= read -r -d $'\0' file; + do + echo "=== Validating kustomizations in ${file/%$kustomize_config} ===" + kustomize build "${file/%$kustomize_config}" "${kustomize_args[@]}" | \ + kubeconform "${kubeconform_args[@]}" + if [[ ${PIPESTATUS[0]} != 0 ]]; then + exit 1 + fi +done diff --git a/talosconfig b/talosconfig new file mode 100644 index 00000000..e35e5db5 --- /dev/null +++ b/talosconfig @@ -0,0 +1,2 @@ +context: "" +contexts: {}